From a7374e660130008188d10d7eff3505ab721c3ccc Mon Sep 17 00:00:00 2001 From: Jonathan Addington Date: Sat, 21 Dec 2024 04:12:28 +0000 Subject: [PATCH] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit 84eeb69596e074f5d0c3a7dcc852ccad8ffd8169 Author: Jonathan Addington Date: Sat Dec 21 03:54:18 2024 +0000 fix: removed e2b_symbol.png in wrong place commit 7ad7f5008079523d242ec181e59abb0d878a4706 Author: Jonathan Addington Date: Sat Dec 21 03:29:47 2024 +0000 chore: enhance sandbox creation logic with template fallback and detailed response message commit d4d46e96de313fd1ca292fc1f3cf53152dd8648e Author: Jonathan Addington Date: Sat Dec 21 03:37:41 2024 +0000 chore: require authentication for E2BCode plugin and add e2b logo commit ac507bd79873aec3c765eabe8ce2b9a611910596 Author: Jonathan Addington Date: Fri Dec 20 14:34:18 2024 +0000 Merge upstream into jm-production commit 332a947cdf2e88399005b210b898f2e7ae4ed6d8 Author: Jonathan Addington Date: Thu Dec 19 22:02:30 2024 +0000 feat: implement output truncation for large responses in E2BCode commit 78caf5c451103c3e5ce1cbb4f37a1775681d5025 Author: Jonathan Addington Date: Wed Dec 18 16:40:30 2024 +0000 feat: add support for e2b sanedbox templates commit 374822c427f4cc2719849dc123b6eab858306d6e Author: Jonathan Addington Date: Tue Dec 17 14:36:23 2024 +0000 refactor: update help for LLM commit 55350ae9fb8215e226b5e8ab50ab4441e899a793 Author: JM Addington Date: Mon Dec 16 13:41:28 2024 -0500 Merge latest New/feature/e2bcode (#20) * ๐Ÿ™Œ a11y: Accessibility Improvements (#4978) * ๐Ÿ”ƒ fix: Safeguard against null token in SSE refresh token handling * ๐Ÿ”ƒ fix: Update import path for AnnounceOptions in LiveAnnouncer component * ๐Ÿ”ƒ a11y: Add aria-live attribute for accessibility in error messages * fix: prevent double screen reader notification for toast * ๐Ÿ”ƒ a11y: Enhance accessibility for main menus and buttons with ARIA roles and labels * refactor: better alt text for logo on login page #4095 * refactor: remove unused import for DropdownNoState in Voices component * fix: Focus management issue in the Export Options Modal #4100 * ๐Ÿ› fix: Enforced Model Spec Icons/Labels and Agent Descriptions (#4979) * fix: Previous convos missing model spec info when enforce is set to `true` #4749 * refactor: Include description field in agent list response * docs: enhance E2BCode documentation with subprocess usage and file transfer instructions * ๐Ÿ”ง fix: Add modelLabel to OpenAIClient and PluginsClient options (#4995) * fix: change description too-long for tools and move to new help action * refactor: move some if stmts into the switfh block * feat: Add command_run * feat: add start_server command * refactor: refactor how we list and kill sandboxes to work better * refactor: add clearer instructions on max commands limit to avoid recursion errors with langchain * refactor: change how we handle sandbox kills * refactor: add processinfo(), system_install and comment out shell, read more... The LLMs are consistently using the shell function instead of the command_run and end up timing out on the api as a result. Taking away the shell functionality is a test at this point to see if we get better performance. * refactor: add user, cwd to create; set default timeout to 60mins --------- Co-authored-by: Danny Avila commit 62facd16fea1aecc1ae20a4a89cb534cc351796c Author: Jonathan Addington Date: Wed Dec 18 16:00:56 2024 +0000 fix: ๐Ÿ›Add missing comma for FluxAPI in tools index commit dd51d153c6d38451786f6ddaec366274b58a0c08 Author: JM Addington Date: Wed Dec 18 10:45:38 2024 -0500 Update index.js fix: remove merge conflict artifact commit 531a41233636c207ebf751d6dbd6dc745e54e0bb Merge: ea38c280 4a734184 Author: Jonathan Addington Date: Tue Dec 17 22:33:26 2024 +0000 Merge branch 'jm-production' of https://github.com/jmaddington/LibreChat into jm-production commit ea38c2804aebbd39060a4b402c530e11b5ca281d Author: Jonathan Addington Date: Tue Dec 17 20:23:11 2024 +0000 test: ๐ŸŒง๏ธAdd integration tests for OpenWeather tool commit d86568b8c19bcda9e205b122c6317e3a8c178e70 Author: Jonathan Addington Date: Tue Dec 17 20:07:32 2024 +0000 feat: ๐ŸŒค๏ธAdd OpenWeather tool and corresponding tests commit 4a73418480c7ff6d68b5c0e552570b2870332ecd Author: JM Addington Date: Tue Dec 17 11:07:40 2024 -0500 Sybc Flux ai with tracking (#25) * fix: Correct syntax by adding missing semicolon for FluxAPI import * fix: Remove unnecessary comment from toolAuthFields in handleTools.js commit b526d2bc7bc4d2b008a234d7605ab762d3055851 Merge: a50c2ea5 609542b8 Author: Jonathan Addington Date: Tue Dec 17 15:18:03 2024 +0000 Merge branch 'jm-production' of https://github.com/jmaddington/LibreChat into jm-production commit 609542b894b21d84dc8d31d646f9cee0a960c0bf Author: JM Addington Date: Tue Dec 17 09:06:54 2024 -0500 Merge latest changes from New/feature/e2bcode into jm-production (#21) * ๐Ÿ™Œ a11y: Accessibility Improvements (#4978) * ๐Ÿ”ƒ fix: Safeguard against null token in SSE refresh token handling * ๐Ÿ”ƒ fix: Update import path for AnnounceOptions in LiveAnnouncer component * ๐Ÿ”ƒ a11y: Add aria-live attribute for accessibility in error messages * fix: prevent double screen reader notification for toast * ๐Ÿ”ƒ a11y: Enhance accessibility for main menus and buttons with ARIA roles and labels * refactor: better alt text for logo on login page #4095 * refactor: remove unused import for DropdownNoState in Voices component * fix: Focus management issue in the Export Options Modal #4100 * ๐Ÿ› fix: Enforced Model Spec Icons/Labels and Agent Descriptions (#4979) * fix: Previous convos missing model spec info when enforce is set to `true` #4749 * refactor: Include description field in agent list response * docs: enhance E2BCode documentation with subprocess usage and file transfer instructions * ๐Ÿ”ง fix: Add modelLabel to OpenAIClient and PluginsClient options (#4995) * fix: change description too-long for tools and move to new help action * refactor: move some if stmts into the switch block * feat: Add command_run * feat: add start_server command * refactor: refactor how we list and kill sandboxes to work better * refactor: add clearer instructions on max commands limit to avoid recursion errors with langchain * refactor: change how we handle sandbox kills * refactor: add processinfo(), system_install and comment out shell, read more... The LLMs are consistently using the shell function instead of the command_run and end up timing out on the api as a result. Taking away the shell functionality is a test at this point to see if we get better performance. * refactor: add user, cwd to create; set default timeout to 60mins * TESTING to see if this increases the langchain recursion limit I'm not at all sure I have the correct line * refactor: add shell back, remove execute Execute looks geared towards Jupyter notebooks, which is not what I am aiming for here. --------- Co-authored-by: Danny Avila commit 5002691cf9f0432fd1d5486398cab586d5722a16 Author: JM Addington Date: Mon Dec 16 13:41:28 2024 -0500 Merge latest New/feature/e2bcode (#20) * ๐Ÿ™Œ a11y: Accessibility Improvements (#4978) * ๐Ÿ”ƒ fix: Safeguard against null token in SSE refresh token handling * ๐Ÿ”ƒ fix: Update import path for AnnounceOptions in LiveAnnouncer component * ๐Ÿ”ƒ a11y: Add aria-live attribute for accessibility in error messages * fix: prevent double screen reader notification for toast * ๐Ÿ”ƒ a11y: Enhance accessibility for main menus and buttons with ARIA roles and labels * refactor: better alt text for logo on login page #4095 * refactor: remove unused import for DropdownNoState in Voices component * fix: Focus management issue in the Export Options Modal #4100 * ๐Ÿ› fix: Enforced Model Spec Icons/Labels and Agent Descriptions (#4979) * fix: Previous convos missing model spec info when enforce is set to `true` #4749 * refactor: Include description field in agent list response * docs: enhance E2BCode documentation with subprocess usage and file transfer instructions * ๐Ÿ”ง fix: Add modelLabel to OpenAIClient and PluginsClient options (#4995) * fix: change description too-long for tools and move to new help action * refactor: move some if stmts into the switfh block * feat: Add command_run * feat: add start_server command * refactor: refactor how we list and kill sandboxes to work better * refactor: add clearer instructions on max commands limit to avoid recursion errors with langchain * refactor: change how we handle sandbox kills * refactor: add processinfo(), system_install and comment out shell, read more... The LLMs are consistently using the shell function instead of the command_run and end up timing out on the api as a result. Taking away the shell functionality is a test at this point to see if we get better performance. * refactor: add user, cwd to create; set default timeout to 60mins --------- Co-authored-by: Danny Avila commit dd379a28e2c351dd863c4930aaf1a2d2adf7898c Author: JM Addington Date: Sat Dec 14 21:42:20 2024 -0500 Merge upstream into jm-production (#19) * ๐Ÿ™Œ a11y: Accessibility Improvements (#4978) * ๐Ÿ”ƒ fix: Safeguard against null token in SSE refresh token handling * ๐Ÿ”ƒ fix: Update import path for AnnounceOptions in LiveAnnouncer component * ๐Ÿ”ƒ a11y: Add aria-live attribute for accessibility in error messages * fix: prevent double screen reader notification for toast * ๐Ÿ”ƒ a11y: Enhance accessibility for main menus and buttons with ARIA roles and labels * refactor: better alt text for logo on login page #4095 * refactor: remove unused import for DropdownNoState in Voices component * fix: Focus management issue in the Export Options Modal #4100 * ๐Ÿ› fix: Enforced Model Spec Icons/Labels and Agent Descriptions (#4979) * fix: Previous convos missing model spec info when enforce is set to `true` #4749 * refactor: Include description field in agent list response * ๐Ÿ”ง fix: Add modelLabel to OpenAIClient and PluginsClient options (#4995) --------- Co-authored-by: Danny Avila commit ec27ff78f9953e24901753a59677d5e9c05ef3a2 Author: JM Addington Date: Sat Dec 14 21:40:25 2024 -0500 e2bcode bugfix (#18) * docs: enhance E2BCode documentation with subprocess usage and file transfer instructions * fix: change description too-long for tools and move to new help action commit a50c2ea5e26b5e5236bc40531a2c01303dc100a9 Author: Jonathan Addington Date: Sat Dec 14 00:19:06 2024 +0000 docs: enhance E2BCode documentation with subprocess usage and file transfer instructions commit 182b96ada9e8b3446b0a5bf7fc702f73df51d02c Author: Jonathan Addington Date: Fri Dec 13 20:31:15 2024 +0000 docs: update README with E2B.dev rationale and additional plugins; enhance E2BCode usage notes commit 8ef386635e227c5a71dc5ad22c45729ff1172ac8 Author: Jonathan Addington Date: Fri Dec 13 20:26:40 2024 +0000 docs: update README to include E2B.dev code interpreter and plugins list commit debe367c794a5258b297349b8c8c52239bf6a6ec Merge: 6b3548f1 b6eebf2d Author: Jonathan Addington Date: Fri Dec 13 20:14:43 2024 +0000 Merge branch 'main' into jm-production commit b6eebf2df4a39b3034283e0da8fa1c4f08788023 Merge: 0ce422a4 763693cc Author: Jonathan Addington Date: Fri Dec 13 15:14:29 2024 -0500 Merge branch 'main' of https://github.com/jmaddington/LibreChat commit 6b3548f12249186609b14929473bf4361e281625 Merge: 943bac35 0ce422a4 Author: Jonathan Addington Date: Fri Dec 13 20:13:45 2024 +0000 Merge branch 'main' into jm-production commit 943bac350c9da66a6907e51be8e353c2aeb07500 Author: Jonathan Addington Date: Fri Dec 13 20:09:10 2024 +0000 fix: enhance E2BCode prompt with detailed action descriptions and usage notes commit 9e998d68a98b7f93303853f7d6803e6ec8f322c3 Merge: f18cdc0c 8510262f Author: Jonathan Addington Date: Fri Dec 13 20:03:20 2024 +0000 Merge branch 'feat/new/e2bcode' into jm-production commit f18cdc0cf0d2d07f8547c7feab79d3d0005fddb0 Author: JM Addington Date: Fri Dec 13 14:23:50 2024 -0500 Feat/new/e2bcode (#17) Add support for e2b.dev code interpreter (Sandbox environment). commit d7551cf6320df474b800a7c77f0d2b3578197609 Merge: 2db7006c 20b91ad0 Author: Jonathan Addington Date: Fri Dec 13 19:05:28 2024 +0000 Merge branch 'jm-production' of https://github.com/jmaddington/LibreChat into jm-production commit 2db7006cdcb968434ea63ecce30ca016a4eecb33 Author: Jonathan Addington Date: Fri Dec 13 19:04:36 2024 +0000 fix: update checkout action to use 'with' syntax for ref commit 20b91ad04981904336c93fdb3d7b67e93750c0a9 Author: Jonathan Addington Date: Fri Dec 13 19:04:36 2024 +0000 fix: update checkout action to use 'with' syntax for ref commit d1ccebc28996503591cfed9c63e57ef3cabf8482 Merge: b5062a45 b9764a8d Author: Jonathan Addington Date: Fri Dec 13 14:03:55 2024 -0500 Merge branch 'jm-production' of https://github.com/jmaddington/LibreChat into jm-production commit b9764a8d3272abef9972afbca48bc16aa229fcfa Author: JM Addington Date: Fri Dec 13 14:03:09 2024 -0500 fix indent in deploy workflow commit b5062a45d3a03b84b2ea8b66635a25fb43bc2345 Author: Jonathan Addington Date: Fri Dec 13 18:41:19 2024 +0000 feat: add GitHub Actions workflow for Docker build and push to GHCR commit 697bb4546deee88d761aad291e373e4e5b85b9af Author: Jonathan Addington Date: Fri Dec 13 18:41:19 2024 +0000 feat: add GitHub Actions workflow for Docker build and push to GHCR commit 8510262fb62e96fddc01f726eefc7e792e018e6b Author: Jonathan Addington Date: Fri Dec 13 18:09:25 2024 +0000 docs: add documentation for E2BCode plugin integration with LibreChat commit f58a5c077228019bc1817c083af5824b7de0db0c Author: Jonathan Addington Date: Fri Dec 13 18:02:19 2024 +0000 feat: enhance E2BCode tool with hidden environment variable support and improve documentation commit 66a4820e3fcd8c89843fb41350ed1b54b97a14d3 Author: Jonathan Addington Date: Fri Dec 13 17:53:19 2024 +0000 feat: add 'set_timeout' action to E2BCode tool for managing sandbox timeout commit fef7a060ae4a0b015371341cb7f40c54138b8508 Author: Jonathan Addington Date: Fri Dec 13 17:43:11 2024 +0000 feat: add background task support and command management to E2BCode tool commit 14058b20015b9c5805bfc6a0c759a1676a2338fa Author: Jonathan Addington Date: Fri Dec 13 17:31:10 2024 +0000 feat: enhance E2BCode tool with environment variable support for sandbox creation and execution commit aa5e78f7fbbc399c595d25696a258592b4990ce6 Author: Jonathan Addington Date: Fri Dec 13 17:24:26 2024 +0000 feat: add 'kill' action to E2BCode tool for managing sandboxes commit c213690e4c5f8d7d4a4d9f167e38b27a22efb6ae Author: Jonathan Addington Date: Fri Dec 13 16:58:43 2024 +0000 feat: misc fixes commit a9298786d4f16c0bd902225d8664b279ec909b83 Author: Jonathan Addington Date: Fri Dec 13 14:50:21 2024 +0000 fix: reverting back to init commit commit e9ff7e63f6ec3f0ea73df3535295326d24a47367 Author: Jonathan Addington Date: Thu Dec 12 16:42:59 2024 +0000 feat: add multiple e2b SDK commands commit 216c47ab571ecd51b7acfe467738f6b658ce07e1 Author: Jonathan Addington Date: Thu Dec 12 16:17:31 2024 +0000 feat: init commit of e2b code interpreter plugin commit 0ce422a47c4b8e00fd5d0fb704ec4c1974611a4b Author: Jonathan Addington Date: Wed Dec 11 17:13:49 2024 +0000 fix: change vite bind to work in docker, update devcontainer exposted ports commit 9df7c10293018753cd940ee5fd8f371987482503 Author: Jonathan Addington Date: Mon Dec 9 13:41:05 2024 +0000 Fix: fix mistaken lines in merge commit 0e8e274ced145be58a685cfe8862b33a23a3d31c Merge: dd32f4aa cd1184a3 Author: JM Addington Date: Mon Dec 9 07:52:45 2024 -0500 Merge branch 'tracking/13-tracking-branch-merge' into jm-production commit dd32f4aa0c7cddaf1a8078d890354b36af4cdd40 Author: Jonathan Addington Date: Mon Dec 9 12:47:44 2024 +0000 docs: update readme with info on this fork commit 4bb349bf1ec52b8ccbe403ec6944dc4bfd39c9ca Merge: 229aa2c6 69d621d5 Author: Jonathan Addington Date: Sat Dec 7 17:41:53 2024 +0000 Merge remote-tracking branch 'origin/new/feature/flux-ai' into jm-production commit 229aa2c61edf048265f7fef570dfadfa8307b0e9 Merge: 1a815f5e 863eb5f7 Author: Jonathan Addington Date: Sat Dec 7 17:40:36 2024 +0000 Merge remote-tracking branch 'origin/new/feature/curl' into jm-production commit 863eb5f7de6b07066a63fe9693a65534b8d87127 Author: Jonathan Addington Date: Mon Nov 18 18:34:21 2024 -0500 fix: Remove redundant comment in handleTools.js commit fa33ab4f3ef8c2d8bbf6263dd6aa06eb719e21e3 Merge: 2140c97e d8dd93f6 Author: JM Addington Date: Mon Nov 18 18:29:48 2024 -0500 Merge branch 'jm-publish' into new/feature/curl commit d8dd93f61303efd1733e85d1058393b6da22ad00 Author: JM Addington Date: Mon Nov 18 18:26:22 2024 -0500 Flux ai - bug fixes (#4) * ๐Ÿ“ธ feat: Add support for Black Labs Flux AI This commit code was mostly generated by ChatGPT o1-preview, using the DALLE3 plugin as a guide. * ๐Ÿ“ธ feat: Add support for Black Labs Flux AI This commit code was mostly generated by ChatGPT o1-preview, using the DALLE3 plugin as a guide. * ๐Ÿ“ธ feat: Update .env.example and manifest.json for Flux API integration * ๐Ÿ“ธ fix: Add missing FluxAPI to available tools in the client module * fix: Correct syntax by adding missing semicolon for FluxAPI import * fix: Remove unnecessary comment from toolAuthFields in handleTools.js commit 2140c97e8dbeda6f76f735ee6d59cb5883c4ee15 Author: Jonathan Addington Date: Mon Nov 18 18:21:46 2024 -0500 fix: Add new WebNavigator icon path in manifest.json commit aacd4ef46689a894738caf97bb7ae5e20e476486 Author: Jonathan Addington Date: Mon Nov 18 17:32:29 2024 -0500 refactor: Rename CurlPlugin to WebNavigator and update references throughout the codebase commit 3aef7dbc3c403fc8ae7047e0a2c1eb1943f8c3e2 Author: Jonathan Addington Date: Mon Nov 18 17:31:09 2024 -0500 feat: Update CurlPlugin guidelines to exclude additional HTML tags and attributes commit 430ae6569ef11fc6bae8dcecb4fb7024ec1ed6dd Author: Jonathan Addington Date: Mon Nov 18 17:09:03 2024 -0500 ๐Ÿ“ธ feat: Enhance CurlPlugin guidelines and add support for including specific HTML attributes in responses It's pretty easy to return too many tokens to the LLM, we're trying to cut down on that as much as possible commit c5e82b906d115e13813bc42e88e780965382cb33 Author: Jonathan Addington Date: Mon Nov 18 17:02:05 2024 -0500 ๐Ÿ“ธ refactor: Enhance CurlPlugin with improved guidelines and additional parameters for better HTTP request handling commit a0c8df192c17a9b4b59aaa593c340cb80716cc27 Author: Jonathan Addington Date: Mon Nov 18 16:54:43 2024 -0500 ๐ŸŒ feat: Add CurlPlugin to tools and update manifest for integration commit 98c0dd6f2079f145583982b40f160c409bdde165 Author: Jonathan Addington Date: Mon Nov 18 10:56:45 2024 -0500 ๐Ÿ“ธ fix: Add missing FluxAPI to available tools in the client module commit 78ca8ed608887ee3d9c204f8fe14714659e54fbe Author: JM Addington Date: Mon Nov 18 10:09:24 2024 -0500 Flux ai (#1) (#3) * ๐Ÿ“ธ feat: Add support for Black Labs Flux AI This commit code was mostly generated by ChatGPT o1-preview, using the DALLE3 plugin as a guide. commit 69d621d591b870303593f1656fceff903d5ce325 Author: JM Addington Date: Mon Nov 18 09:22:58 2024 -0500 Flux ai (#1) * ๐Ÿ“ธ feat: Add support for Black Labs Flux AI This commit code was mostly generated by ChatGPT o1-preview, using the DALLE3 plugin as a guide. commit 53d02803a8a389e361c1f66e4d356bbbbda4eeb0 Author: Jonathan Addington Date: Mon Nov 18 09:13:20 2024 -0500 ๐Ÿ“ธ feat: Update .env.example and manifest.json for Flux API integration commit 6582880e5ca897cf05dded7ce73e74f6eb2929a4 Merge: e58d66a5 655b2843 Author: Jonathan Addington Date: Mon Nov 18 08:53:32 2024 -0500 Merge branch 'flux-ai' of https://github.com/jmaddington/LibreChat into flux-ai commit e58d66a540ba70f2f8dab4e986a0e2102000058e Merge: 8f05031d ef83e4c4 Author: Jonathan Addington Date: Mon Nov 18 08:07:57 2024 -0500 Merge branch 'flux-ai' of https://github.com/jmaddington/LibreChat into flux-ai commit 655b28436e489efcb0b07b40fbf125e8d2e86360 Merge: 8f05031d ef83e4c4 Author: Jonathan Addington Date: Mon Nov 18 08:07:57 2024 -0500 Merge branch 'flux-ai' of https://github.com/jmaddington/LibreChat into flux-ai commit 8f05031df71d4594b6e9799a581b0fb723e38c1c Author: Jonathan Addington Date: Sun Nov 17 15:33:12 2024 -0500 ๐Ÿ“ธ feat: Add support for Black Labs Flux AI This commit code was mostly generated by ChatGPT o1-preview, using the DALLE3 plugin as a guide. commit ef83e4c4916df58f66f196a962003ac66b06b6ec Author: Jonathan Addington Date: Sun Nov 17 15:33:12 2024 -0500 ๐Ÿ“ธ feat: Add support for Black Labs Flux AI This commit code was mostly generated by ChatGPT o1-preview, using the DALLE3 plugin as a guide. --- .devcontainer/docker-compose.yml | 18 +- .env.example | 4 + .github/workflows/deploy-jm.yml | 38 + README.md | 183 +-- api/app/clients/GoogleClient.js | 2 +- api/app/clients/index.js | 1 + .../__tests__/openWeather.integration.test.js | 43 + .../tools/__tests__/openweather.test.js | 116 ++ api/app/clients/tools/index.js | 8 + api/app/clients/tools/manifest.json | 36 + api/app/clients/tools/structured/E2BCode.js | 1237 +++++++++++++++++ api/app/clients/tools/structured/E2BCode.md | 433 ++++++ api/app/clients/tools/structured/FluxAPI.js | 238 ++++ .../clients/tools/structured/OpenWeather.js | 293 ++++ .../clients/tools/structured/WebNavigator.js | 281 ++++ api/app/clients/tools/util/handleTools.js | 10 + api/server/controllers/agents/client.js | 1 + api/test/__tests__/openweather-plugin.test.js | 193 +++ client/public/assets/e2b_symbol.png | Bin 0 -> 6730 bytes client/public/assets/flux.png | Bin 0 -> 435687 bytes client/public/assets/webnavigator.png | Bin 0 -> 240687 bytes client/vite.config.ts | 2 +- package-lock.json | 66 + package.json | 4 + 24 files changed, 3028 insertions(+), 179 deletions(-) create mode 100644 .github/workflows/deploy-jm.yml create mode 100644 api/app/clients/tools/__tests__/openWeather.integration.test.js create mode 100644 api/app/clients/tools/__tests__/openweather.test.js create mode 100644 api/app/clients/tools/structured/E2BCode.js create mode 100644 api/app/clients/tools/structured/E2BCode.md create mode 100644 api/app/clients/tools/structured/FluxAPI.js create mode 100644 api/app/clients/tools/structured/OpenWeather.js create mode 100644 api/app/clients/tools/structured/WebNavigator.js create mode 100644 api/test/__tests__/openweather-plugin.test.js create mode 100644 client/public/assets/e2b_symbol.png create mode 100644 client/public/assets/flux.png create mode 100644 client/public/assets/webnavigator.png diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index e7c36c55352..788784d65c0 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -7,10 +7,12 @@ services: links: - mongodb - meilisearch - # ports: - # - 3080:3080 # Change it to 9000:3080 to use nginx - extra_hosts: # if you are running APIs on docker you need access to, you will need to uncomment this line and next - - "host.docker.internal:host-gateway" + ports: + - 80:80 + - 3080:3090 # Change it to 9000:3080 to use nginx + - 3090:3090 + # extra_hosts: # if you are running APIs on docker you need access to, you will need to uncomment this line and next + # - "host.docker.internal:host-gateway" volumes: # This is where VS Code should expect to find your project's source code and the value of "workspaceFolder" in .devcontainer/devcontainer.json @@ -18,7 +20,7 @@ services: # Uncomment the next line to use Docker from inside the container. See https://aka.ms/vscode-remote/samples/docker-from-docker-compose for details. # - /var/run/docker.sock:/var/run/docker.sock environment: - - HOST=0.0.0.0 + # - HOST=0.0.0.0 - MONGO_URI=mongodb://mongodb:27017/LibreChat # - CHATGPT_REVERSE_PROXY=http://host.docker.internal:8080/api/conversation # if you are hosting your own chatgpt reverse proxy with docker # - OPENAI_REVERSE_PROXY=http://host.docker.internal:8070/v1/chat/completions # if you are hosting your own chatgpt reverse proxy with docker @@ -41,7 +43,7 @@ services: expose: - 27017 # ports: - # - 27018:27017 + - 27017:27017 image: mongo # restart: always volumes: @@ -54,8 +56,8 @@ services: expose: - 7700 # Uncomment this to access meilisearch from outside docker - # ports: - # - 7700:7700 # if exposing these ports, make sure your master key is not the default value + ports: + - 7700:7700 # if exposing these ports, make sure your master key is not the default value environment: - MEILI_NO_ANALYTICS=true - MEILI_MASTER_KEY=5c71cf56d672d009e36070b5bc5e47b743535ae55c818ae3b735bb6ebfb4ba63 diff --git a/.env.example b/.env.example index f2a51198f42..feedbbd716c 100644 --- a/.env.example +++ b/.env.example @@ -256,6 +256,10 @@ AZURE_AI_SEARCH_SEARCH_OPTION_SELECT= # DALLE3_AZURE_API_VERSION= # DALLE2_AZURE_API_VERSION= +# Flux +#----------------- +# FLUX_API_KEY + # Google #----------------- GOOGLE_SEARCH_API_KEY= diff --git a/.github/workflows/deploy-jm.yml b/.github/workflows/deploy-jm.yml new file mode 100644 index 00000000000..6f847763ffe --- /dev/null +++ b/.github/workflows/deploy-jm.yml @@ -0,0 +1,38 @@ +name: Docker Build and Push to GHCR + +on: + workflow_dispatch: + + push: + branches: + - jm-production + +jobs: + deploy-gh-runner-aci: + runs-on: ubuntu-latest + steps: + # checkout the repo + - name: 'Checkout GitHub Action' + uses: actions/checkout@v4 + with: + ref: jm-production + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Log in to GitHub Docker Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + context: . + push: true + tags: ghcr.io/jmaddington/librechat:latest + + - name: Log out from Docker Hub + run: docker logout ghcr.io diff --git a/README.md b/README.md index a2a23a483dc..c371f9c7627 100644 --- a/README.md +++ b/README.md @@ -1,175 +1,20 @@ -

- - - -

- LibreChat -

-

+# About this Fork -

- - - - - - - - - - - - -

+This fork is a personal project to add a few features to LibreChat and integrate features from other forks. -

- - Deploy on Railway - - - Deploy on Zeabur - - - Deploy on Sealos - -

+## Branches +`jm-productuion` - The main branch for this fork for production use. Stable-ish, but has been at least minimally tested. +`main` - A clone of the upstream main branch. +`new/feature/X` - Branches for new features, kept open until they are feature complete and merged. -# โœจ Features +## Known Changes from danny-avila/LibreChat +- E2B.dev code interpreter added to the tools list +- Web Navigator plugin added to the tools list. +- Flux AI plugin added to the tools list. -- ๐Ÿ–ฅ๏ธ **UI & Experience** inspired by ChatGPT with enhanced design and features +### Why E2B? +LibreChat recently introduced their own code interpreter service. It's affordable, integrates seamlessly with their platform, and provides a viable revenue stream. So why not use it? -- ๐Ÿค– **AI Model Selection**: - - Anthropic (Claude), AWS Bedrock, OpenAI, Azure OpenAI, Google, Vertex AI, OpenAI Assistants API (incl. Azure) - - [Custom Endpoints](https://www.librechat.ai/docs/quick_start/custom_endpoints): Use any OpenAI-compatible API with LibreChat, no proxy required - - Compatible with [Local & Remote AI Providers](https://www.librechat.ai/docs/configuration/librechat_yaml/ai_endpoints): - - Ollama, groq, Cohere, Mistral AI, Apple MLX, koboldcpp, together.ai, - - OpenRouter, Perplexity, ShuttleAI, Deepseek, Qwen, and more +For our internal needs, however, we require a code interpreter with network accessโ€”a feature not offered by LibreChat's interpreter for safety reasons. E2B.dev provides an excellent alternative that meets our specific requirements. -- ๐Ÿ”ง **[Code Interpreter API](https://www.librechat.ai/docs/features/code_interpreter)**: - - Secure, Sandboxed Execution in Python, Node.js (JS/TS), Go, C/C++, Java, PHP, Rust, and Fortran - - Seamless File Handling: Upload, process, and download files directly - - No Privacy Concerns: Fully isolated and secure execution - -- ๐Ÿ”ฆ **Agents & Tools Integration**: - - **[LibreChat Agents](https://www.librechat.ai/docs/features/agents)**: - - No-Code Custom Assistants: Build specialized, AI-driven helpers without coding - - Flexible & Extensible: Attach tools like DALL-E-3, file search, code execution, and more - - Compatible with Custom Endpoints, OpenAI, Azure, Anthropic, AWS Bedrock, and more - - [Model Context Protocol (MCP) Support](https://modelcontextprotocol.io/clients#librechat) for Tools - - Use LibreChat Agents and OpenAI Assistants with Files, Code Interpreter, Tools, and API Actions - -- ๐Ÿช„ **Generative UI with Code Artifacts**: - - [Code Artifacts](https://youtu.be/GfTj7O4gmd0?si=WJbdnemZpJzBrJo3) allow creation of React, HTML, and Mermaid diagrams directly in chat - -- ๐Ÿ’พ **Presets & Context Management**: - - Create, Save, & Share Custom Presets - - Switch between AI Endpoints and Presets mid-chat - - Edit, Resubmit, and Continue Messages with Conversation branching - - [Fork Messages & Conversations](https://www.librechat.ai/docs/features/fork) for Advanced Context control - -- ๐Ÿ’ฌ **Multimodal & File Interactions**: - - Upload and analyze images with Claude 3, GPT-4o, o1, Llama-Vision, and Gemini ๐Ÿ“ธ - - Chat with Files using Custom Endpoints, OpenAI, Azure, Anthropic, AWS Bedrock, & Google ๐Ÿ—ƒ๏ธ - -- ๐ŸŒŽ **Multilingual UI**: - - English, ไธญๆ–‡, Deutsch, Espaรฑol, Franรงais, Italiano, Polski, Portuguรชs Brasileiro - - ะ ัƒััะบะธะน, ๆ—ฅๆœฌ่ชž, Svenska, ํ•œ๊ตญ์–ด, Tiแบฟng Viแป‡t, ็น้ซ”ไธญๆ–‡, ุงู„ุนุฑุจูŠุฉ, Tรผrkรงe, Nederlands, ืขื‘ืจื™ืช - -- ๐ŸŽจ **Customizable Interface**: - - Customizable Dropdown & Interface that adapts to both power users and newcomers - -- ๐Ÿ—ฃ๏ธ **Speech & Audio**: - - Chat hands-free with Speech-to-Text and Text-to-Speech - - Automatically send and play Audio - - Supports OpenAI, Azure OpenAI, and Elevenlabs - -- ๐Ÿ“ฅ **Import & Export Conversations**: - - Import Conversations from LibreChat, ChatGPT, Chatbot UI - - Export conversations as screenshots, markdown, text, json - -- ๐Ÿ” **Search & Discovery**: - - Search all messages/conversations - -- ๐Ÿ‘ฅ **Multi-User & Secure Access**: - - Multi-User, Secure Authentication with OAuth2, LDAP, & Email Login Support - - Built-in Moderation, and Token spend tools - -- โš™๏ธ **Configuration & Deployment**: - - Configure Proxy, Reverse Proxy, Docker, & many Deployment options - - Use completely local or deploy on the cloud - -- ๐Ÿ“– **Open-Source & Community**: - - Completely Open-Source & Built in Public - - Community-driven development, support, and feedback - -[For a thorough review of our features, see our docs here](https://docs.librechat.ai/) ๐Ÿ“š - -## ๐Ÿชถ All-In-One AI Conversations with LibreChat - -LibreChat brings together the future of assistant AIs with the revolutionary technology of OpenAI's ChatGPT. Celebrating the original styling, LibreChat gives you the ability to integrate multiple AI models. It also integrates and enhances original client features such as conversation and message search, prompt templates and plugins. - -With LibreChat, you no longer need to opt for ChatGPT Plus and can instead use free or pay-per-call APIs. We welcome contributions, cloning, and forking to enhance the capabilities of this advanced chatbot platform. - -[![Watch the video](https://raw.githubusercontent.com/LibreChat-AI/librechat.ai/main/public/images/changelog/v0.7.5.png)](https://www.youtube.com/watch?v=IDukQ7a2f3U) -Click on the thumbnail to open the videoโ˜๏ธ - ---- - -## ๐ŸŒ Resources - -**GitHub Repo:** - - **RAG API:** [github.com/danny-avila/rag_api](https://github.com/danny-avila/rag_api) - - **Website:** [github.com/LibreChat-AI/librechat.ai](https://github.com/LibreChat-AI/librechat.ai) - -**Other:** - - **Website:** [librechat.ai](https://librechat.ai) - - **Documentation:** [docs.librechat.ai](https://docs.librechat.ai) - - **Blog:** [blog.librechat.ai](https://blog.librechat.ai) - ---- - -## ๐Ÿ“ Changelog - -Keep up with the latest updates by visiting the releases page and notes: -- [Releases](https://github.com/danny-avila/LibreChat/releases) -- [Changelog](https://www.librechat.ai/changelog) - -**โš ๏ธ Please consult the [changelog](https://www.librechat.ai/changelog) for breaking changes before updating.** - ---- - -## โญ Star History - -

- - Star History Chart - -

-

- - danny-avila%2FLibreChat | Trendshift - - - ROSS Index - Fastest Growing Open-Source Startups in Q1 2024 | Runa Capital - -

- ---- - -## โœจ Contributions - -Contributions, suggestions, bug reports and fixes are welcome! - -For new features, components, or extensions, please open an issue and discuss before sending a PR. - ---- - -## ๐Ÿ’– This project exists in its current state thanks to all the people who contribute - - - - +***WE STILL LOVE LIBRECHAT!*** In fact, we're proud to be monthly sponsors. Our choice to use E2B.dev is not about detracting from LibreChat's service; we simply need additional functionality to fulfill our unique needs. \ No newline at end of file diff --git a/api/app/clients/GoogleClient.js b/api/app/clients/GoogleClient.js index f6fca55112a..9df414b6aac 100644 --- a/api/app/clients/GoogleClient.js +++ b/api/app/clients/GoogleClient.js @@ -946,4 +946,4 @@ class GoogleClient extends BaseClient { } } -module.exports = GoogleClient; +module.exports = GoogleClient; \ No newline at end of file diff --git a/api/app/clients/index.js b/api/app/clients/index.js index a5e8eee5045..5d34ec5ff38 100644 --- a/api/app/clients/index.js +++ b/api/app/clients/index.js @@ -6,6 +6,7 @@ const TextStream = require('./TextStream'); const AnthropicClient = require('./AnthropicClient'); const toolUtils = require('./tools/util'); + module.exports = { ChatGPTClient, OpenAIClient, diff --git a/api/app/clients/tools/__tests__/openWeather.integration.test.js b/api/app/clients/tools/__tests__/openWeather.integration.test.js new file mode 100644 index 00000000000..e8a0df7b88c --- /dev/null +++ b/api/app/clients/tools/__tests__/openWeather.integration.test.js @@ -0,0 +1,43 @@ +// __tests__/openWeather.integration.test.js +const OpenWeather = require('../structured/OpenWeather'); +const fetch = require('node-fetch'); + +// If you havenโ€™t mocked fetch globally for other tests, you may remove the mocking. +// If fetch is mocked globally, you will need to unmock it here. +// For example: +// jest.unmock('node-fetch'); + +describe('OpenWeather Tool (Integration Test)', () => { + let tool; + + beforeAll(() => { + tool = new OpenWeather(); + }); + + test('current_forecast with a real API key, if available', async () => { + // Check if API key is available + if (!process.env.OPENWEATHER_API_KEY) { + console.warn("Skipping real API test, no OPENWEATHER_API_KEY found."); + return; // Test passes but does nothing + } + + // Provide a real city and action + const result = await tool.call({ + action: 'current_forecast', + city: 'London', + units: 'Celsius' + }); + + // Try to parse the JSON result + let parsed; + try { + parsed = JSON.parse(result); + } catch (e) { + throw new Error(`Could not parse JSON from response: ${result}`); + } + + // Check that the response contains expected fields + expect(parsed).toHaveProperty('current'); + expect(typeof parsed.current.temp).toBe('number'); + }); +}); diff --git a/api/app/clients/tools/__tests__/openweather.test.js b/api/app/clients/tools/__tests__/openweather.test.js new file mode 100644 index 00000000000..2f9c512c0c7 --- /dev/null +++ b/api/app/clients/tools/__tests__/openweather.test.js @@ -0,0 +1,116 @@ +// __tests__/openWeather.test.js +const OpenWeather = require('../structured/OpenWeather'); +const fetch = require('node-fetch'); + +// Mock environment variable +process.env.OPENWEATHER_API_KEY = 'test-api-key'; + +// Mock the fetch function globally +jest.mock('node-fetch', () => jest.fn()); + +describe('OpenWeather Tool', () => { + let tool; + + beforeAll(() => { + tool = new OpenWeather(); + }); + + beforeEach(() => { + fetch.mockReset(); + }); + + test('action=help returns help instructions', async () => { + const result = await tool.call({ + action: 'help' + }); + + expect(typeof result).toBe('string'); + const parsed = JSON.parse(result); + expect(parsed.title).toBe('OpenWeather One Call API 3.0 Help'); + }); + + test('current_forecast with a city and successful geocoding + forecast', async () => { + // Mock geocoding response + fetch.mockImplementationOnce((url) => { + if (url.includes('geo/1.0/direct')) { + return Promise.resolve({ + ok: true, + json: async () => [{ lat: 35.9606, lon: -83.9207 }] + }); + } + return Promise.reject('Unexpected fetch call for geocoding'); + }); + + // Mock forecast response + fetch.mockImplementationOnce(() => Promise.resolve({ + ok: true, + json: async () => ({ + current: { temp: 293.15, feels_like: 295.15 }, + daily: [{ temp: { day: 293.15, night: 283.15 } }] + }) + })); + + const result = await tool.call({ + action: 'current_forecast', + city: 'Knoxville, Tennessee', + units: 'Kelvin' + }); + + const parsed = JSON.parse(result); + expect(parsed.current.temp).toBe(293); + expect(parsed.current.feels_like).toBe(295); + expect(parsed.daily[0].temp.day).toBe(293); + expect(parsed.daily[0].temp.night).toBe(283); + }); + + test('timestamp action without a date returns an error message', async () => { + const result = await tool.call({ + action: 'timestamp', + lat: 35.9606, + lon: -83.9207 + }); + expect(result).toMatch(/Error: For timestamp action, a 'date' in YYYY-MM-DD format is required./); + }); + + test('unknown action returns an error due to schema validation', async () => { + await expect(tool.call({ + action: 'unknown_action' + })).rejects.toThrow(/Received tool input did not match expected schema/); + }); + + + test('geocoding failure returns a descriptive error', async () => { + fetch.mockImplementationOnce(() => Promise.resolve({ + ok: true, + json: async () => [] + })); + + const result = await tool.call({ + action: 'current_forecast', + city: 'NowhereCity' + }); + expect(result).toMatch(/Error: Could not find coordinates for city: NowhereCity/); + }); + + test('API request failure returns an error', async () => { + // Mock geocoding success + fetch.mockImplementationOnce(() => Promise.resolve({ + ok: true, + json: async () => [{ lat: 35.9606, lon: -83.9207 }] + })); + + // Mock weather request failure (e.g., 404) + fetch.mockImplementationOnce(() => Promise.resolve({ + ok: false, + status: 404, + json: async () => ({ message: 'Not found' }) + })); + + const result = await tool.call({ + action: 'current_forecast', + city: 'Knoxville, Tennessee' + }); + // Adjusted regex to match without quotes + expect(result).toMatch(/Error: OpenWeather API request failed with status 404: Not found/); + }); +}); diff --git a/api/app/clients/tools/index.js b/api/app/clients/tools/index.js index a8532d4581f..7fe270651bc 100644 --- a/api/app/clients/tools/index.js +++ b/api/app/clients/tools/index.js @@ -8,6 +8,10 @@ const StructuredSD = require('./structured/StableDiffusion'); const GoogleSearchAPI = require('./structured/GoogleSearch'); const TraversaalSearch = require('./structured/TraversaalSearch'); const TavilySearchResults = require('./structured/TavilySearchResults'); +const FluxAPI = require('./structured/FluxAPI'); +const WebNavigator = require('./structured/WebNavigator'); +const E2BCode = require('./structured/E2BCode'); +const OpenWeather = require('./structured/OpenWeather'); module.exports = { availableTools, @@ -19,4 +23,8 @@ module.exports = { TraversaalSearch, StructuredWolfram, TavilySearchResults, + FluxAPI, + WebNavigator, + E2BCode, + OpenWeather, }; diff --git a/api/app/clients/tools/manifest.json b/api/app/clients/tools/manifest.json index d2748cdea11..fe216694e7c 100644 --- a/api/app/clients/tools/manifest.json +++ b/api/app/clients/tools/manifest.json @@ -138,5 +138,41 @@ "description": "You need to provideq your API Key for Azure AI Search." } ] + }, + { + "name": "WebNavigator", + "pluginKey": "WebNavigator", + "description": "WebNavigator", + "icon": "/assets/webnavigator.png", + "isAuthRequired": "false", + "authConfig": [] + }, + { + "name": "Flux", + "pluginKey": "flux", + "description": "Generate images using text with the Flux API.", + "icon": "/assets/flux.png", + "isAuthRequired": "true", + "authConfig": [ + { + "authField": "FLUX_API_KEY", + "label": "Your Flux API Key", + "description": "Provide your Flux API key from your user profile." + } + ] + }, + { + "name": "E2BCode", + "pluginKey": "E2BCode", + "description": "Run code in an E2B sandbox", + "icon": "/assets/e2b_symbol.png", + "isAuthRequired": "true", + "authConfig": [ + { + "authField": "E2B_API_KEY", + "label": "E2B.dev API key", + "description": "API Key from the e2b.dev dashboard" + } + ] } ] diff --git a/api/app/clients/tools/structured/E2BCode.js b/api/app/clients/tools/structured/E2BCode.js new file mode 100644 index 00000000000..a532daa7402 --- /dev/null +++ b/api/app/clients/tools/structured/E2BCode.js @@ -0,0 +1,1237 @@ +const { z } = require('zod'); +const { Tool } = require('@langchain/core/tools'); +const { getEnvironmentVariable } = require('@langchain/core/utils/env'); +const { Sandbox } = require('@e2b/code-interpreter'); +const { logger } = require('~/config'); + +const MAX_OUTPUT_LENGTH = 10000; // 10k characters +const MAX_TAIL_LINES = 20; +const MAX_TAIL_BYTES = 2000; // ~2KB + +// Store active sandboxes with their session IDs +const sandboxes = (global.sandboxes = global.sandboxes || new Map()); + +class E2BCode extends Tool { + constructor(fields = {}) { + super(); + const envVar = 'E2B_API_KEY'; + const override = fields.override ?? false; + this.apiKey = fields.apiKey ?? this.getApiKey(envVar, override); + const keySuffix = this.apiKey ? this.apiKey.slice(-5) : 'none'; + logger.debug('[E2BCode] Initialized with API key ' + `*****${keySuffix}`); + this.name = 'E2BCode'; + this.description = ` + Use E2B to execute code, run shell commands, manage files, install packages, and manage sandbox environments in an isolated sandbox environment. + + YOU CANNOT RUN MORE THAN 25 COMMANDS SEQUENTIALLY WITHOUT OUTPUT TO THE USER! + + Sessions: You must provide a unique \`sessionId\` string to maintain session state between calls. Use the same \`sessionId\` for related actions. + + Use the help action before executing anything else to understand the available actions and parameters. Before you run a command for the first + time, use the help action for that command to understand the parameters required for that action. + + To copy files from one sandbox to another is to gzip them, then use the get_download_url action to get a link, + and then use wget on the new sandbox to download. + `; + + this.schema = z.object({ + sessionId: z + .string() + .optional() + .describe( + 'A unique identifier for the session. Use the same `sessionId` to maintain state across multiple calls.' + ), + sandboxId: z + .string() + .optional() + .describe( + 'The sandbox ID to use for the kill_sandbox action. If not provided, the sandbox associated with the `sessionId` will be used.' + ), + action: z + .enum([ + 'help', + 'create', + 'list_sandboxes', + 'kill', + 'set_timeout', + 'shell', + 'kill_command', + 'write_file', + 'read_file', + 'install', + 'get_file_downloadurl', + 'get_host', + 'command_run', + 'start_server', + 'command_list', + 'command_kill', + 'processinfo', + 'system_install', + ]) + .describe('The action to perform.'), + template: z + .string() + .optional() + .describe( + 'Sandbox template name or ID to create the sandbox from (used with `create` action).' + ), + language: z + .enum(['python', 'javascript', 'typescript', 'shell']) + .optional() + .describe('The programming language environment for installs. Defaults to `python`.'), + cmd: z + .string() + .optional() + .describe( + 'Command to execute (used with `shell`, `command_run` and `start_server` actions).' + ), + background: z + .boolean() + .optional() + .describe( + 'Whether to run the command in the background (for `command_run`, `shell` actions). Defaults to `false`.' + ), + cwd: z + .string() + .optional() + .describe( + 'Working directory for the command (used with `command_run` and `start_server` actions).' + ), + timeoutMs: z + .number() + .int() + .min(1000) + .default(60 * 1000) + .optional() + .describe( + 'Timeout in milliseconds for the command (used with `command_run` and `start_server` actions).' + ), + user: z + .string() + .optional() + .describe( + 'User to run the command as (used with `command_run` and `start_server` actions).' + ), + commandId: z + .string() + .optional() + .describe( + 'The ID of the background command to kill (required for `kill_command` action).' + ), + filePath: z + .string() + .optional() + .describe( + 'Path for read/write operations (used with `write_file`, `read_file`, and `get_file_downloadurl` actions).' + ), + fileContent: z + .string() + .optional() + .describe('Content to write to file (required for `write_file` action).'), + port: z + .number() + .int() + .optional() + .describe( + 'Port number to use for the host (used with `get_host` and `start_server` actions).' + ), + logFile: z + .string() + .optional() + .describe( + 'Path to the log file where stdout and stderr will be redirected (required for `start_server` action).' + ), + timeout: z + .number() + .int() + .optional() + .default(60) + .describe( + 'Timeout in minutes for the sandbox environment. Defaults to 60 minutes.' + ), + envs: z + .record(z.string(), z.string()) + .optional() + .describe( + 'Environment variables to set when creating the sandbox (used with `create` action) and for other actions that run commands.' + ), + command_name: z + .string() + .optional() + .describe( + 'The name of the command to get detailed help about (used with the `help` action).' + ), + pid: z + .number() + .int() + .optional() + .describe( + 'Process ID of the command to kill (required for `command_kill` action) or get info (required for `processinfo` action).' + ), + packages: z + .array(z.string()) + .optional() + .describe('List of packages to install (used with `install` and `system_install` actions).') + }); + } + + getApiKey(envVar, override) { + const key = getEnvironmentVariable(envVar); + if (!key && !override) { + logger.error(`[E2BCode] Missing ${envVar} environment variable`); + throw new Error(`Missing ${envVar} environment variable.`); + } + return key; + } + + // Method to retrieve hidden environment variables starting with E2B_CODE_EV_ + getHiddenEnvVars() { + const hiddenEnvVars = {}; + Object.keys(process.env).forEach((key) => { + if (key.startsWith('E2B_CODE_EV_')) { + hiddenEnvVars[key.substring('E2B_CODE_EV_'.length)] = process.env[key]; + } + }); + return hiddenEnvVars; + } + + // If the output is too large, return an error message and the last 20 lines (up to 2KB). + safeReturn(obj) { + let str = JSON.stringify(obj); + if (str.length > MAX_OUTPUT_LENGTH) { + // Extract last 20 lines of the JSON output (based on newline). + const lines = str.split('\n'); + const tailLines = lines.slice(-MAX_TAIL_LINES).join('\n'); + + // Truncate to 2KB if needed + let truncated = tailLines; + if (Buffer.byteLength(truncated, 'utf8') > MAX_TAIL_BYTES) { + truncated = truncated.slice(0, 2000); + } + + // Return error message with truncated output + return JSON.stringify({ + error: 'Output too long. We are truncating the output. If you need more, please rerun the command and redirect the output to a log file that you can tail if needed.', + truncated_tail: truncated + }); + } + return str; + } + + getDetailedHelp(commandName) { + const helpTexts = { + 'help': ` + Returns information about every possible action that can be performed using the E2BCode tool. + `, + 'create': ` + **create** + + - **Description:** Create a new E2B sandbox environment from a template. + + - **Required Parameters:** + - \`sessionId\`: A unique identifier for the session. Use the same \`sessionId\` to maintain state across multiple calls. + + - **Optional Parameters:** + - \`template\`: The sandbox template name or ID to create the environment from. + - \`timeout\`: Timeout in minutes for the sandbox environment. Defaults to 60 minutes. + - \`envs\`: A key-value object of environment variables to set when creating the sandbox. + `, + 'list_sandboxes': ` + **list_sandboxes** + + - **Description:** List all active E2B sandboxes for the current session. + + - **Parameters:** None (include \`sessionId\` for consistency). + `, + 'kill': ` + **kill** + + - **Description:** Terminate the E2B sandbox environment associated with the provided \`sessionId\` or \`sandboxId\`. + + - **Required Parameters:** + - Either \`sessionId\` or \`sandboxId\` must be provided. If both are provided \`sandboxId\` will take precedence. + `, + 'set_timeout': ` + **set_timeout** + + - **Description:** Update the timeout for the sandbox environment to keep it alive for the specified duration. + + - **Required Parameters:** + - \`sessionId\` + - \`timeout\`: Timeout in minutes for the sandbox environment. + `, + 'shell': ` + **shell** + + - **Description:** Run a shell command inside the sandbox environment. + + - **Required Parameters:** + - \`sessionId\` + - \`cmd\`: The shell command to execute. + + - **Optional Parameters:** + - \`background\`: Whether to run the shell command in the background. Boolean value; defaults to \`false\`. + - \`envs\`: Environment variables to set for this execution. + `, + 'kill_command': ` + **kill_command** + + - **Description:** Terminate a background shell command that was previously started. + + - **Required Parameters:** + - \`sessionId\` + - \`commandId\`: The ID of the background command to kill. + `, + 'write_file': ` + **write_file** + + - **Description:** Write content to a file in the sandbox environment. + + - **Required Parameters:** + - \`sessionId\` + - \`filePath\`: The path to the file where content will be written. + - \`fileContent\`: The content to write to the file. + `, + 'read_file': ` + **read_file** + + - **Description:** Read the content of a file from the sandbox environment. + + - **Required Parameters:** + - \`sessionId\` + - \`filePath\`: The path to the file to read. + `, + 'install': ` + **install** + + - **Description:** Install python or node packages within the sandbox environment. + Use \`system_install\` for system packages. + + - **Required Parameters:** + - \`sessionId\` + - \`packages\`: An array of package names to install. + + - **Optional Parameters:** + - \`language\`: The environment to use (\`python\` uses pip, \`javascript\` or \`typescript\` use npm). Defaults to \`python\`. + - \`envs\`: Environment variables to set for this installation. + `, + 'get_file_downloadurl': ` + **get_file_downloadurl** + + - **Description:** Obtain a download URL for a file in the sandbox environment. + + - **Required Parameters:** + - \`sessionId\` + - \`filePath\`: The path to the file for which to generate a download URL. + `, + 'get_host': ` + **get_host** + + - **Description:** Retrieve the host and port information for accessing services running inside the sandbox. + + - **Required Parameters:** + - \`sessionId\` + - \`port\`: The port number that the service is running on inside the sandbox. + `, + 'command_run': ` + **command_run** + + - **Description:** Start a new command and wait until it finishes executing, or run it in the background. + Use this for running most commands that do not require a PTY session. + + - **Required Parameters:** + - \`sessionId\` + - \`cmd\`: The command to execute. + + - **Optional Parameters:** + - \`background\`: Whether to run the command in the background. Defaults to \`false\`. + - \`cwd\`: Working directory for the command. + - \`timeoutMs\`: Timeout in milliseconds for the command. + - \`user\`: User to run the command as. + - \`envs\`: Environment variables to set for this command. + `, + 'start_server': ` + **start_server** + + - **Description:** Start a server process (e.g., nginx, flask) in the sandbox environment by executing a command in the background, + redirecting stdout and stderr to a specified log file, and returning the host and port information for accessing the server. + + - **Required Parameters:** + - \`sessionId\` + - \`cmd\`: The command to execute to start the server. + - \`port\`: The port number on which the server is expected to listen inside the sandbox. + - \`logFile\`: The path to the log file where stdout and stderr will be redirected. + + - **Optional Parameters:** + - \`cwd\`: Working directory for the command. + - \`timeoutMs\`: Timeout in milliseconds for the command. + - \`user\`: User to run the command as. + - \`envs\`: Environment variables to set for this execution. + + - **Returns:** + - \`sessionId\`: The session ID. + - \`commandId\`: The ID of the background command started. + - \`host\`: The host address to access the server. + - \`logFile\`: The location of the log file. + - \`message\`: Confirmation message of server start and log file location. + `, + 'command_list': ` + **command_list** + + - **Description:** List all running commands and PTY sessions within the sandbox environment. + + - **Required Parameters:** + - \`sessionId\` + `, + 'command_kill': ` + **command_kill** + + - **Description:** Kill a running command specified by its process ID. + + - **Required Parameters:** + - \`sessionId\` + - \`pid\`: Process ID of the command to kill. + `, + 'processinfo': ` + **processinfo** + + - **Description:** Get detailed information about a running command specified by its process ID. + + - **Required Parameters:** + - \`sessionId\` + - \`pid\`: Process ID of the command to get information about. + `, + 'system_install': ` + **system_install** + + - **Description:** Install system packages within the sandbox environment using \`sudo apt-get install\`. + + - **Required Parameters:** + - \`sessionId\` + - \`packages\`: An array of system package names to install. + + - **Optional Parameters:** + - \`envs\`: Environment variables to set for this installation. + `, + }; + + return helpTexts[commandName]; + } + + async _call(input) { + const { + sessionId, + sandboxId, + packages, + language = 'python', + action, + cmd, + background = false, + cwd, + timeoutMs = 30 * 1000, + user, + commandId, + filePath, + fileContent, + port, + timeout = 60 * 60, + envs, + command_name, + logFile, + pid, + template, + } = input; + + // Make sure we have sessionId or sandboxId for all actions except help and list_sandboxes, and create + if ( + action !== 'help' && + action !== 'list_sandboxes' && + action !== 'create' && + (!sessionId || (!sandboxId && action !== 'create')) + ) { + logger.error('[E2BCode] `sessionId` is required for most actions', { + action, + }); + } + + let adjustedTimeoutMs = timeoutMs; + if (adjustedTimeoutMs < 1000) { + adjustedTimeoutMs = 1000; + } + + let adjustedTimeout = timeout; + if (adjustedTimeout < 1) { + adjustedTimeout = 1; + } + + logger.debug('[E2BCode] Processing request', { + action, + language, + sessionId, + }); + + try { + switch (action) { + case 'help': + if (command_name) { + // Return detailed help about the specified command + const detailedHelp = this.getDetailedHelp(command_name.trim()); + if (detailedHelp) { + return JSON.stringify({ message: detailedHelp }); + } else { + return JSON.stringify({ + message: `No detailed help available for command '${command_name}'.`, + }); + } + } else { + // Return overview of available commands + const commandList = [ + 'help', + 'create', + 'list_sandboxes', + 'kill', + 'set_timeout', + 'shell', + 'kill_command', + 'write_file', + 'read_file', + 'install', + 'system_install', + 'get_file_downloadurl', + 'get_host', + 'command_run', + 'start_server', + 'command_list', + 'command_kill', + 'processinfo', + ]; + const overview = `Available actions: ${commandList.join(', ')}. Use 'help' with a command name to get detailed help about a specific command. You are HIGHLY encouraged to run help for system_install, command_run, shell and start_server to understand the differences between them and how to use them.`; + return JSON.stringify({ message: overview }); + } + + case 'create': { + if (sandboxes.has(sessionId)) { + logger.error('[E2BCode] Sandbox already exists', { sessionId }); + throw new Error(`Sandbox with sessionId ${sessionId} already exists.`); + } + logger.debug('[E2BCode] Creating new sandbox', { + sessionId, + timeout: adjustedTimeout, + }); + const sandboxCreateOptions = { + apiKey: this.apiKey, + timeoutMs: adjustedTimeout * 60 * 1000, + }; + + // Get hidden environment variables + const hiddenEnvVarsCreate = this.getHiddenEnvVars(); + // Merge hidden env vars with any provided envs, without exposing hidden vars to the LLM + if (Object.keys(hiddenEnvVarsCreate).length > 0 || envs) { + sandboxCreateOptions.env = { + ...hiddenEnvVarsCreate, + ...envs, + }; + } + + let sandboxCreate; + let skippedTemplate = false; + //Try to use a template if provided: + if (template) { + try { + sandboxCreate = await Sandbox.create(template, sandboxCreateOptions); + } catch (error) { + //Try without a template: + try { + sandboxCreate = await Sandbox.create(sandboxCreateOptions); + skippedTemplate = true; + } catch (error) { + logger.error('[E2BCode] Error creating sandbox', { sessionId, error: error.message }); + throw new Error(`Error creating sandbox: ${error.message}`); + } + } + } else { + //Try without a template: + try { + sandboxCreate = await Sandbox.create(sandboxCreateOptions); + } catch (error) { + logger.error('[E2BCode] Error creating sandbox', { sessionId, error: error.message }); + throw new Error(`Error creating sandbox: ${error.message}`); + } + } + + sandboxes.set(sessionId, { + sandbox: sandboxCreate, + lastAccessed: Date.now(), + commands: new Map(), + }); + + // Get current user and current directory inside the sandbox + const whoamiResult = await sandboxCreate.commands.run('whoami'); + const currentUser = whoamiResult.stdout.trim(); + + const pwdResult = await sandboxCreate.commands.run('pwd'); + const currentDirectory = pwdResult.stdout.trim(); + + // Get sandbox ID + const createdSandboxId = sandboxCreate.sandboxId; + + let message; + if (!skippedTemplate) { + message = `Sandbox created with sandboxId ${createdSandboxId} from template '${template}' with timeout ${adjustedTimeout} minutes.`; + } else { + message = `Sandbox created with sandboxId ${createdSandboxId} with timeout ${adjustedTimeout} minutes. There was an error attempting to use the template so none was used.`; + } + + //Add the current user and current directory to the response + message += ` You are user ${currentUser} and current directory is ${currentDirectory}.`; + + return JSON.stringify({ + sessionId, + sandboxId: createdSandboxId, + currentUser, + currentDirectory, + success: true, + message: message, + }); + } + + case 'list_sandboxes': + logger.debug('[E2BCode] Listing all active sandboxes'); + try { + const sandboxesList = await Sandbox.list({ apiKey: this.apiKey }); + if (sandboxesList.length === 0) { + logger.debug('[E2BCode] No active sandboxes found'); + return JSON.stringify({ + message: 'No active sandboxes found', + }); + } + // Map sandbox info to include sandboxId and any other relevant details + const sandboxDetails = sandboxesList.map((sandbox) => { + const [id] = sandbox.sandboxId.split('-'); + return { + sandboxId: id, + createdAt: sandbox.createdAt, + status: sandbox.status, + }; + }); + return JSON.stringify({ + message: 'Active sandboxes found', + sandboxes: sandboxDetails, + }); + } catch (error) { + logger.error('[E2BCode] Error listing sandboxes', { error: error.message }); + return JSON.stringify({ + error: 'Error listing sandboxes: ' + error.message, + }); + } + + case 'kill': + let killSandboxId = sandboxId; + if (!killSandboxId) { + // Try to get it from sessionId mapping + if (sandboxes.has(sessionId)) { + const sandboxInfo = sandboxes.get(sessionId); + killSandboxId = sandboxInfo.sandbox.sandboxId; + } + } + if (!killSandboxId) { + logger.error('[E2BCode] No sandboxId or sessionId provided to kill', { sessionId }); + throw new Error(`No sandboxId or sessionId provided. Cannot kill sandbox.`); + } + const [validSandboxId] = killSandboxId.split('-'); + logger.debug('[E2BCode] Killing sandbox', { sessionId, validSandboxId }); + let sandboxToKill; + try { + sandboxToKill = await Sandbox.connect(validSandboxId, { apiKey: this.apiKey }); + } catch (error) { + logger.error('[E2BCode] Error connecting to sandbox to kill', { sessionId, validSandboxId, error: error.message }); + if (sandboxes.has(sessionId)) { + sandboxes.delete(sessionId); + } + return JSON.stringify({ + sessionId, + success: false, + message: `No sandbox found with sandboxId ${validSandboxId} and sessionId ${sessionId}.`, + }); + } + + try { + await sandboxToKill.kill(); + } catch (error) { + logger.error('[E2BCode] Error killing sandbox', { sessionId, validSandboxId, error: error.message }); + if (sandboxes.has(sessionId)) { + sandboxes.delete(sessionId); + } + return JSON.stringify({ + sessionId, + success: false, + message: `Failed to kill sandbox with sandboxId ${validSandboxId} and sessionId ${sessionId}.`, + }); + } + + if (sandboxes.has(sessionId)) { + sandboxes.delete(sessionId); + } + + return JSON.stringify({ + sessionId, + success: true, + message: `Sandbox with sessionId ${sessionId} and sandboxId ${validSandboxId} has been killed.`, + }); + + case 'set_timeout': + if (!sandboxes.has(sessionId)) { + logger.error('[E2BCode] No sandbox found to set timeout', { + sessionId, + }); + throw new Error(`No sandbox found with sessionId ${sessionId}.`); + } + if (!timeout) { + logger.error( + '[E2BCode] `timeout` is required for set_timeout action', + { sessionId } + ); + throw new Error('`timeout` is required for `set_timeout` action.'); + } + logger.debug('[E2BCode] Setting sandbox timeout', { + sessionId, + timeout: adjustedTimeout, + }); + const { sandbox: sandboxSetTimeout } = sandboxes.get(sessionId); + await sandboxSetTimeout.setTimeout(adjustedTimeout * 60 * 1000); + return JSON.stringify({ + sessionId, + success: true, + message: `Sandbox timeout updated to ${adjustedTimeout} minutes.`, + }); + + default: + // For other actions, proceed to get the sandbox + const sandboxInfo = await this.getSandboxInfo(sessionId); + const sandbox = sandboxInfo.sandbox; + // Get hidden environment variables + const hiddenEnvVars = this.getHiddenEnvVars(); + + switch (action) { + case 'shell': + if (!cmd) { + logger.error('[E2BCode] Command (cmd) missing for shell action', { + sessionId, + }); + throw new Error('Command (cmd) is required for `shell` action.'); + } + logger.debug('[E2BCode] Executing shell command', { + sessionId, + cmd, + background, + }); + const shellOptions = {}; + if (Object.keys(hiddenEnvVars).length > 0 || envs) { + shellOptions.envs = { + ...hiddenEnvVars, + ...envs, + }; + } + if (background) { + shellOptions.background = true; + const backgroundCommand = await sandbox.commands.run( + cmd, + shellOptions + ); + const cmdId = backgroundCommand.id; + sandboxInfo.commands.set(cmdId, backgroundCommand); + logger.debug('[E2BCode] Background command started', { + sessionId, + commandId: cmdId, + }); + return JSON.stringify({ + sessionId, + commandId: cmdId, + success: true, + message: `Background command started with ID ${cmdId}`, + }); + } else { + const shellResult = await sandbox.commands.run( + cmd, + shellOptions + ); + logger.debug('[E2BCode] Shell command completed', { + sessionId, + exitCode: shellResult.exitCode, + }); + return JSON.stringify({ + sessionId, + output: shellResult.stdout, + error: shellResult.stderr, + exitCode: shellResult.exitCode, + }); + } + + case 'kill_command': + if (!commandId) { + logger.error( + '[E2BCode] `commandId` missing for kill_command action', + { sessionId } + ); + throw new Error( + '`commandId` is required for `kill_command` action.' + ); + } + logger.debug('[E2BCode] Killing background command', { + sessionId, + commandId, + }); + const commandToKill = sandboxInfo.commands.get(commandId); + if (!commandToKill) { + logger.error('[E2BCode] No command found to kill', { + sessionId, + commandId, + }); + throw new Error( + `No background command found with ID ${commandId}.` + ); + } + await commandToKill.kill(); + sandboxInfo.commands.delete(commandId); + return JSON.stringify({ + sessionId, + success: true, + message: `Background command with ID ${commandId} has been killed.`, + }); + + case 'write_file': + if (!filePath || !fileContent) { + logger.error( + '[E2BCode] Missing parameters for write_file action', + { + sessionId, + hasFilePath: !!filePath, + hasContent: !!fileContent, + } + ); + throw new Error( + '`filePath` and `fileContent` are required for `write_file` action.' + ); + } + logger.debug('[E2BCode] Writing file', { sessionId, filePath }); + await sandbox.files.write(filePath, fileContent); + logger.debug('[E2BCode] File written successfully', { + sessionId, + filePath, + }); + return JSON.stringify({ + sessionId, + success: true, + message: `File written to ${filePath}`, + }); + + case 'read_file': + if (!filePath) { + logger.error( + '[E2BCode] `filePath` missing for read_file action', + { sessionId } + ); + throw new Error('`filePath` is required for `read_file` action.'); + } + logger.debug('[E2BCode] Reading file', { sessionId, filePath }); + const content = await sandbox.files.read(filePath); + logger.debug('[E2BCode] File read successfully', { + sessionId, + filePath, + }); + return JSON.stringify({ + sessionId, + content: content.toString(), + success: true, + }); + + case 'install': + if (!packages || packages.length === 0) { + logger.error( + '[E2BCode] Packages missing for install action', + { + sessionId, + language, + } + ); + throw new Error('`packages` array is required for `install` action.'); + } + logger.debug('[E2BCode] Installing packages', { + sessionId, + language, + packages, + }); + const installOptions = {}; + if (Object.keys(hiddenEnvVars).length > 0 || envs) { + installOptions.envs = { + ...hiddenEnvVars, + ...envs, + }; + } + if (language === 'python') { + const pipResult = await sandbox.commands.run( + `pip install ${packages.join(' ')}`, + installOptions + ); + logger.debug( + '[E2BCode] Python package installation completed', + { + sessionId, + success: pipResult.exitCode === 0, + } + ); + return JSON.stringify({ + sessionId, + success: pipResult.exitCode === 0, + output: pipResult.stdout, + error: pipResult.stderr, + }); + } else if (language === 'javascript' || language === 'typescript') { + const npmResult = await sandbox.commands.run( + `npm install ${packages.join(' ')}`, + installOptions + ); + logger.debug( + '[E2BCode] Node package installation completed', + { + sessionId, + success: npmResult.exitCode === 0, + } + ); + return JSON.stringify({ + sessionId, + success: npmResult.exitCode === 0, + output: npmResult.stdout, + error: npmResult.stderr, + }); + } else { + logger.error( + '[E2BCode] Unsupported language for package installation', + { sessionId, language } + ); + throw new Error( + `Unsupported language for package installation: ${language}` + ); + } + + case 'get_file_downloadurl': + if (!filePath) { + logger.error( + '[E2BCode] `filePath` is required for get_file_downloadurl action', + { + sessionId, + } + ); + throw new Error( + '`filePath` is required for `get_file_downloadurl` action.' + ); + } + logger.debug('[E2BCode] Generating download URL for file', { + sessionId, + filePath, + }); + const downloadUrl = await sandbox.downloadUrl(filePath); + logger.debug('[E2BCode] Download URL generated', { + sessionId, + filePath, + downloadUrl, + }); + return JSON.stringify({ + sessionId, + success: true, + downloadUrl, + message: `Download URL generated for ${filePath}`, + }); + + case 'get_host': + if (!port) { + logger.error('[E2BCode] `port` is required for get_host action', { + sessionId, + }); + throw new Error('`port` is required for `get_host` action.'); + } + logger.debug('[E2BCode] Getting host+port', { sessionId, port }); + const host = await sandbox.getHost(port); + logger.debug('[E2BCode] Host+port retrieved', { sessionId, host }); + return JSON.stringify({ + sessionId, + host, + port, + message: `Host+port retrieved for port ${port}`, + }); + + case 'system_install': + if (!packages || packages.length === 0) { + logger.error('[E2BCode] Packages missing for system_install action', { sessionId }); + throw new Error('`packages` array is required for `system_install` action.'); + } + logger.debug('[E2BCode] Installing system packages', { + sessionId, + packages, + }); + const aptGetInstallCommand = `sudo apt-get update && sudo apt-get install -y ${packages.join(' ')}`; + const systemInstallOptions = {}; + if (Object.keys(hiddenEnvVars).length > 0 || envs) { + systemInstallOptions.envs = { + ...hiddenEnvVars, + ...envs, + }; + } + const aptGetResult = await sandbox.commands.run(aptGetInstallCommand, systemInstallOptions); + logger.debug('[E2BCode] System package installation completed', { + sessionId, + success: aptGetResult.exitCode === 0, + }); + return JSON.stringify({ + sessionId, + success: aptGetResult.exitCode === 0, + output: aptGetResult.stdout, + error: aptGetResult.stderr, + }); + + case 'command_run': + if (!cmd) { + logger.error('[E2BCode] `cmd` is missing for command_run action', { + sessionId, + }); + throw new Error('`cmd` is required for `command_run` action.'); + } + logger.debug('[E2BCode] Running command', { + sessionId, + cmd, + background, + }); + const commandOptions = {}; + if (background !== undefined) { + commandOptions.background = background; + } + if (cwd) { + commandOptions.cwd = cwd; + } + if (adjustedTimeoutMs) { + commandOptions.timeoutMs = adjustedTimeoutMs; + } + if (user) { + commandOptions.user = user; + } + if (Object.keys(hiddenEnvVars).length > 0 || envs) { + commandOptions.envs = { + ...hiddenEnvVars, + ...envs, + }; + } + if (background) { + const commandHandle = await sandbox.commands.run(cmd, commandOptions); + const cmdId = commandHandle.id; + sandboxInfo.commands.set(cmdId, commandHandle); + logger.debug('[E2BCode] Background command started', { + sessionId, + commandId: cmdId, + }); + return JSON.stringify({ + sessionId, + commandId: cmdId, + success: true, + message: `Background command started with ID ${cmdId}`, + }); + } else { + const commandResult = await sandbox.commands.run(cmd, commandOptions); + logger.debug('[E2BCode] Command execution completed', { + sessionId, + exitCode: commandResult.exitCode, + }); + return JSON.stringify({ + sessionId, + stdout: commandResult.stdout, + stderr: commandResult.stderr, + exitCode: commandResult.exitCode, + success: commandResult.exitCode === 0, + }); + } + + case 'start_server': + if (!cmd) { + logger.error('[E2BCode] `cmd` is missing for start_server action', { + sessionId, + }); + throw new Error('`cmd` is required for `start_server` action.'); + } + if (!port) { + logger.error('[E2BCode] `port` is missing for start_server action', { + sessionId, + }); + throw new Error('`port` is required for `start_server` action.'); + } + if (!logFile) { + logger.error('[E2BCode] `logFile` is missing for start_server action', { + sessionId, + }); + throw new Error('`logFile` is required for `start_server` action.'); + } + logger.debug('[E2BCode] Starting server', { + sessionId, + cmd, + port, + logFile, + }); + const serverCommand = `${cmd} > ${logFile} 2>&1`; + const serverOptions = {}; + serverOptions.background = true; + if (cwd) { + serverOptions.cwd = cwd; + } + if (adjustedTimeoutMs) { + serverOptions.timeoutMs = adjustedTimeoutMs; + } + if (user) { + serverOptions.user = user; + } + if (Object.keys(hiddenEnvVars).length > 0 || envs) { + serverOptions.envs = { + ...hiddenEnvVars, + ...envs, + }; + } + const serverHandle = await sandbox.commands.run( + serverCommand, + serverOptions + ); + const serverCommandId = serverHandle.id; + sandboxInfo.commands.set(serverCommandId, serverHandle); + logger.debug('[E2BCode] Server started', { + sessionId, + commandId: serverCommandId, + }); + const serverHost = await sandbox.getHost(port); + logger.debug('[E2BCode] Host+port retrieved', { sessionId, serverHost }); + return JSON.stringify({ + sessionId, + commandId: serverCommandId, + success: true, + serverHost, + logFile, + message: `Server started with ID ${serverCommandId}, accessible at ${serverHost}:${port}. Logs are redirected to ${logFile}`, + }); + + case 'command_list': + // Retrieve the list of running commands and PTY sessions + const processList = await sandbox.commands.list(); + logger.debug('[E2BCode] Retrieved list of commands', { + sessionId, + processCount: processList.length, + }); + return JSON.stringify({ + sessionId, + success: true, + processes: processList, + }); + + case 'command_kill': + if (pid === undefined) { + logger.error( + '[E2BCode] `pid` is missing for `command_kill` action', + { sessionId } + ); + throw new Error('`pid` is required for `command_kill` action.'); + } + logger.debug('[E2BCode] Killing process', { + sessionId, + pid, + }); + const killResult = await sandbox.commands.kill(pid); + if (killResult) { + logger.debug('[E2BCode] Process killed successfully', { + sessionId, + pid, + }); + return JSON.stringify({ + sessionId, + success: true, + message: `Process with PID ${pid} has been killed.`, + }); + } else { + logger.error('[E2BCode] Failed to kill process', { + sessionId, + pid, + }); + return JSON.stringify({ + sessionId, + success: false, + message: `Failed to kill process with PID ${pid}.`, + }); + } + + case 'processinfo': + if (pid === undefined) { + logger.error( + '[E2BCode] `pid` is missing for `processinfo` action', + { sessionId } + ); + throw new Error('`pid` is required for `processinfo` action.'); + } + logger.debug('[E2BCode] Getting process info', { + sessionId, + pid, + }); + const processinfo_processList = await sandbox.commands.list(); + const processInfo = processinfo_processList.find((p) => p.pid === pid); + if (processInfo) { + logger.debug('[E2BCode] Process info retrieved', { + sessionId, + pid, + }); + return JSON.stringify({ + sessionId, + success: true, + process: processInfo, + }); + } else { + logger.error('[E2BCode] Process not found', { + sessionId, + pid, + }); + return JSON.stringify({ + sessionId, + success: false, + message: `No process found with PID ${pid}.`, + }); + } + + default: + logger.error('[E2BCode] Unknown action requested', { + sessionId, + action, + }); + throw new Error(`Unknown action: ${action}`); + } + } + } catch (error) { + logger.error('[E2BCode] Error during execution', { + sessionId, + action, + error: error.message, + }); + return JSON.stringify({ + sessionId, + error: error.message, + success: false, + }); + } + } + + // Method to get an existing sandbox and its info based on sessionId + async getSandboxInfo(sessionId) { + if (sandboxes.has(sessionId)) { + logger.debug('[E2BCode] Reusing existing sandbox', { sessionId }); + const sandboxInfo = sandboxes.get(sessionId); + sandboxInfo.lastAccessed = Date.now(); + return sandboxInfo; + } + logger.error('[E2BCode] No sandbox found for session', { sessionId }); + throw new Error( + `No sandbox found for sessionId ${sessionId}. Please create one using the 'create' action.` + ); + } +} + +module.exports = E2BCode; \ No newline at end of file diff --git a/api/app/clients/tools/structured/E2BCode.md b/api/app/clients/tools/structured/E2BCode.md new file mode 100644 index 00000000000..b890cf718b6 --- /dev/null +++ b/api/app/clients/tools/structured/E2BCode.md @@ -0,0 +1,433 @@ +# E2BCode Plugin for LibreChat + +## Overview + +The **E2BCode Plugin** integrates the capabilities of the [E2B Code Interpreter](https://e2b.dev/) into [LibreChat](https://librechat.ai/), allowing users to execute code, run shell commands, manage files, and more within an isolated sandbox environment directly from the chat interface. This plugin leverages the E2B SDK and is built as a LangChain plugin. + +## Features + +- **Isolated Sandboxing**: Run code and commands in a secure, isolated sandbox. +- **Code Execution**: Supports execution of code in Python, JavaScript, TypeScript, and Shell. +- **File Management**: Read from and write to files within the sandbox. +- **Package Installation**: Install packages using `pip` or `npm`. +- **Environment Variables**: Set environment variables for sandbox environments and executions. +- **Background Processes**: Run and manage background shell commands. +- **Timeout Management**: Configure and adjust sandbox timeouts. + +--- + +## Prerequisites + +- **LibreChat** installed and running. +- **Node.js** (version 14.x or higher). +- **E2B API Key**: Sign up at [E2B](https://e2b.dev/) to obtain an API key. + +--- + +## Installation and Configuration + +### 1. Clone the Repository + +Clone the LibreChat fork at https://github.com/jmaddington/libreChat/ to access the E2BCode plugin. + +Not all branches have the E2BCode plugin. `jm-production` is a branch that has the plugin. Probably. + + +### 2. Set Environment Variables + +Create a `.env` file in the plugin directory or set environment variables in your system. The plugin requires the following environment variables: + +- `E2B_API_KEY`: Your E2B API key. +- Any environment variables that start with `E2B_CODE_EV_` will be passed to the sandbox without being revealed to the LLM. For example, to pass a variable `SECRET_KEY`, you would set `E2B_CODE_EV_SECRET_KEY`. + +Example `.env` file: + +```dotenv +E2B_API_KEY=your_e2b_api_key +E2B_CODE_EV_SECRET_KEY=your_secret_value +``` + +--- + +## Usage Instructions + +### Overview + +The E2BCode plugin allows you to interact with an isolated sandbox environment directly from LibreChat. You can perform various actions such as creating sandboxes, executing code, running shell commands, managing files, installing packages, and more. + +### General Workflow + +1. **Create a Sandbox**: Before executing any code or commands, you need to create a new sandbox environment. +2. **Perform Actions**: Use the various actions provided by the plugin to interact with the sandbox. +3. **Manage Sandbox**: Adjust the sandbox timeout or kill the sandbox when done. + +### Available Actions + +Below are the actions you can perform, along with the required parameters: + +1. **create** + + - **Purpose**: Create a new sandbox environment. + - **Parameters**: + - `sessionId` (required): A unique identifier to maintain session state. + - `timeout` (optional): Timeout in minutes for the sandbox environment (default is 60 minutes). + - `envs` (optional): Environment variables to set when creating the sandbox. + - **Example**: + + ```json + { + "action": "create", + "sessionId": "unique_session_id", + "timeout": 120, + "envs": { "MY_VAR": "my_value" } + } + ``` + +2. **list_sandboxes** + + - **Purpose**: List all active sandboxes. + - **Parameters**: Only `action` parameter. + - **Example**: + + ```json + { + "action": "list_sandboxes" + } + ``` + +3. **set_timeout** + + - **Purpose**: Change the timeout of an existing sandbox. + - **Parameters**: + - `sessionId` (required) + - `timeout` (required): New timeout in minutes. + - **Example**: + + ```json + { + "action": "set_timeout", + "sessionId": "unique_session_id", + "timeout": 180 + } + ``` + +4. **kill** + + - **Purpose**: Terminate an existing sandbox. + - **Parameters**: + - `sessionId` (required) + - **Example**: + + ```json + { + "action": "kill", + "sessionId": "unique_session_id" + } + ``` + +5. **execute** + + - **Purpose**: Execute code within the sandbox. + - **Parameters**: + - `sessionId` (required) + - `code` (required): The code to execute. + - `language` (optional): Programming language (`"python"`, `"javascript"`, `"typescript"`, `"shell"`). Defaults to `"python"`. + - `envs` (optional): Environment variables for the execution. + - **Example**: + + ```json + { + "action": "execute", + "sessionId": "unique_session_id", + "code": "import os; print(os.environ.get('MY_VAR'))", + "language": "python", + "envs": { "MY_VAR": "value" } + } + ``` + +6. **shell** + + - **Purpose**: Run a shell command within the sandbox. + - **Parameters**: + - `sessionId` (required) + - `command` (required): The shell command to execute. + - `background` (optional): Whether to run the command in the background. Defaults to `false`. + - `envs` (optional) + - **Example (foreground)**: + + ```json + { + "action": "shell", + "sessionId": "unique_session_id", + "command": "ls -la" + } + ``` + + - **Example (background)**: + + ```json + { + "action": "shell", + "sessionId": "unique_session_id", + "command": "python app.py > output.log", + "background": true + } + ``` + +7. **kill_command** + + - **Purpose**: Terminate a background command. + - **Parameters**: + - `sessionId` (required) + - `commandId` (required): The ID of the background command to kill. + - **Example**: + + ```json + { + "action": "kill_command", + "sessionId": "unique_session_id", + "commandId": "command_id_from_background_command" + } + ``` + +8. **write_file** + + - **Purpose**: Write content to a file in the sandbox. + - **Parameters**: + - `sessionId` (required) + - `filePath` (required): Path to the file. + - `fileContent` (required): Content to write. + - **Example**: + + ```json + { + "action": "write_file", + "sessionId": "unique_session_id", + "filePath": "/home/user/test.txt", + "fileContent": "Hello, world!" + } + ``` + +9. **read_file** + + - **Purpose**: Read content from a file in the sandbox. + - **Parameters**: + - `sessionId` (required) + - `filePath` (required): Path to the file. + - **Example**: + + ```json + { + "action": "read_file", + "sessionId": "unique_session_id", + "filePath": "/home/user/test.txt" + } + ``` + +10. **install** + + - **Purpose**: Install a package within the sandbox. + - **Parameters**: + - `sessionId` (required) + - `code` (required): Name of the package to install. + - `language` (optional): Programming language (`"python"`, `"javascript"`, `"typescript"`). Defaults to `"python"`. + - `envs` (optional) + - **Example**: + + ```json + { + "action": "install", + "sessionId": "unique_session_id", + "code": "requests", + "language": "python" + } + ``` + +11. **get_file_downloadurl** + + - **Purpose**: Generate a download URL for a file in the sandbox. + - **Parameters**: + - `sessionId` (required) + - `filePath` (required): Path to the file. + - **Example**: + + ```json + { + "action": "get_file_downloadurl", + "sessionId": "unique_session_id", + "filePath": "/home/user/output.txt" + } + ``` + +12. **get_host** + + - **Purpose**: Get the host and port for accessing a service running in the sandbox. + - **Parameters**: + - `sessionId` (required) + - `port` (required): Port number used by the service. + - **Example**: + + ```json + { + "action": "get_host", + "sessionId": "unique_session_id", + "port": 8080 + } + ``` + +### Steps to Use the Plugin in LibreChat + +1. **Start a New Conversation** + + Open LibreChat and start a new conversation with the assistant. + +2. **Initialize Session** + + Begin by creating a sandbox: + + ```json + { + "action": "create", + "sessionId": "my_unique_session_id", + "timeout": 60 + } + ``` + +3. **Perform Actions** + + Use any of the available actions to interact with the sandbox. Ensure you include the same `sessionId` used when creating the sandbox. + + **Example - Execute Code:** + + ```json + { + "action": "execute", + "sessionId": "my_unique_session_id", + "code": "print('Hello from the sandbox!')" + } + ``` + +4. **View Responses** + + The plugin will return the output or result of the action, which will be displayed in the chat interface. + +5. **Terminate Sandbox** + + When finished, you can kill the sandbox to free up resources: + + ```json + { + "action": "kill", + "sessionId": "my_unique_session_id" + } + ``` + +### Notes on Environment Variables + +- **Passing Hidden Environment Variables**: + + Any environment variable set on the host system that starts with `E2B_CODE_EV_` will be passed to the sandbox without being revealed to the LLM. The prefix `E2B_CODE_EV_` is stripped when passed to the sandbox. + + - **Example**: If you set `E2B_CODE_EV_SECRET_KEY=supersecret`, the sandbox will have an environment variable `SECRET_KEY` set to `supersecret`. + +- **Setting Environment Variables in Actions**: + + You can also pass `envs` in actions to set environment variables for specific executions. + +### Error Handling + +- **Error Responses**: + + If an error occurs while executing an action, you will receive a response containing an `error` message and `success` set to `false`. + + - **Example**: + + ```json + { + "sessionId": "my_unique_session_id", + "error": "Code is required for `execute` action.", + "success": false + } + ``` + +- **Success Indicator**: + + Check the `success` field in the response to determine if the action was successful. + +### Background Commands and Hosting Services + +- **Running Services**: + + To run services like Flask or Node.js servers, use the `shell` action with `background` set to `true` and redirect output to a log file. + +- **Example**: + + ```json + { + "action": "shell", + "sessionId": "my_unique_session_id", + "command": "python app.py > output.log", + "background": true + } + ``` + +- **Accessing the Service**: + + Use the `get_host` action to retrieve the host and port to access the running service. + + ```json + { + "action": "get_host", + "sessionId": "my_unique_session_id", + "port": 5000 + } + ``` + +--- + +## Best Practices + +- **Unique Session IDs**: Always use unique and consistent `sessionId` values for your sessions. +- **Security**: Do not expose sensitive information directly in requests. Use the hidden environment variables feature to securely pass secrets. +- **Resource Management**: Remember to kill sandboxes when they are no longer needed to free up resources. + +--- + +## Troubleshooting + +- **Sandbox Not Found**: + + If you receive an error stating that the sandbox was not found, ensure that you have created a sandbox and are using the correct `sessionId`. + +- **Timeouts**: + + If actions are not completing, check if the sandbox has expired due to the timeout. Increase the timeout if necessary using the `set_timeout` action. + +- **Permissions**: + + Ensure that LibreChat has the necessary permissions and configurations to run plugins and access environment variables. + +--- + +## Support + +For issues related to the E2BCode plugin, please open an issue on the plugin's repository: [GitHub Repository](https://github.com/jmaddington/libreChat/issues) + +For issues related to LibreChat, visit [LibreChat Support](https://librechat.ai) or consult their Discord. + +--- + +## Contributing + +Contributions are welcome! Fork the repository and submit a pull request with your changes. + +--- + +## Acknowledgments + +- **E2B**: For providing the E2B Code Interpreter SDK. +- **LibreChat**: For the chat platform that makes this integration possible. + +--- + +Feel free to reach out if you have any questions or need assistance with setup and usage. diff --git a/api/app/clients/tools/structured/FluxAPI.js b/api/app/clients/tools/structured/FluxAPI.js new file mode 100644 index 00000000000..13b76f07557 --- /dev/null +++ b/api/app/clients/tools/structured/FluxAPI.js @@ -0,0 +1,238 @@ +// FluxAPI.js + +const axios = require('axios'); +const { v4: uuidv4 } = require('uuid'); +const { Tool } = require('@langchain/core/tools'); +const { z } = require('zod'); +const { logger } = require('~/config'); +const { FileContext } = require('librechat-data-provider'); +const { processFileURL } = require('~/server/services/Files/process'); + +class FluxAPI extends Tool { + constructor(fields) { + super(); + + this.override = fields.override ?? false; + this.returnMetadata = fields.returnMetadata ?? false; + + this.userId = fields.userId; + this.fileStrategy = fields.fileStrategy; + + if (fields.processFileURL) { + this.processFileURL = fields.processFileURL.bind(this); + } + + this.name = 'flux'; + this.apiKey = fields.FLUX_API_KEY || this.getApiKey(); + this.description = + "Use Flux to generate images from text descriptions. This tool is exclusively for visual content."; + this.description_for_model = `// Use Flux to generate images from text descriptions. + // Guidelines: + // - Provide a detailed and vivid prompt for the image you want to generate, but don't change it if the user asks you not to. + // - Include parameters for image width and height if necessary (default width: 1024, height: 768). + // - Visually describe the moods, details, structures, styles, and proportions of the image. + // - Craft your input by "showing" and not "telling" the imagery. + // - Generate images only once per human query unless explicitly requested by the user. + // - If the user requests multiple images, set the 'number_of_images' parameter to the desired number (up to 24). + // - Output in PNG format by default. + // - Default to the endpoint /v1/flux-pro-1.1 unless the user says otherwise. + // - Upsample if the user says so. + // - **Include the generated image(s) in your text response to the user by embedding the Markdown links.** + // - **Include the prompt you created for flux in your response so the user can see what you generated.** + + /* Available endpoints: + - /v1/flux-pro-1.1 + - /v1/flux-pro + - /v1/flux-dev + - /v1/flux-pro-1.1-ultra + */ + `; + + // Define the schema for structured input + this.schema = z.object({ + prompt: z.string().describe('Text prompt for image generation.'), + width: z + .number() + .optional() + .describe( + 'Width of the generated image in pixels. Must be a multiple of 32. Default is 1024.' + ), + height: z + .number() + .optional() + .describe( + 'Height of the generated image in pixels. Must be a multiple of 32. Default is 768.' + ), + prompt_upsampling: z + .boolean() + .optional() + .describe('Whether to perform upsampling on the prompt.'), + steps: z + .number() + .int() + .optional() + .describe('Number of steps to run the model for, a number from 1 to 50. Default is 40.'), + seed: z.number().optional().describe('Optional seed for reproducibility.'), + safety_tolerance: z + .number() + .optional() + .describe( + 'Tolerance level for input and output moderation. Between 0 and 6, 0 being most strict, 6 being least strict.' + ), + output_format: z + .string() + .optional() + .describe('Output format for the generated image. Can be "jpeg" or "png".'), + endpoint: z + .string() + .optional() + .describe('Endpoint to use for image generation. Default is /v1/flux-pro.'), + number_of_images: z + .number() + .int() + .min(1) + .max(24) + .optional() + .describe('Number of images to generate, up to a maximum of 24. Default is 1.'), + }); + } + + getApiKey() { + const apiKey = process.env.FLUX_API_KEY || ''; + if (!apiKey && !this.override) { + throw new Error('Missing FLUX_API_KEY environment variable.'); + } + return apiKey; + } + + wrapInMarkdown(imageUrl) { + return `![generated image](${imageUrl})`; + } + + async _call(data) { + const baseUrl = 'https://api.bfl.ml'; + const { + prompt, + width = 1024, + height = 768, + steps = 40, + prompt_upsampling = false, + seed = null, + safety_tolerance = 2, + output_format = 'png', + endpoint = '/v1/flux-pro', + number_of_images = 1, + } = data; + + const generateUrl = `${baseUrl}${endpoint}`; + const resultUrl = `${baseUrl}/v1/get_result`; + + const payload = { + prompt, + width, + height, + steps, + prompt_upsampling, + seed, + safety_tolerance, + output_format, + }; + + const headers = { + 'x-key': this.apiKey, + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + + const totalImages = Math.min(Math.max(number_of_images, 1), 24); + + const imageResults = []; + + for (let i = 0; i < totalImages; i++) { + let taskResponse; + try { + taskResponse = await axios.post(generateUrl, payload, { headers }); + } catch (error) { + logger.error( + '[FluxAPI] Error while submitting task:', + error.response ? error.response.data : error.message + ); + imageResults.push('Error submitting task to Flux API.'); + continue; + } + + const taskId = taskResponse.data.id; + + // Polling for the result + let status = 'Pending'; + let resultData = null; + while (status !== 'Ready' && status !== 'Error') { + try { + await new Promise((resolve) => setTimeout(resolve, 2000)); + const resultResponse = await axios.get(resultUrl, { + headers, + params: { id: taskId }, + }); + status = resultResponse.data.status; + + if (status === 'Ready') { + resultData = resultResponse.data.result; + break; + } else if (status === 'Error') { + logger.error('[FluxAPI] Error in task:', resultResponse.data); + imageResults.push('Error occurred during image generation.'); + break; + } + } catch (error) { + logger.error( + '[FluxAPI] Error while getting result:', + error.response ? error.response.data : error.message + ); + imageResults.push('Error getting result from Flux API.'); + break; + } + } + + if (!resultData || !resultData.sample) { + logger.error('[FluxAPI] No image data received from API. Response:', resultData); + imageResults.push('No image data received from Flux API.'); + continue; + } + + const imageUrl = resultData.sample; + const imageName = `img-${uuidv4()}.png`; + + try { + const result = await this.processFileURL({ + fileStrategy: this.fileStrategy, + userId: this.userId, + URL: imageUrl, + fileName: imageName, + basePath: 'images', + context: FileContext.image_generation, + }); + + if (this.returnMetadata) { + imageResults.push(result); + } else { + const markdownImage = this.wrapInMarkdown(result.filepath); + imageResults.push(markdownImage); + } + } catch (error) { + logger.error('Error while saving the image:', error); + imageResults.push(`Failed to save the image locally. ${error.message}`); + } + } // End of loop + + if (this.returnMetadata) { + this.result = imageResults; + } else { + // Join the markdown image links with double newlines for better spacing + this.result = imageResults.join('\n\n'); + } + + return this.result; + } +} + +module.exports = FluxAPI; \ No newline at end of file diff --git a/api/app/clients/tools/structured/OpenWeather.js b/api/app/clients/tools/structured/OpenWeather.js new file mode 100644 index 00000000000..4223b1d344a --- /dev/null +++ b/api/app/clients/tools/structured/OpenWeather.js @@ -0,0 +1,293 @@ +const { StructuredTool } = require('langchain/tools'); +const { z } = require('zod'); +const { getEnvironmentVariable } = require('@langchain/core/utils/env'); +const fetch = require('node-fetch'); + +// Utility to retrieve API key +function getApiKey(envVar, override, providedKey) { + if (providedKey) return providedKey; + const key = getEnvironmentVariable(envVar); + if (!key && !override) { + throw new Error(`Missing ${envVar} environment variable.`); + } + return key; +} + +/** + * Map user-friendly units to OpenWeather units. + * Defaults to Celsius if not specified. + */ +function mapUnitsToOpenWeather(unit) { + if (!unit) return 'metric'; // Default to Celsius + switch (unit) { + case 'Celsius': + return 'metric'; + case 'Kelvin': + return 'standard'; + case 'Fahrenheit': + return 'imperial'; + default: + return 'metric'; // fallback + } +} + +/** + * Recursively round temperature fields in the API response. + */ +function roundTemperatures(obj) { + const tempKeys = new Set([ + 'temp', 'feels_like', 'dew_point', + 'day', 'min', 'max', 'night', 'eve', 'morn', + 'afternoon', 'morning', 'evening' + ]); + + if (Array.isArray(obj)) { + return obj.map((item) => roundTemperatures(item)); + } else if (obj && typeof obj === 'object') { + for (const key of Object.keys(obj)) { + const value = obj[key]; + if (value && typeof value === 'object') { + obj[key] = roundTemperatures(value); + } else if (typeof value === 'number' && tempKeys.has(key)) { + obj[key] = Math.round(value); + } + } + } + return obj; +} + +class OpenWeather extends StructuredTool { + name = 'OpenWeather'; + description = 'Provides weather data from OpenWeather One Call API 3.0. ' + + 'Actions: help, current_forecast, timestamp, daily_aggregation, overview. ' + + 'If lat/lon not provided, specify "city" for geocoding. ' + + 'Units: "Celsius", "Kelvin", or "Fahrenheit" (default: Celsius). ' + + 'For timestamp action, use "date" in YYYY-MM-DD format.'; + + schema = z.object({ + action: z.enum(["help", "current_forecast", "timestamp", "daily_aggregation", "overview"]), + city: z.string().optional(), + lat: z.number().optional(), + lon: z.number().optional(), + exclude: z.string().optional(), + units: z.enum(["Celsius", "Kelvin", "Fahrenheit"]).optional(), + lang: z.string().optional(), + date: z.string().optional(), // For timestamp and daily_aggregation + tz: z.string().optional() + }); + + constructor(options = {}) { + super(); + const { apiKey, override = false } = options; + this.apiKey = getApiKey('OPENWEATHER_API_KEY', override, apiKey); + } + + async geocodeCity(city) { + const geocodeUrl = `https://api.openweathermap.org/geo/1.0/direct?q=${encodeURIComponent(city)}&limit=1&appid=${this.apiKey}`; + const res = await fetch(geocodeUrl); + const data = await res.json(); + if (!res.ok || !Array.isArray(data) || data.length === 0) { + throw new Error(`Could not find coordinates for city: ${city}`); + } + return { lat: data[0].lat, lon: data[0].lon }; + } + + convertDateToUnix(dateStr) { + const parts = dateStr.split('-'); + if (parts.length !== 3) { + throw new Error("Invalid date format. Expected YYYY-MM-DD."); + } + const year = parseInt(parts[0], 10); + const month = parseInt(parts[1], 10); + const day = parseInt(parts[2], 10); + if (isNaN(year) || isNaN(month) || isNaN(day)) { + throw new Error("Invalid date format. Expected YYYY-MM-DD with valid numbers."); + } + + const dateObj = new Date(Date.UTC(year, month - 1, day, 0, 0, 0)); + if (isNaN(dateObj.getTime())) { + throw new Error("Invalid date provided. Cannot parse into a valid date."); + } + + return Math.floor(dateObj.getTime() / 1000); + } + + async _call(args) { + try { + const { action, city, lat, lon, exclude, units, lang, date, tz } = args; + const owmUnits = mapUnitsToOpenWeather(units); + + if (action === 'help') { + return JSON.stringify({ + title: "OpenWeather One Call API 3.0 Help", + description: "Guidance on using the OpenWeather One Call API 3.0.", + endpoints: { + current_and_forecast: { + endpoint: "data/3.0/onecall", + data_provided: [ + "Current weather", + "Minute forecast (1h)", + "Hourly forecast (48h)", + "Daily forecast (8 days)", + "Government weather alerts" + ], + required_params: [ + ["lat", "lon"], + ["city"] + ], + optional_params: ["exclude", "units (Celsius/Kelvin/Fahrenheit)", "lang"], + usage_example: { + city: "Knoxville, Tennessee", + units: "Fahrenheit", + lang: "en" + } + }, + weather_for_timestamp: { + endpoint: "data/3.0/onecall/timemachine", + data_provided: [ + "Historical weather (since 1979-01-01)", + "Future forecast up to 4 days ahead" + ], + required_params: [ + ["lat", "lon", "date (YYYY-MM-DD)"], + ["city", "date (YYYY-MM-DD)"] + ], + optional_params: ["units (Celsius/Kelvin/Fahrenheit)", "lang"], + usage_example: { + city: "Knoxville, Tennessee", + date: "2020-03-04", + units: "Fahrenheit", + lang: "en" + } + }, + daily_aggregation: { + endpoint: "data/3.0/onecall/day_summary", + data_provided: [ + "Aggregated weather data for a specific date (1979-01-02 to 1.5 years ahead)" + ], + required_params: [ + ["lat", "lon", "date (YYYY-MM-DD)"], + ["city", "date (YYYY-MM-DD)"] + ], + optional_params: ["units (Celsius/Kelvin/Fahrenheit)", "lang", "tz"], + usage_example: { + city: "Knoxville, Tennessee", + date: "2020-03-04", + units: "Celsius", + lang: "en" + } + }, + weather_overview: { + endpoint: "data/3.0/onecall/overview", + data_provided: ["Human-readable weather summary (today/tomorrow)"], + required_params: [ + ["lat", "lon"], + ["city"] + ], + optional_params: ["date (YYYY-MM-DD)", "units (Celsius/Kelvin/Fahrenheit)"], + usage_example: { + city: "Knoxville, Tennessee", + date: "2024-05-13", + units: "Celsius" + } + } + }, + notes: [ + "If lat/lon not provided, you can specify a city name and it will be geocoded.", + "For the timestamp action, provide a date in YYYY-MM-DD format instead of a Unix timestamp.", + "By default, temperatures are returned in Celsius.", + "You can specify units as Celsius, Kelvin, or Fahrenheit.", + "All temperatures are rounded to the nearest degree." + ], + errors: [ + "400: Bad Request (missing/invalid params)", + "401: Unauthorized (check API key)", + "404: Not Found (no data or city)", + "429: Too many requests", + "5xx: Internal error" + ] + }, null, 2); + } + + let finalLat = lat; + let finalLon = lon; + + // If lat/lon not provided but city is given, geocode it + if ((finalLat == null || finalLon == null) && city) { + const coords = await this.geocodeCity(city); + finalLat = coords.lat; + finalLon = coords.lon; + } + + if (["current_forecast", "timestamp", "daily_aggregation", "overview"].includes(action)) { + if (typeof finalLat !== 'number' || typeof finalLon !== 'number') { + return "Error: lat and lon are required and must be numbers for this action (or specify 'city')."; + } + } + + const baseUrl = "https://api.openweathermap.org/data/3.0"; + let endpoint = ""; + const params = new URLSearchParams({ appid: this.apiKey, units: owmUnits }); + + let dt; + if (action === "timestamp") { + if (!date) { + return "Error: For timestamp action, a 'date' in YYYY-MM-DD format is required."; + } + dt = this.convertDateToUnix(date); + } + + if (action === "daily_aggregation" && !date) { + return "Error: date (YYYY-MM-DD) is required for daily_aggregation action."; + } + + switch (action) { + case "current_forecast": + endpoint = "/onecall"; + params.append("lat", String(finalLat)); + params.append("lon", String(finalLon)); + if (exclude) params.append('exclude', exclude); + if (lang) params.append('lang', lang); + break; + case "timestamp": + endpoint = "/onecall/timemachine"; + params.append("lat", String(finalLat)); + params.append("lon", String(finalLon)); + params.append("dt", String(dt)); + if (lang) params.append('lang', lang); + break; + case "daily_aggregation": + endpoint = "/onecall/day_summary"; + params.append("lat", String(finalLat)); + params.append("lon", String(finalLon)); + params.append("date", date); + if (lang) params.append('lang', lang); + if (tz) params.append('tz', tz); + break; + case "overview": + endpoint = "/onecall/overview"; + params.append("lat", String(finalLat)); + params.append("lon", String(finalLon)); + if (date) params.append('date', date); + break; + default: + return `Error: Unknown action: ${action}`; + } + + const url = `${baseUrl}${endpoint}?${params.toString()}`; + const response = await fetch(url); + const json = await response.json(); + if (!response.ok) { + return `Error: OpenWeather API request failed with status ${response.status}: ${json.message || JSON.stringify(json)}`; + } + + const roundedJson = roundTemperatures(json); + return JSON.stringify(roundedJson); + + } catch (err) { + return `Error: ${err.message}`; + } + } +} + +module.exports = OpenWeather; diff --git a/api/app/clients/tools/structured/WebNavigator.js b/api/app/clients/tools/structured/WebNavigator.js new file mode 100644 index 00000000000..6581fde83ee --- /dev/null +++ b/api/app/clients/tools/structured/WebNavigator.js @@ -0,0 +1,281 @@ +const { z } = require('zod'); +const { Tool } = require('@langchain/core/tools'); +const fetch = require('node-fetch'); +const { URL, URLSearchParams } = require('url'); +const cheerio = require('cheerio'); + +class WebNavigator extends Tool { + constructor(fields = {}) { + super(); + + this.name = 'WebNavigator'; + this.description = + 'Simulates a curl action. Useful for making HTTP requests with various methods and parameters. Accepts an array of commands similar to curl.'; + this.description_for_model = `Simulates a curl action by making HTTP requests with various methods and parameters. Accepts input similar to curl commands. + +Guidelines: +- **Default Behavior:** By default, the response will: + - Exclude \`