Compare commits

...

163 Commits

Author SHA1 Message Date
dependabot[bot]
ec4b7da528 build(deps): bump the npm_and_yarn group across 2 directories with 5 updates
Bumps the npm_and_yarn group with 3 updates in the /docs directory: [cross-spawn](https://github.com/moxystudio/node-cross-spawn), [nextra](https://github.com/shuding/nextra) and [nextra-theme-docs](https://github.com/shuding/nextra).
Bumps the npm_and_yarn group with 1 update in the /extensions/react-widget directory: [dompurify](https://github.com/cure53/DOMPurify).


Updates `cross-spawn` from 7.0.3 to 7.0.6
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6)

Updates `nextra` from 2.13.2 to 4.2.17
- [Release notes](https://github.com/shuding/nextra/releases)
- [Commits](https://github.com/shuding/nextra/compare/nextra@2.13.2...nextra@4.2.17)

Updates `nextra-theme-docs` from 2.13.2 to 4.2.17
- [Release notes](https://github.com/shuding/nextra/releases)
- [Commits](https://github.com/shuding/nextra/compare/nextra-theme-docs@2.13.2...nextra-theme-docs@4.2.17)

Updates `micromatch` from 4.0.5 to 4.0.8
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/micromatch/compare/4.0.5...4.0.8)

Updates `dompurify` from 3.2.4 to 3.2.6
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.2.4...3.2.6)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-version: 7.0.6
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: nextra
  dependency-version: 4.2.17
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: nextra-theme-docs
  dependency-version: 4.2.17
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: micromatch
  dependency-version: 4.0.8
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: dompurify
  dependency-version: 3.2.6
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-12 00:16:46 +00:00
Alex
0aceda96e4 Merge pull request #1824 from siiddhantt/refactor/llm-handler
feat: reorganize LLM handler structure with better abstraction
2025-06-11 17:19:50 +01:00
Siddhant Rai
b414f79bc5 fix: adjust width of tool calls display in ConversationBubble component 2025-06-11 19:37:32 +05:30
Siddhant Rai
6e08fe21d0 Merge branch 'refactor/llm-handler' of https://github.com/siiddhantt/DocsGPT into refactor/llm-handler 2025-06-11 19:28:47 +05:30
Siddhant Rai
9b839655a7 refactor: improve tool call result handling and display in conversation components 2025-06-11 19:28:15 +05:30
Siddhant Rai
3353c0ee1d Merge branch 'main' into refactor/llm-handler 2025-06-11 19:27:33 +05:30
Alex
aaecf52c99 refactor: update docs LLM_NAME and MODEL_NAME to LLM_PROVIDER and LLM_NAME 2025-06-11 12:30:34 +01:00
Siddhant Rai
3351f71813 refactor: tool calls sent when pending and after completion 2025-06-11 12:40:32 +05:30
Alex
7490256303 Merge pull request #1830 from ManishMadan2882/main
UI update: attachments in question bubble
2025-06-10 14:46:05 +01:00
ManishMadan2882
041d600e45 (feat:prompts) delete after confirmation 2025-06-10 18:00:11 +05:30
ManishMadan2882
b4e2588a24 (fix:prompts) save when content changes 2025-06-10 17:02:24 +05:30
ManishMadan2882
68dc14c5a1 (feat:attachments) clear after passing 2025-06-10 02:50:07 +05:30
ManishMadan2882
ef35864e16 (fix) type error, ui adjust 2025-06-09 19:50:07 +05:30
ManishMadan2882
c0d385b983 (refactor:attachments) moved to new uploadSlice 2025-06-09 17:10:12 +05:30
ManishMadan2882
b2df431fa4 (feat:attachments) shared conversations route 2025-06-07 20:04:33 +05:30
ManishMadan2882
69a4bd415a (feat:attachment) message input update 2025-06-06 21:52:51 +05:30
ManishMadan2882
4862548e65 (feat:attach) renaming, semantic identifier names 2025-06-06 21:52:10 +05:30
ManishMadan2882
50248cc9ea Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-06-06 18:55:11 +05:30
ManishMadan2882
430822bae3 (feat:attach)state manage, follow camelCase 2025-06-06 18:54:50 +05:30
Siddhant Rai
dd9d18208d Merge branch 'main' into refactor/llm-handler 2025-06-06 17:36:31 +05:30
Siddhant Rai
e5b1a71659 refactor: update fallback LLM initialization to use factory method 2025-06-06 17:23:27 +05:30
Siddhant Rai
35f4b13237 refactor: add fallback LLM configuration options to settings 2025-06-06 17:05:15 +05:30
Siddhant Rai
5f5c31cd5b refactor: enhance LLM fallback handling and streamline method execution 2025-06-06 16:55:57 +05:30
Siddhant Rai
e9530d5ec5 refactor: update env variable names 2025-06-06 15:29:53 +05:30
Siddhant Rai
143f4aa886 refactor: streamline conversation handling and update agent pinning logic 2025-06-06 14:41:44 +05:30
GH Action - Upstream Sync
ece5c8bb31 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-06-06 01:42:12 +00:00
Alex
31baf181a3 fix: default optimisations 2025-06-05 12:21:40 +01:00
ManishMadan2882
3bae30c70c (fix:messages) attachments are for questions 2025-06-05 03:09:37 +05:30
ManishMadan2882
12b18c6bd1 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-06-05 02:54:51 +05:30
ManishMadan2882
787d9e3bf5 (feat:attachments) ui details in bubble 2025-06-05 02:54:36 +05:30
ManishMadan2882
f325b54895 (fix:get_single_conversation) return attachments with filename 2025-06-05 02:53:43 +05:30
GH Action - Upstream Sync
c5616705b0 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-06-04 01:43:58 +00:00
Alex
c0f693d35d remove abount 2025-06-03 15:40:16 +01:00
Alex
52a5f132c1 Merge pull request #1814 from siiddhantt/fix/agents-bugs
fix: shared agent redirect and pinned agents error
2025-06-03 15:37:20 +01:00
Siddhant Rai
f14eac6d10 Merge branch 'main' into fix/agents-bugs 2025-06-03 19:59:10 +05:30
ManishMadan2882
e90fe117ec (feat:attachments) render in bubble 2025-06-03 18:05:47 +05:30
Siddhant Rai
381d737d24 fix: correct vectorstore path in get_vectorstore function 2025-06-03 15:14:00 +05:30
ManishMadan2882
7cab5b3b09 (feat:attachments) store filenames in worker 2025-06-03 04:02:41 +05:30
ManishMadan2882
9f911cb5cb (feat:stream) store attachment_ids for query 2025-06-03 03:30:06 +05:30
Alex
b47af9600f Merge pull request #1821 from ManishMadan2882/main
Chore: Frontend refinements, i18n sync
2025-06-02 10:13:02 +01:00
Siddhant Rai
92c3c707e1 refactor: reorganize LLM handler structure and improve tool call parsing 2025-06-02 12:17:29 +05:30
GH Action - Upstream Sync
5acc54e609 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-06-01 01:58:40 +00:00
Manish Madan
9c6352dd5b Merge pull request #1823 from arc53/dependabot/npm_and_yarn/docs/next-15.3.3
build(deps): bump next from 14.2.26 to 15.3.3 in /docs
2025-05-31 16:08:57 +05:30
GH Action - Upstream Sync
8e29a07df5 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-05-31 01:39:33 +00:00
Manish Madan
bd88cd3a06 Merge pull request #1818 from arc53/dependabot/npm_and_yarn/frontend/eslint-config-prettier-10.1.5
build(deps-dev): bump eslint-config-prettier from 9.1.0 to 10.1.5 in /frontend
2025-05-31 02:58:41 +05:30
dependabot[bot]
f371b9702f build(deps-dev): bump eslint-config-prettier in /frontend
Bumps [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) from 9.1.0 to 10.1.5.
- [Release notes](https://github.com/prettier/eslint-config-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-config-prettier/compare/v9.1.0...v10.1.5)

---
updated-dependencies:
- dependency-name: eslint-config-prettier
  dependency-version: 10.1.5
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-30 21:25:20 +00:00
Manish Madan
3ff4ae29af Merge pull request #1817 from arc53/dependabot/npm_and_yarn/frontend/eslint-plugin-react-7.37.5
build(deps-dev): bump eslint-plugin-react from 7.37.3 to 7.37.5 in /frontend
2025-05-31 02:53:20 +05:30
dependabot[bot]
eae0f2e7a9 build(deps-dev): bump eslint-plugin-react in /frontend
Bumps [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) from 7.37.3 to 7.37.5.
- [Release notes](https://github.com/jsx-eslint/eslint-plugin-react/releases)
- [Changelog](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jsx-eslint/eslint-plugin-react/compare/v7.37.3...v7.37.5)

---
updated-dependencies:
- dependency-name: eslint-plugin-react
  dependency-version: 7.37.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-30 21:18:03 +00:00
Manish Madan
305a98bb79 Merge pull request #1815 from arc53/dependabot/npm_and_yarn/frontend/react-syntax-highlighter-15.6.1
build(deps): bump react-syntax-highlighter from 15.5.0 to 15.6.1 in /frontend
2025-05-31 02:46:15 +05:30
dependabot[bot]
8040a3ed60 build(deps): bump react-syntax-highlighter in /frontend
Bumps [react-syntax-highlighter](https://github.com/react-syntax-highlighter/react-syntax-highlighter) from 15.5.0 to 15.6.1.
- [Release notes](https://github.com/react-syntax-highlighter/react-syntax-highlighter/releases)
- [Changelog](https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/CHANGELOG.MD)
- [Commits](https://github.com/react-syntax-highlighter/react-syntax-highlighter/compare/15.5.0...v15.6.1)

---
updated-dependencies:
- dependency-name: react-syntax-highlighter
  dependency-version: 15.6.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-30 21:11:29 +00:00
dependabot[bot]
bb9de7d9b0 build(deps): bump next from 14.2.26 to 15.3.3 in /docs
Bumps [next](https://github.com/vercel/next.js) from 14.2.26 to 15.3.3.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v14.2.26...v15.3.3)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 15.3.3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-30 21:10:55 +00:00
Manish Madan
d8e8bc0068 Merge pull request #1765 from arc53/dependabot/npm_and_yarn/frontend/vite-6.3.5
build(deps-dev): bump vite from 5.4.14 to 6.3.5 in /frontend
2025-05-31 02:39:41 +05:30
ManishMadan2882
6577e9d852 (chore) peer deps 2025-05-31 02:37:27 +05:30
dependabot[bot]
3f8625c65a build(deps-dev): bump vite from 5.4.14 to 6.3.5 in /frontend
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.14 to 6.3.5.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.5/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.5
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-30 20:08:19 +00:00
ManishMadan2882
92d69636a7 (fix/ui) minor adjustments 2025-05-30 19:33:07 +05:30
ManishMadan2882
9c28817fba (chore:i18n) sync all locales 2025-05-30 19:05:01 +05:30
Siddhant Rai
773788fb32 fix: correct vectorstore path and improve file existence checks in FaissStore 2025-05-30 14:30:51 +05:30
Siddhant Rai
a393ad8e04 refactor: standardize string quotes and improve retriever type handling in RetrieverCreator 2025-05-30 12:50:11 +05:30
ManishMadan2882
71d3714347 (fix:nav) tablets behave like mobile, use tailwind breakpoints 2025-05-29 17:17:29 +05:30
ManishMadan2882
b7e1329c13 (feat:chunksModal) i18n, use wrapperModal 2025-05-29 17:15:13 +05:30
ManishMadan2882
59e6d9d10e Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-05-29 15:47:42 +05:30
ManishMadan2882
46efb446fb (clean:about) purge route 2025-05-29 15:43:58 +05:30
ManishMadan2882
d31e3a54fd (feat:layout) tablet sidebar behave like mobile 2025-05-29 15:43:00 +05:30
Siddhant Rai
c4e471ac47 fix: ensure shared metadata is displayed only when available in SharedAgentCard 2025-05-29 11:16:38 +05:30
Siddhant Rai
3b8733e085 feat: add tool details resolution and update SharedAgentCard to display tool names 2025-05-29 10:58:49 +05:30
Alex
a7c67d83ca Merge pull request #1820 from ManishMadan2882/main
Chore:  Frontend Refinements
2025-05-29 00:53:59 +01:00
ManishMadan2882
8abc1de26d (feat:hooks) update useMediaQuery 2025-05-29 04:01:47 +05:30
ManishMadan2882
2ca9f708a6 (fix:menu) position smartly 2025-05-29 04:00:25 +05:30
ManishMadan2882
f8f369fbb2 (fix/tools) avoid max width for buttons, i18n 2025-05-29 03:58:34 +05:30
ManishMadan2882
3e9155767b (fix:input) placeholder overflow 2025-05-29 03:57:11 +05:30
Siddhant Rai
8cd4195657 feat: add SharedAgentCard to display selected agent in Conversation component 2025-05-28 14:25:37 +05:30
Siddhant Rai
ad1a944276 refactor: agents sharing and shared with me logic 2025-05-28 13:58:55 +05:30
ManishMadan2882
02ff4c5657 (fix:menu) larger strings break 2025-05-28 03:29:48 +05:30
ManishMadan2882
b1b27f2dde (feat:toolConfig) i18n 2025-05-28 01:10:10 +05:30
ManishMadan2882
5097f77469 (feat:toolConfig) ui details, no actions placeholder 2025-05-27 19:02:41 +05:30
Manish Madan
7e826d5002 Merge pull request #1778 from arc53/dependabot/npm_and_yarn/docs/multi-61ed51ac21
build(deps): bump estree-util-value-to-estree and remark-reading-time in /docs
2025-05-27 16:37:32 +05:30
dependabot[bot]
fe8143a56c build(deps): bump estree-util-value-to-estree and remark-reading-time
Bumps [estree-util-value-to-estree](https://github.com/remcohaszing/estree-util-value-to-estree) and [remark-reading-time](https://github.com/mattjennings/remark-reading-time). These dependencies needed to be updated together.

Updates `estree-util-value-to-estree` from 1.3.0 to 3.4.0
- [Release notes](https://github.com/remcohaszing/estree-util-value-to-estree/releases)
- [Commits](https://github.com/remcohaszing/estree-util-value-to-estree/compare/v1.3.0...v3.4.0)

Updates `remark-reading-time` from 2.0.1 to 2.0.2
- [Release notes](https://github.com/mattjennings/remark-reading-time/releases)
- [Commits](https://github.com/mattjennings/remark-reading-time/compare/2.0.1...2.0.2)

---
updated-dependencies:
- dependency-name: estree-util-value-to-estree
  dependency-version: 3.4.0
  dependency-type: indirect
- dependency-name: remark-reading-time
  dependency-version: 2.0.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-27 11:04:37 +00:00
Manish Madan
e5442a713a Merge pull request #1760 from arc53/dependabot/npm_and_yarn/extensions/react-widget/base-x-3.0.11
build(deps-dev): bump base-x from 3.0.9 to 3.0.11 in /extensions/react-widget
2025-05-27 16:32:38 +05:30
dependabot[bot]
1982a46f36 build(deps-dev): bump base-x in /extensions/react-widget
Bumps [base-x](https://github.com/cryptocoinjs/base-x) from 3.0.9 to 3.0.11.
- [Commits](https://github.com/cryptocoinjs/base-x/compare/v3.0.9...v3.0.11)

---
updated-dependencies:
- dependency-name: base-x
  dependency-version: 3.0.11
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-27 11:02:07 +00:00
Manish Madan
c8c3640baf Merge pull request #1743 from arc53/dependabot/npm_and_yarn/frontend/vite-5.4.18
build(deps-dev): bump vite from 5.4.14 to 5.4.18 in /frontend
2025-05-27 15:52:41 +05:30
dependabot[bot]
fdf47b3f2c build(deps-dev): bump vite from 5.4.14 to 5.4.18 in /frontend
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.14 to 5.4.18.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.18/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.18/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 5.4.18
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-27 10:18:54 +00:00
Manish Madan
93fa4b6a37 Merge pull request #1756 from arc53/dependabot/npm_and_yarn/frontend/multi-08a24af093
build(deps): bump react-router and react-router-dom in /frontend
2025-05-27 15:43:27 +05:30
dependabot[bot]
90e9ab70b0 build(deps): bump react-router and react-router-dom in /frontend
Bumps [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) to 7.5.3 and updates ancestor dependency [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom). These dependencies need to be updated together.


Updates `react-router` from 7.1.1 to 7.5.3
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router@7.5.3/packages/react-router)

Updates `react-router-dom` from 7.1.1 to 7.5.3
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.5.3/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router
  dependency-version: 7.5.3
  dependency-type: indirect
- dependency-name: react-router-dom
  dependency-version: 7.5.3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-27 10:10:25 +00:00
Manish Madan
573c2386b7 Merge pull request #1736 from arc53/dependabot/npm_and_yarn/frontend/typescript-5.8.3
build(deps-dev): bump typescript from 5.7.2 to 5.8.3 in /frontend
2025-05-27 15:37:53 +05:30
dependabot[bot]
d2176aeeb9 build(deps-dev): bump typescript from 5.7.2 to 5.8.3 in /frontend
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.7.2 to 5.8.3.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml)
- [Commits](https://github.com/microsoft/TypeScript/commits)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 5.8.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-27 10:04:27 +00:00
Manish Madan
920aec5c3e Merge pull request #1706 from arc53/dependabot/npm_and_yarn/docs/babel/runtime-7.26.10
build(deps): bump @babel/runtime from 7.23.7 to 7.26.10 in /docs
2025-05-27 15:31:39 +05:30
dependabot[bot]
b792c5459a build(deps): bump @babel/runtime from 7.23.7 to 7.26.10 in /docs
Bumps [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime) from 7.23.7 to 7.26.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.10/packages/babel-runtime)

---
updated-dependencies:
- dependency-name: "@babel/runtime"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-27 10:00:19 +00:00
Manish Madan
87fbf05fa1 Merge pull request #1707 from arc53/dependabot/npm_and_yarn/extensions/react-widget/babel/helpers-7.26.10
build(deps): bump @babel/helpers from 7.24.6 to 7.26.10 in /extensions/react-widget
2025-05-27 15:28:08 +05:30
dependabot[bot]
67c53250c5 build(deps): bump @babel/helpers in /extensions/react-widget
Bumps [@babel/helpers](https://github.com/babel/babel/tree/HEAD/packages/babel-helpers) from 7.24.6 to 7.26.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.10/packages/babel-helpers)

---
updated-dependencies:
- dependency-name: "@babel/helpers"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-27 09:55:00 +00:00
Manish Madan
d657eea910 Merge pull request #1705 from arc53/dependabot/npm_and_yarn/docs/babel/helpers-7.26.10
build(deps): bump @babel/helpers from 7.24.0 to 7.26.10 in /docs
2025-05-27 15:23:01 +05:30
dependabot[bot]
b5fbb825ed build(deps): bump @babel/helpers from 7.24.0 to 7.26.10 in /docs
Bumps [@babel/helpers](https://github.com/babel/babel/tree/HEAD/packages/babel-helpers) from 7.24.0 to 7.26.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.10/packages/babel-helpers)

---
updated-dependencies:
- dependency-name: "@babel/helpers"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-27 09:51:46 +00:00
Manish Madan
d094e7a4c6 Merge pull request #1704 from arc53/dependabot/npm_and_yarn/frontend/babel/runtime-7.26.10
build(deps): bump @babel/runtime from 7.25.0 to 7.26.10 in /frontend
2025-05-27 15:19:39 +05:30
dependabot[bot]
945c155b17 build(deps): bump @babel/runtime from 7.25.0 to 7.26.10 in /frontend
Bumps [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime) from 7.25.0 to 7.26.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.10/packages/babel-runtime)

---
updated-dependencies:
- dependency-name: "@babel/runtime"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-27 09:44:53 +00:00
Manish Madan
f798072a1e Merge pull request #1632 from arc53/dependabot/npm_and_yarn/extensions/react-widget/dompurify-3.2.4
build(deps): bump dompurify from 3.1.5 to 3.2.4 in /extensions/react-widget
2025-05-27 15:04:39 +05:30
Manish Madan
f967214b57 Merge pull request #1524 from arc53/dependabot/npm_and_yarn/frontend/react-redux-9.2.0
build(deps): bump react-redux from 8.1.3 to 9.2.0 in /frontend
2025-05-27 15:03:56 +05:30
dependabot[bot]
d0b92e2540 build(deps): bump react-redux from 8.1.3 to 9.2.0 in /frontend
Bumps [react-redux](https://github.com/reduxjs/react-redux) from 8.1.3 to 9.2.0.
- [Release notes](https://github.com/reduxjs/react-redux/releases)
- [Changelog](https://github.com/reduxjs/react-redux/blob/master/CHANGELOG.md)
- [Commits](https://github.com/reduxjs/react-redux/compare/v8.1.3...v9.2.0)

---
updated-dependencies:
- dependency-name: react-redux
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-27 09:25:20 +00:00
dependabot[bot]
8ddfe272bf build(deps): bump dompurify in /extensions/react-widget
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.1.5 to 3.2.4.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.1.5...3.2.4)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-27 09:20:40 +00:00
Siddhant Rai
b7a6bad7cd fix: minor bugs and route errors 2025-05-27 13:50:13 +05:30
Alex
e2f6c04406 (fix:AgentDetailsModal) update shared token URL format in AgentDetailsModal 2025-05-24 00:12:31 +01:00
Alex
c662725955 Merge pull request #1812 from ManishMadan2882/main
Tools redesign
2025-05-23 23:49:45 +01:00
ManishMadan2882
4b66ddfdef (fix:config) use default variant for confirm modal 2025-05-24 01:20:57 +05:30
ManishMadan2882
2d55b1f592 (fix:confirmModal) only close modal on click outisde 2025-05-24 01:19:14 +05:30
ManishMadan2882
14adfabf7e (feat:tools) reflect custom name on ui 2025-05-24 01:17:22 +05:30
Alex
e7a76ede76 Merge pull request #1813 from arc53/react-improve
React improve
2025-05-23 15:25:19 +01:00
Alex
de47df3bf9 fix: enhance ReActAgent's reasoning iterations and update planning prompt structure 2025-05-23 15:10:12 +01:00
Alex
5475e6f7c5 fix: enhance ReActAgent's response handling and update planning prompt 2025-05-23 14:21:02 +01:00
ManishMadan2882
8e3f3d74d4 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-05-23 18:22:20 +05:30
ManishMadan2882
046f6c66ed (feat:tools) warn on unsaved changes, deep compare 2025-05-23 18:22:06 +05:30
Manish Madan
79f9d6552e Merge branch 'arc53:main' into main 2025-05-23 03:37:53 +05:30
ManishMadan2882
56b4b63749 (fix/tools) custom actions only for api tool, allow delete 2025-05-23 03:36:27 +05:30
ManishMadan2882
b3246a48c7 (fix/addAction) duplicate placeholders 2025-05-23 03:11:54 +05:30
ManishMadan2882
71722ef6a3 (fix/config) correctly placed save btn 2025-05-23 00:57:42 +05:30
ManishMadan2882
ebf8f00302 (fix/input) minor details 2025-05-23 00:56:50 +05:30
Alex
7445928c7e (fix:stream) handle empty history input and improve user identification 2025-05-22 13:14:10 +01:00
ManishMadan2882
5ab7602f2f (feat:tools) ask for custom names 2025-05-22 17:14:52 +05:30
ManishMadan2882
a340aff63a (feat:tools) store custom names 2025-05-22 17:14:05 +05:30
ManishMadan2882
f82042ff00 (feat:config) custom name 2025-05-22 15:27:29 +05:30
ManishMadan2882
920422e28c Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-05-22 15:25:28 +05:30
ManishMadan2882
50d6b7a6f8 (fix/input) placeholder 2025-05-22 15:24:58 +05:30
Alex
41d624a36a Merge pull request #1810 from ManishMadan2882/main
(fix:layout) even action buttons
2025-05-21 18:59:01 +01:00
ManishMadan2882
f42c37c82e (fix:layout) even action buttons 2025-05-21 23:23:49 +05:30
Alex
119fcdf6f6 Merge pull request #1809 from ManishMadan2882/feat/sources-in-widget
(release-widget)0.5.1
2025-05-21 14:04:38 +01:00
ManishMadan2882
a5b093d1a9 (release-widget)0.5.1 2025-05-21 16:36:52 +05:30
Alex
e07cb44a3e Merge pull request #1806 from arc53/documentation-agents
Documentation agents
2025-05-21 11:44:05 +03:00
Alex
fec1bcfd5c Merge pull request #1789 from ManishMadan2882/main
Frontend Fixes
2025-05-21 01:05:47 +03:00
Alex
dbcf658343 Merge pull request #1808 from ManishMadan2882/feat/sources-in-widget
Glassmorphic effect on search widget
2025-05-21 01:00:49 +03:00
ManishMadan2882
d89e78c9ca (feat:search) minor adjust 2025-05-21 02:59:22 +05:30
ManishMadan2882
ec50650dfa (fix/searchwidget) prominent placeholder, sleek spinner 2025-05-21 02:00:47 +05:30
ManishMadan2882
7432e551f9 (fix/loader) drop on empty input 2025-05-21 01:40:03 +05:30
ManishMadan2882
4ee6bd44d1 (feat:glassy) search widget 2025-05-21 01:26:34 +05:30
Pavel
26f819098d agents and tools doc update 2025-05-20 20:32:38 +04:00
Alex
a1c79f93d7 Merge pull request #1801 from siiddhantt/feat/agents-enhance
feat: enhance agent sharing functionality and UI improvements
2025-05-20 18:40:22 +03:00
GH Action - Upstream Sync
9c1b202d74 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-05-20 01:42:17 +00:00
Pavel
8ad0f59f19 jwt update 2025-05-19 21:12:14 +04:00
GH Action - Upstream Sync
af40a77d24 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-05-19 01:45:51 +00:00
GH Action - Upstream Sync
d80b7017cf Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-05-17 01:38:42 +00:00
Siddhant Rai
56793c8db7 feat: enhance agent sharing functionality and UI improvements
- Added shared agents state management in Navigation and AgentsList components.
- Implemented fetching and displaying shared agents in the AgentsList.
- Introduced functionality to hide shared agents with appropriate API integration.
- Updated the SharedAgent component layout for better UI consistency.
- Improved error handling in conversation fetching logic.
- Added new API endpoint for hiding shared agents.
- Updated Redux slice to manage shared agents state.
- Refactored AgentCard and AgentSection components for better code organization and readability.
2025-05-17 05:53:56 +05:30
ManishMadan2882
8edb217943 (fix) source.link is new source.source 2025-05-17 01:41:29 +05:30
ManishMadan2882
23ebcf1065 (fix:all_sources) inlined svg, dom heirarchy 2025-05-17 01:33:37 +05:30
ManishMadan2882
68a5a3d62a (feat:source_cards) enhance ux 2025-05-17 00:15:42 +05:30
ManishMadan2882
8d7236b0db (fix:action-buttons) redirect to conversations 2025-05-17 00:07:54 +05:30
ManishMadan2882
96c7daf818 (fix:modals) use portals 2025-05-16 16:15:02 +05:30
ManishMadan2882
fc4942e189 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-05-16 14:33:55 +05:30
ManishMadan2882
ca69d025bd (fix:agents) adhere to new MessageInput 2025-05-16 14:33:30 +05:30
GH Action - Upstream Sync
ffa428e32a Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-05-16 01:41:33 +00:00
ManishMadan2882
c24e90eaae Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-05-16 04:12:27 +05:30
ManishMadan2882
ab32eff588 (fix/tool_calls) prevent overscroll somehow 2025-05-16 04:09:52 +05:30
ManishMadan2882
7f592f2b35 (fix/layout) prevent overlap in tab screen 2025-05-16 01:56:53 +05:30
GH Action - Upstream Sync
130ece7bc0 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-05-15 01:38:39 +00:00
Manish Madan
b2582796a2 Merge branch 'arc53:main' into main 2025-05-15 01:20:23 +05:30
GH Action - Upstream Sync
cd556d5d43 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-05-14 01:40:32 +00:00
ManishMadan2882
2855283a2c (fix/shared) append to right state 2025-05-14 04:15:11 +05:30
ManishMadan2882
06c29500f2 (fix:input-lag) localised the input state to messageInput 2025-05-14 04:13:53 +05:30
ManishMadan2882
81104153a6 (feat:action-buttons) combine the buttons at the top of layout 2025-05-14 03:01:57 +05:30
ManishMadan2882
e1e608b744 (fix:bubble) sleeker source cards 2025-05-13 06:16:16 +05:30
ManishMadan2882
ea9ab5b27c (feat:sources) remove None option, don't close on select 2025-05-12 17:15:03 +05:30
ManishMadan2882
357ced6cba Revert "(fix:message-input) no badge buttons when shared"
This reverts commit d873539856.
2025-05-12 16:32:17 +05:30
ManishMadan2882
3ffda69651 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-05-12 14:38:33 +05:30
ManishMadan2882
e1bf4e0762 (fix/logs)log must render once 2025-05-12 14:38:15 +05:30
GH Action - Upstream Sync
17e4fad6fb Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-05-10 01:36:32 +00:00
ManishMadan2882
8552e81022 (fix:input) sync with translations 2025-05-09 21:39:54 +05:30
ManishMadan2882
eacdde829f (fix/logs) abnormal scroll over page 2025-05-09 20:07:51 +05:30
ManishMadan2882
d873539856 (fix:message-input) no badge buttons when shared 2025-05-09 20:02:49 +05:30
112 changed files with 10372 additions and 5360 deletions

View File

@@ -2,16 +2,18 @@ import uuid
from abc import ABC, abstractmethod
from typing import Dict, Generator, List, Optional
from application.agents.llm_handler import get_llm_handler
from bson.objectid import ObjectId
from application.agents.tools.tool_action_parser import ToolActionParser
from application.agents.tools.tool_manager import ToolManager
from application.core.mongo_db import MongoDB
from application.core.settings import settings
from application.llm.handlers.handler_creator import LLMHandlerCreator
from application.llm.llm_creator import LLMCreator
from application.logging import build_stack_data, log_activity, LogContext
from application.retriever.base import BaseRetriever
from application.core.settings import settings
from bson.objectid import ObjectId
class BaseAgent(ABC):
@@ -45,7 +47,9 @@ class BaseAgent(ABC):
user_api_key=user_api_key,
decoded_token=decoded_token,
)
self.llm_handler = get_llm_handler(llm_name)
self.llm_handler = LLMHandlerCreator.create_handler(
llm_name if llm_name else "default"
)
self.attachments = attachments or []
@log_activity()
@@ -132,6 +136,15 @@ class BaseAgent(ABC):
parser = ToolActionParser(self.llm.__class__.__name__)
tool_id, action_name, call_args = parser.parse_args(call)
call_id = getattr(call, "id", None) or str(uuid.uuid4())
tool_call_data = {
"tool_name": tools_dict[tool_id]["name"],
"call_id": call_id,
"action_name": f"{action_name}_{tool_id}",
"arguments": call_args,
}
yield {"type": "tool_call", "data": {**tool_call_data, "status": "pending"}}
tool_data = tools_dict[tool_id]
action_data = (
tool_data["config"]["actions"][action_name]
@@ -184,19 +197,29 @@ class BaseAgent(ABC):
else:
print(f"Executing tool: {action_name} with args: {call_args}")
result = tool.execute_action(action_name, **parameters)
call_id = getattr(call, "id", None)
tool_call_data["result"] = (
f"{str(result)[:50]}..." if len(str(result)) > 50 else result
)
tool_call_data = {
"tool_name": tool_data["name"],
"call_id": call_id if call_id is not None else "None",
"action_name": f"{action_name}_{tool_id}",
"arguments": call_args,
"result": result,
}
yield {"type": "tool_call", "data": {**tool_call_data, "status": "completed"}}
self.tool_calls.append(tool_call_data)
return result, call_id
def _get_truncated_tool_calls(self):
return [
{
**tool_call,
"result": (
f"{str(tool_call['result'])[:50]}..."
if len(str(tool_call["result"])) > 50
else tool_call["result"]
),
"status": "completed",
}
for tool_call in self.tool_calls
]
def _build_messages(
self,
system_prompt: str,
@@ -252,9 +275,16 @@ class BaseAgent(ABC):
return retrieved_data
def _llm_gen(self, messages: List[Dict], log_context: Optional[LogContext] = None):
resp = self.llm.gen_stream(
model=self.gpt_model, messages=messages, tools=self.tools
)
gen_kwargs = {"model": self.gpt_model, "messages": messages}
if (
hasattr(self.llm, "_supports_tools")
and self.llm._supports_tools
and self.tools
):
gen_kwargs["tools"] = self.tools
resp = self.llm.gen_stream(**gen_kwargs)
if log_context:
data = build_stack_data(self.llm, exclude_attributes=["client"])
log_context.stacks.append({"component": "llm", "data": data})
@@ -268,10 +298,30 @@ class BaseAgent(ABC):
log_context: Optional[LogContext] = None,
attachments: Optional[List[Dict]] = None,
):
resp = self.llm_handler.handle_response(
self, resp, tools_dict, messages, attachments
resp = self.llm_handler.process_message_flow(
self, resp, tools_dict, messages, attachments, True
)
if log_context:
data = build_stack_data(self.llm_handler, exclude_attributes=["tool_calls"])
log_context.stacks.append({"component": "llm_handler", "data": data})
return resp
def _handle_response(self, response, tools_dict, messages, log_context):
if isinstance(response, str):
yield {"answer": response}
return
if hasattr(response, "message") and getattr(response.message, "content", None):
yield {"answer": response.message.content}
return
processed_response_gen = self._llm_handler(
response, tools_dict, messages, log_context, self.attachments
)
for event in processed_response_gen:
if isinstance(event, str):
yield {"answer": event}
elif hasattr(event, "message") and getattr(event.message, "content", None):
yield {"answer": event.message.content}
elif isinstance(event, dict) and "type" in event:
yield event

View File

@@ -1,8 +1,6 @@
from typing import Dict, Generator
from application.agents.base import BaseAgent
from application.logging import LogContext
from application.retriever.base import BaseRetriever
import logging
@@ -10,55 +8,46 @@ logger = logging.getLogger(__name__)
class ClassicAgent(BaseAgent):
"""A simplified classic agent with clear execution flow.
Usage:
1. Processes a query through retrieval
2. Sets up available tools
3. Generates responses using LLM
4. Handles tool interactions if needed
5. Returns standardized outputs
Easy to extend by overriding specific steps.
"""
def _gen_inner(
self, query: str, retriever: BaseRetriever, log_context: LogContext
) -> Generator[Dict, None, None]:
# Step 1: Retrieve relevant data
retrieved_data = self._retriever_search(retriever, query, log_context)
if self.user_api_key:
tools_dict = self._get_tools(self.user_api_key)
else:
tools_dict = self._get_user_tools(self.user)
# Step 2: Prepare tools
tools_dict = (
self._get_user_tools(self.user)
if not self.user_api_key
else self._get_tools(self.user_api_key)
)
self._prepare_tools(tools_dict)
# Step 3: Build and process messages
messages = self._build_messages(self.prompt, query, retrieved_data)
llm_response = self._llm_gen(messages, log_context)
resp = self._llm_gen(messages, log_context)
# Step 4: Handle the response
yield from self._handle_response(
llm_response, tools_dict, messages, log_context
)
attachments = self.attachments
if isinstance(resp, str):
yield {"answer": resp}
return
if (
hasattr(resp, "message")
and hasattr(resp.message, "content")
and resp.message.content is not None
):
yield {"answer": resp.message.content}
return
resp = self._llm_handler(resp, tools_dict, messages, log_context, attachments)
if isinstance(resp, str):
yield {"answer": resp}
elif (
hasattr(resp, "message")
and hasattr(resp.message, "content")
and resp.message.content is not None
):
yield {"answer": resp.message.content}
else:
for line in resp:
if isinstance(line, str):
yield {"answer": line}
# Step 5: Return metadata
yield {"sources": retrieved_data}
yield {"tool_calls": self._get_truncated_tool_calls()}
# Log tool calls for debugging
log_context.stacks.append(
{"component": "agent", "data": {"tool_calls": self.tool_calls.copy()}}
)
yield {"sources": retrieved_data}
# clean tool_call_data only send first 50 characters of tool_call['result']
for tool_call in self.tool_calls:
if len(str(tool_call["result"])) > 50:
tool_call["result"] = str(tool_call["result"])[:50] + "..."
yield {"tool_calls": self.tool_calls.copy()}

View File

@@ -1,351 +0,0 @@
import json
import logging
from abc import ABC, abstractmethod
from application.logging import build_stack_data
logger = logging.getLogger(__name__)
class LLMHandler(ABC):
def __init__(self):
self.llm_calls = []
self.tool_calls = []
@abstractmethod
def handle_response(self, agent, resp, tools_dict, messages, attachments=None, **kwargs):
pass
def prepare_messages_with_attachments(self, agent, messages, attachments=None):
"""
Prepare messages with attachment content if available.
Args:
agent: The current agent instance.
messages (list): List of message dictionaries.
attachments (list): List of attachment dictionaries with content.
Returns:
list: Messages with attachment context added to the system prompt.
"""
if not attachments:
return messages
logger.info(f"Preparing messages with {len(attachments)} attachments")
supported_types = agent.llm.get_supported_attachment_types()
supported_attachments = []
unsupported_attachments = []
for attachment in attachments:
mime_type = attachment.get('mime_type')
if mime_type in supported_types:
supported_attachments.append(attachment)
else:
unsupported_attachments.append(attachment)
# Process supported attachments with the LLM's custom method
prepared_messages = messages
if supported_attachments:
logger.info(f"Processing {len(supported_attachments)} supported attachments with {agent.llm.__class__.__name__}'s method")
prepared_messages = agent.llm.prepare_messages_with_attachments(messages, supported_attachments)
# Process unsupported attachments with the default method
if unsupported_attachments:
logger.info(f"Processing {len(unsupported_attachments)} unsupported attachments with default method")
prepared_messages = self._append_attachment_content_to_system(prepared_messages, unsupported_attachments)
return prepared_messages
def _append_attachment_content_to_system(self, messages, attachments):
"""
Default method to append attachment content to the system prompt.
Args:
messages (list): List of message dictionaries.
attachments (list): List of attachment dictionaries with content.
Returns:
list: Messages with attachment context added to the system prompt.
"""
prepared_messages = messages.copy()
attachment_texts = []
for attachment in attachments:
logger.info(f"Adding attachment {attachment.get('id')} to context")
if 'content' in attachment:
attachment_texts.append(f"Attached file content:\n\n{attachment['content']}")
if attachment_texts:
combined_attachment_text = "\n\n".join(attachment_texts)
system_found = False
for i in range(len(prepared_messages)):
if prepared_messages[i].get("role") == "system":
prepared_messages[i]["content"] += f"\n\n{combined_attachment_text}"
system_found = True
break
if not system_found:
prepared_messages.insert(0, {"role": "system", "content": combined_attachment_text})
return prepared_messages
class OpenAILLMHandler(LLMHandler):
def handle_response(self, agent, resp, tools_dict, messages, attachments=None, stream: bool = True):
messages = self.prepare_messages_with_attachments(agent, messages, attachments)
logger.info(f"Messages with attachments: {messages}")
if not stream:
while hasattr(resp, "finish_reason") and resp.finish_reason == "tool_calls":
message = json.loads(resp.model_dump_json())["message"]
keys_to_remove = {"audio", "function_call", "refusal"}
filtered_data = {
k: v for k, v in message.items() if k not in keys_to_remove
}
messages.append(filtered_data)
tool_calls = resp.message.tool_calls
for call in tool_calls:
try:
self.tool_calls.append(call)
tool_response, call_id = agent._execute_tool_action(
tools_dict, call
)
function_call_dict = {
"function_call": {
"name": call.function.name,
"args": call.function.arguments,
"call_id": call_id,
}
}
function_response_dict = {
"function_response": {
"name": call.function.name,
"response": {"result": tool_response},
"call_id": call_id,
}
}
messages.append(
{"role": "assistant", "content": [function_call_dict]}
)
messages.append(
{"role": "tool", "content": [function_response_dict]}
)
messages = self.prepare_messages_with_attachments(agent, messages, attachments)
except Exception as e:
logging.error(f"Error executing tool: {str(e)}", exc_info=True)
messages.append(
{
"role": "tool",
"content": f"Error executing tool: {str(e)}",
"tool_call_id": call_id,
}
)
resp = agent.llm.gen_stream(
model=agent.gpt_model, messages=messages, tools=agent.tools
)
self.llm_calls.append(build_stack_data(agent.llm))
return resp
else:
text_buffer = ""
while True:
tool_calls = {}
for chunk in resp:
if isinstance(chunk, str) and len(chunk) > 0:
yield chunk
continue
elif hasattr(chunk, "delta"):
chunk_delta = chunk.delta
if (
hasattr(chunk_delta, "tool_calls")
and chunk_delta.tool_calls is not None
):
for tool_call in chunk_delta.tool_calls:
index = tool_call.index
if index not in tool_calls:
tool_calls[index] = {
"id": "",
"function": {"name": "", "arguments": ""},
}
current = tool_calls[index]
if tool_call.id:
current["id"] = tool_call.id
if tool_call.function.name:
current["function"][
"name"
] = tool_call.function.name
if tool_call.function.arguments:
current["function"][
"arguments"
] += tool_call.function.arguments
tool_calls[index] = current
if (
hasattr(chunk, "finish_reason")
and chunk.finish_reason == "tool_calls"
):
for index in sorted(tool_calls.keys()):
call = tool_calls[index]
try:
self.tool_calls.append(call)
tool_response, call_id = agent._execute_tool_action(
tools_dict, call
)
if isinstance(call["function"]["arguments"], str):
call["function"]["arguments"] = json.loads(call["function"]["arguments"])
function_call_dict = {
"function_call": {
"name": call["function"]["name"],
"args": call["function"]["arguments"],
"call_id": call["id"],
}
}
function_response_dict = {
"function_response": {
"name": call["function"]["name"],
"response": {"result": tool_response},
"call_id": call["id"],
}
}
messages.append(
{
"role": "assistant",
"content": [function_call_dict],
}
)
messages.append(
{
"role": "tool",
"content": [function_response_dict],
}
)
except Exception as e:
logging.error(f"Error executing tool: {str(e)}", exc_info=True)
messages.append(
{
"role": "assistant",
"content": f"Error executing tool: {str(e)}",
}
)
tool_calls = {}
if hasattr(chunk_delta, "content") and chunk_delta.content:
# Add to buffer or yield immediately based on your preference
text_buffer += chunk_delta.content
yield text_buffer
text_buffer = ""
if (
hasattr(chunk, "finish_reason")
and chunk.finish_reason == "stop"
):
return resp
elif isinstance(chunk, str) and len(chunk) == 0:
continue
logger.info(f"Regenerating with messages: {messages}")
resp = agent.llm.gen_stream(
model=agent.gpt_model, messages=messages, tools=agent.tools
)
self.llm_calls.append(build_stack_data(agent.llm))
class GoogleLLMHandler(LLMHandler):
def handle_response(self, agent, resp, tools_dict, messages, attachments=None, stream: bool = True):
from google.genai import types
messages = self.prepare_messages_with_attachments(agent, messages, attachments)
while True:
if not stream:
response = agent.llm.gen(
model=agent.gpt_model, messages=messages, tools=agent.tools
)
self.llm_calls.append(build_stack_data(agent.llm))
if response.candidates and response.candidates[0].content.parts:
tool_call_found = False
for part in response.candidates[0].content.parts:
if part.function_call:
tool_call_found = True
self.tool_calls.append(part.function_call)
tool_response, call_id = agent._execute_tool_action(
tools_dict, part.function_call
)
function_response_part = types.Part.from_function_response(
name=part.function_call.name,
response={"result": tool_response},
)
messages.append(
{"role": "model", "content": [part.to_json_dict()]}
)
messages.append(
{
"role": "tool",
"content": [function_response_part.to_json_dict()],
}
)
if (
not tool_call_found
and response.candidates[0].content.parts
and response.candidates[0].content.parts[0].text
):
return response.candidates[0].content.parts[0].text
elif not tool_call_found:
return response.candidates[0].content.parts
else:
return response
else:
response = agent.llm.gen_stream(
model=agent.gpt_model, messages=messages, tools=agent.tools
)
self.llm_calls.append(build_stack_data(agent.llm))
tool_call_found = False
for result in response:
if hasattr(result, "function_call"):
tool_call_found = True
self.tool_calls.append(result.function_call)
tool_response, call_id = agent._execute_tool_action(
tools_dict, result.function_call
)
function_response_part = types.Part.from_function_response(
name=result.function_call.name,
response={"result": tool_response},
)
messages.append(
{"role": "model", "content": [result.to_json_dict()]}
)
messages.append(
{
"role": "tool",
"content": [function_response_part.to_json_dict()],
}
)
else:
tool_call_found = False
yield result
if not tool_call_found:
return response
def get_llm_handler(llm_type):
handlers = {
"openai": OpenAILLMHandler(),
"google": GoogleLLMHandler(),
}
return handlers.get(llm_type, OpenAILLMHandler())

View File

@@ -1,33 +1,95 @@
import os
from typing import Dict, Generator, List
from typing import Dict, Generator, List, Any
import logging
from application.agents.base import BaseAgent
from application.logging import build_stack_data, LogContext
from application.retriever.base import BaseRetriever
logger = logging.getLogger(__name__)
current_dir = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
with open(
os.path.join(current_dir, "application/prompts", "react_planning_prompt.txt"), "r"
) as f:
planning_prompt = f.read()
planning_prompt_template = f.read()
with open(
os.path.join(current_dir, "application/prompts", "react_final_prompt.txt"),
"r",
) as f:
final_prompt = f.read()
final_prompt_template = f.read()
MAX_ITERATIONS_REASONING = 10
class ReActAgent(BaseAgent):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.plan = ""
self.plan: str = ""
self.observations: List[str] = []
def _extract_content_from_llm_response(self, resp: Any) -> str:
"""
Helper to extract string content from various LLM response types.
Handles strings, message objects (OpenAI-like), and streams.
Adapt stream handling for your specific LLM client if not OpenAI.
"""
collected_content = []
if isinstance(resp, str):
collected_content.append(resp)
elif ( # OpenAI non-streaming or Anthropic non-streaming (older SDK style)
hasattr(resp, "message")
and hasattr(resp.message, "content")
and resp.message.content is not None
):
collected_content.append(resp.message.content)
elif ( # OpenAI non-streaming (Pydantic model), Anthropic new SDK non-streaming
hasattr(resp, "choices") and resp.choices and
hasattr(resp.choices[0], "message") and
hasattr(resp.choices[0].message, "content") and
resp.choices[0].message.content is not None
):
collected_content.append(resp.choices[0].message.content) # OpenAI
elif ( # Anthropic new SDK non-streaming content block
hasattr(resp, "content") and isinstance(resp.content, list) and resp.content and
hasattr(resp.content[0], "text")
):
collected_content.append(resp.content[0].text) # Anthropic
else:
# Assume resp is a stream if not a recognized object
try:
for chunk in resp: # This will fail if resp is not iterable (e.g. a non-streaming response object)
content_piece = ""
# OpenAI-like stream
if hasattr(chunk, 'choices') and len(chunk.choices) > 0 and \
hasattr(chunk.choices[0], 'delta') and \
hasattr(chunk.choices[0].delta, 'content') and \
chunk.choices[0].delta.content is not None:
content_piece = chunk.choices[0].delta.content
# Anthropic-like stream (ContentBlockDelta)
elif hasattr(chunk, 'type') and chunk.type == 'content_block_delta' and \
hasattr(chunk, 'delta') and hasattr(chunk.delta, 'text'):
content_piece = chunk.delta.text
elif isinstance(chunk, str): # Simplest case: stream of strings
content_piece = chunk
if content_piece:
collected_content.append(content_piece)
except TypeError: # If resp is not iterable (e.g. a final response object that wasn't caught above)
logger.debug(f"Response type {type(resp)} could not be iterated as a stream. It might be a non-streaming object not handled by specific checks.")
except Exception as e:
logger.error(f"Error processing potential stream chunk: {e}, chunk was: {getattr(chunk, '__dict__', chunk)}")
return "".join(collected_content)
def _gen_inner(
self, query: str, retriever: BaseRetriever, log_context: LogContext
) -> Generator[Dict, None, None]:
# Reset state for this generation call
self.plan = ""
self.observations = []
retrieved_data = self._retriever_search(retriever, query, log_context)
if self.user_api_key:
@@ -37,96 +99,131 @@ class ReActAgent(BaseAgent):
self._prepare_tools(tools_dict)
docs_together = "\n".join([doc["text"] for doc in retrieved_data])
plan = self._create_plan(query, docs_together, log_context)
for line in plan:
if isinstance(line, str):
self.plan += line
yield {"thought": line}
iterating_reasoning = 0
while iterating_reasoning < MAX_ITERATIONS_REASONING:
iterating_reasoning += 1
# 1. Create Plan
logger.info("ReActAgent: Creating plan...")
plan_stream = self._create_plan(query, docs_together, log_context)
current_plan_parts = []
yield {"thought": f"Reasoning... (iteration {iterating_reasoning})\n\n"}
for line_chunk in plan_stream:
current_plan_parts.append(line_chunk)
yield {"thought": line_chunk}
self.plan = "".join(current_plan_parts)
if self.plan:
self.observations.append(f"Plan: {self.plan} Iteration: {iterating_reasoning}")
prompt = self.prompt + f"\nFollow this plan: {self.plan}"
messages = self._build_messages(prompt, query, retrieved_data)
resp = self._llm_gen(messages, log_context)
if isinstance(resp, str):
self.observations.append(resp)
if (
hasattr(resp, "message")
and hasattr(resp.message, "content")
and resp.message.content is not None
):
self.observations.append(resp.message.content)
resp = self._llm_handler(resp, tools_dict, messages, log_context)
for tool_call in self.tool_calls:
observation = (
f"Action '{tool_call['action_name']}' of tool '{tool_call['tool_name']}' "
f"with arguments '{tool_call['arguments']}' returned: '{tool_call['result']}'"
max_obs_len = 20000
obs_str = "\n".join(self.observations)
if len(obs_str) > max_obs_len:
obs_str = obs_str[:max_obs_len] + "\n...[observations truncated]"
execution_prompt_str = (
(self.prompt or "")
+ f"\n\nFollow this plan:\n{self.plan}"
+ f"\n\nObservations:\n{obs_str}"
+ f"\n\nIf there is enough data to complete user query '{query}', Respond with 'SATISFIED' only. Otherwise, continue. Dont Menstion 'SATISFIED' in your response if you are not ready. "
)
self.observations.append(observation)
messages = self._build_messages(execution_prompt_str, query, retrieved_data)
if isinstance(resp, str):
self.observations.append(resp)
elif (
hasattr(resp, "message")
and hasattr(resp.message, "content")
and resp.message.content is not None
):
self.observations.append(resp.message.content)
else:
completion = self.llm.gen_stream(
model=self.gpt_model, messages=messages, tools=self.tools
)
for line in completion:
if isinstance(line, str):
self.observations.append(line)
resp_from_llm_gen = self._llm_gen(messages, log_context)
log_context.stacks.append(
{"component": "agent", "data": {"tool_calls": self.tool_calls.copy()}}
)
initial_llm_thought_content = self._extract_content_from_llm_response(resp_from_llm_gen)
if initial_llm_thought_content:
self.observations.append(f"Initial thought/response: {initial_llm_thought_content}")
else:
logger.info("ReActAgent: Initial LLM response (before handler) had no textual content (might be only tool calls).")
resp_after_handler = self._llm_handler(resp_from_llm_gen, tools_dict, messages, log_context)
for tool_call_info in self.tool_calls: # Iterate over self.tool_calls populated by _llm_handler
observation_string = (
f"Executed Action: Tool '{tool_call_info.get('tool_name', 'N/A')}' "
f"with arguments '{tool_call_info.get('arguments', '{}')}'. Result: '{str(tool_call_info.get('result', ''))[:200]}...'"
)
self.observations.append(observation_string)
yield {"sources": retrieved_data}
# clean tool_call_data only send first 50 characters of tool_call['result']
for tool_call in self.tool_calls:
if len(str(tool_call["result"])) > 50:
tool_call["result"] = str(tool_call["result"])[:50] + "..."
yield {"tool_calls": self.tool_calls.copy()}
content_after_handler = self._extract_content_from_llm_response(resp_after_handler)
if content_after_handler:
self.observations.append(f"Response after tool execution: {content_after_handler}")
else:
logger.info("ReActAgent: LLM response after handler had no textual content.")
final_answer = self._create_final_answer(query, self.observations, log_context)
for line in final_answer:
if isinstance(line, str):
yield {"answer": line}
if log_context:
log_context.stacks.append(
{"component": "agent_tool_calls", "data": {"tool_calls": self.tool_calls.copy()}}
)
yield {"sources": retrieved_data}
display_tool_calls = []
for tc in self.tool_calls:
cleaned_tc = tc.copy()
if len(str(cleaned_tc.get("result", ""))) > 50:
cleaned_tc["result"] = str(cleaned_tc["result"])[:50] + "..."
display_tool_calls.append(cleaned_tc)
if display_tool_calls:
yield {"tool_calls": display_tool_calls}
if "SATISFIED" in content_after_handler:
logger.info("ReActAgent: LLM satisfied with the plan and data. Stopping reasoning.")
break
# 3. Create Final Answer based on all observations
final_answer_stream = self._create_final_answer(query, self.observations, log_context)
for answer_chunk in final_answer_stream:
yield {"answer": answer_chunk}
logger.info("ReActAgent: Finished generating final answer.")
def _create_plan(
self, query: str, docs_data: str, log_context: LogContext = None
) -> Generator[str, None, None]:
plan_prompt = planning_prompt.replace("{query}", query)
if "{summaries}" in planning_prompt:
summaries = docs_data
plan_prompt = plan_prompt.replace("{summaries}", summaries)
plan_prompt_filled = planning_prompt_template.replace("{query}", query)
if "{summaries}" in plan_prompt_filled:
summaries = docs_data if docs_data else "No documents retrieved."
plan_prompt_filled = plan_prompt_filled.replace("{summaries}", summaries)
plan_prompt_filled = plan_prompt_filled.replace("{prompt}", self.prompt or "")
plan_prompt_filled = plan_prompt_filled.replace("{observations}", "\n".join(self.observations))
messages = [{"role": "user", "content": plan_prompt}]
print(self.tools)
plan = self.llm.gen_stream(
model=self.gpt_model, messages=messages, tools=self.tools
messages = [{"role": "user", "content": plan_prompt_filled}]
plan_stream_from_llm = self.llm.gen_stream(
model=self.gpt_model, messages=messages, tools=getattr(self, 'tools', None) # Use self.tools
)
if log_context:
data = build_stack_data(self.llm)
log_context.stacks.append({"component": "planning_llm", "data": data})
return plan
for chunk in plan_stream_from_llm:
content_piece = self._extract_content_from_llm_response(chunk)
if content_piece:
yield content_piece
def _create_final_answer(
self, query: str, observations: List[str], log_context: LogContext = None
) -> str:
) -> Generator[str, None, None]:
observation_string = "\n".join(observations)
final_answer_prompt = final_prompt.format(
max_obs_len = 10000
if len(observation_string) > max_obs_len:
observation_string = observation_string[:max_obs_len] + "\n...[observations truncated]"
logger.warning("ReActAgent: Truncated observations for final answer prompt due to length.")
final_answer_prompt_filled = final_prompt_template.format(
query=query, observations=observation_string
)
messages = [{"role": "user", "content": final_answer_prompt}]
final_answer = self.llm.gen_stream(model=self.gpt_model, messages=messages)
messages = [{"role": "user", "content": final_answer_prompt_filled}]
# Final answer should synthesize, not call tools.
final_answer_stream_from_llm = self.llm.gen_stream(
model=self.gpt_model, messages=messages, tools=None
)
if log_context:
data = build_stack_data(self.llm)
log_context.stacks.append({"component": "final_answer_llm", "data": data})
return final_answer
for chunk in final_answer_stream_from_llm:
content_piece = self._extract_content_from_llm_response(chunk)
if content_piece:
yield content_piece

View File

@@ -17,26 +17,21 @@ class ToolActionParser:
return parser(call)
def _parse_openai_llm(self, call):
if isinstance(call, dict):
try:
call_args = json.loads(call["function"]["arguments"])
tool_id = call["function"]["name"].split("_")[-1]
action_name = call["function"]["name"].rsplit("_", 1)[0]
except (KeyError, TypeError) as e:
logger.error(f"Error parsing OpenAI LLM call: {e}")
return None, None, None
else:
try:
call_args = json.loads(call.function.arguments)
tool_id = call.function.name.split("_")[-1]
action_name = call.function.name.rsplit("_", 1)[0]
except (AttributeError, TypeError) as e:
logger.error(f"Error parsing OpenAI LLM call: {e}")
return None, None, None
try:
call_args = json.loads(call.arguments)
tool_id = call.name.split("_")[-1]
action_name = call.name.rsplit("_", 1)[0]
except (AttributeError, TypeError) as e:
logger.error(f"Error parsing OpenAI LLM call: {e}")
return None, None, None
return tool_id, action_name, call_args
def _parse_google_llm(self, call):
call_args = call.args
tool_id = call.name.split("_")[-1]
action_name = call.name.rsplit("_", 1)[0]
try:
call_args = call.arguments
tool_id = call.name.split("_")[-1]
action_name = call.name.rsplit("_", 1)[0]
except (AttributeError, TypeError) as e:
logger.error(f"Error parsing Google LLM call: {e}")
return None, None, None
return tool_id, action_name, call_args

View File

@@ -37,17 +37,17 @@ api.add_namespace(answer_ns)
gpt_model = ""
# to have some kind of default behaviour
if settings.LLM_NAME == "openai":
if settings.LLM_PROVIDER == "openai":
gpt_model = "gpt-4o-mini"
elif settings.LLM_NAME == "anthropic":
elif settings.LLM_PROVIDER == "anthropic":
gpt_model = "claude-2"
elif settings.LLM_NAME == "groq":
elif settings.LLM_PROVIDER == "groq":
gpt_model = "llama3-8b-8192"
elif settings.LLM_NAME == "novita":
elif settings.LLM_PROVIDER == "novita":
gpt_model = "deepseek/deepseek-r1"
if settings.MODEL_NAME: # in case there is particular model name configured
gpt_model = settings.MODEL_NAME
if settings.LLM_NAME: # in case there is particular model name configured
gpt_model = settings.LLM_NAME
# load the prompts
current_dir = os.path.dirname(
@@ -164,6 +164,7 @@ def save_conversation(
agent_id=None,
is_shared_usage=False,
shared_token=None,
attachment_ids=None,
):
current_time = datetime.datetime.now(datetime.timezone.utc)
if conversation_id is not None and index is not None:
@@ -177,6 +178,7 @@ def save_conversation(
f"queries.{index}.sources": source_log_docs,
f"queries.{index}.tool_calls": tool_calls,
f"queries.{index}.timestamp": current_time,
f"queries.{index}.attachments": attachment_ids,
}
},
)
@@ -197,6 +199,7 @@ def save_conversation(
"sources": source_log_docs,
"tool_calls": tool_calls,
"timestamp": current_time,
"attachments": attachment_ids,
}
}
},
@@ -233,6 +236,7 @@ def save_conversation(
"sources": source_log_docs,
"tool_calls": tool_calls,
"timestamp": current_time,
"attachments": attachment_ids,
}
],
}
@@ -273,20 +277,13 @@ def complete_stream(
isNoneDoc=False,
index=None,
should_save_conversation=True,
attachments=None,
attachment_ids=None,
agent_id=None,
is_shared_usage=False,
shared_token=None,
):
try:
response_full, thought, source_log_docs, tool_calls = "", "", [], []
attachment_ids = []
if attachments:
attachment_ids = [attachment["id"] for attachment in attachments]
logger.info(
f"Processing request with {len(attachments)} attachments: {attachment_ids}"
)
answer = agent.gen(query=question, retriever=retriever)
@@ -310,19 +307,20 @@ def complete_stream(
yield f"data: {data}\n\n"
elif "tool_calls" in line:
tool_calls = line["tool_calls"]
data = json.dumps({"type": "tool_calls", "tool_calls": tool_calls})
yield f"data: {data}\n\n"
elif "thought" in line:
thought += line["thought"]
data = json.dumps({"type": "thought", "thought": line["thought"]})
yield f"data: {data}\n\n"
elif "type" in line:
data = json.dumps(line)
yield f"data: {data}\n\n"
if isNoneDoc:
for doc in source_log_docs:
doc["source"] = "None"
llm = LLMCreator.create_llm(
settings.LLM_NAME,
settings.LLM_PROVIDER,
api_key=settings.API_KEY,
user_api_key=user_api_key,
decoded_token=decoded_token,
@@ -340,6 +338,7 @@ def complete_stream(
decoded_token,
index,
api_key=user_api_key,
attachment_ids=attachment_ids,
agent_id=agent_id,
is_shared_usage=is_shared_usage,
shared_token=shared_token,
@@ -439,7 +438,7 @@ class Stream(Resource):
try:
question = data["question"]
history = limit_chat_history(
json.loads(data.get("history", [])), gpt_model=gpt_model
json.loads(data.get("history", "[]")), gpt_model=gpt_model
)
conversation_id = data.get("conversation_id")
prompt_id = data.get("prompt_id", "default")
@@ -451,9 +450,9 @@ class Stream(Resource):
retriever_name = data.get("retriever", "classic")
agent_id = data.get("agent_id", None)
agent_type = settings.AGENT_NAME
agent_key, is_shared_usage, shared_token = get_agent_key(
agent_id, request.decoded_token.get("sub")
)
decoded_token = getattr(request, "decoded_token", None)
user_sub = decoded_token.get("sub") if decoded_token else None
agent_key, is_shared_usage, shared_token = get_agent_key(agent_id, user_sub)
if agent_key:
data.update({"api_key": agent_key})
@@ -504,7 +503,7 @@ class Stream(Resource):
agent = AgentCreator.create_agent(
agent_type,
endpoint="stream",
llm_name=settings.LLM_NAME,
llm_name=settings.LLM_PROVIDER,
gpt_model=gpt_model,
api_key=settings.API_KEY,
user_api_key=user_api_key,
@@ -525,8 +524,7 @@ class Stream(Resource):
user_api_key=user_api_key,
decoded_token=decoded_token,
)
is_shared_usage_val = data.get("is_shared_usage", False)
is_shared_token_val = data.get("shared_token", None)
return Response(
complete_stream(
question=question,
@@ -538,9 +536,10 @@ class Stream(Resource):
isNoneDoc=data.get("isNoneDoc"),
index=index,
should_save_conversation=save_conv,
attachment_ids=attachment_ids,
agent_id=agent_id,
is_shared_usage=is_shared_usage_val,
shared_token=is_shared_token_val,
is_shared_usage=is_shared_usage,
shared_token=shared_token,
),
mimetype="text/event-stream",
)
@@ -658,7 +657,7 @@ class Answer(Resource):
agent = AgentCreator.create_agent(
agent_type,
endpoint="api/answer",
llm_name=settings.LLM_NAME,
llm_name=settings.LLM_PROVIDER,
gpt_model=gpt_model,
api_key=settings.API_KEY,
user_api_key=user_api_key,
@@ -727,7 +726,7 @@ class Answer(Resource):
doc["source"] = "None"
llm = LLMCreator.create_llm(
settings.LLM_NAME,
settings.LLM_PROVIDER,
api_key=settings.API_KEY,
user_api_key=user_api_key,
decoded_token=decoded_token,

File diff suppressed because it is too large Load Diff

View File

@@ -11,18 +11,18 @@ current_dir = os.path.dirname(
class Settings(BaseSettings):
AUTH_TYPE: Optional[str] = None
LLM_NAME: str = "docsgpt"
MODEL_NAME: Optional[str] = (
None # if LLM_NAME is openai, MODEL_NAME can be gpt-4 or gpt-3.5-turbo
LLM_PROVIDER: str = "docsgpt"
LLM_NAME: Optional[str] = (
None # if LLM_PROVIDER is openai, LLM_NAME can be gpt-4 or gpt-3.5-turbo
)
EMBEDDINGS_NAME: str = "huggingface_sentence-transformers/all-mpnet-base-v2"
CELERY_BROKER_URL: str = "redis://localhost:6379/0"
CELERY_RESULT_BACKEND: str = "redis://localhost:6379/1"
MONGO_URI: str = "mongodb://localhost:27017/docsgpt"
MONGO_DB_NAME: str = "docsgpt"
MODEL_PATH: str = os.path.join(current_dir, "models/docsgpt-7b-f16.gguf")
LLM_PATH: str = os.path.join(current_dir, "models/docsgpt-7b-f16.gguf")
DEFAULT_MAX_HISTORY: int = 150
MODEL_TOKEN_LIMITS: dict = {
LLM_TOKEN_LIMITS: dict = {
"gpt-4o-mini": 128000,
"gpt-3.5-turbo": 4096,
"claude-2": 1e5,
@@ -35,6 +35,9 @@ class Settings(BaseSettings):
)
RETRIEVERS_ENABLED: list = ["classic_rag", "duckduck_search"] # also brave_search
AGENT_NAME: str = "classic"
FALLBACK_LLM_PROVIDER: Optional[str] = None # provider for fallback llm
FALLBACK_LLM_NAME: Optional[str] = None # model name for fallback llm
FALLBACK_LLM_API_KEY: Optional[str] = None # api key for fallback llm
# LLM Cache
CACHE_REDIS_URL: str = "redis://localhost:6379/2"
@@ -99,8 +102,7 @@ class Settings(BaseSettings):
BRAVE_SEARCH_API_KEY: Optional[str] = None
FLASK_DEBUG_MODE: bool = False
STORAGE_TYPE: str = "local" # local or s3
STORAGE_TYPE: str = "local" # local or s3
JWT_SECRET_KEY: str = ""

View File

@@ -1,53 +1,117 @@
import logging
from abc import ABC, abstractmethod
from application.cache import gen_cache, stream_cache
from application.core.settings import settings
from application.usage import gen_token_usage, stream_token_usage
logger = logging.getLogger(__name__)
class BaseLLM(ABC):
def __init__(self, decoded_token=None):
def __init__(
self,
decoded_token=None,
):
self.decoded_token = decoded_token
self.token_usage = {"prompt_tokens": 0, "generated_tokens": 0}
self.fallback_provider = settings.FALLBACK_LLM_PROVIDER
self.fallback_model_name = settings.FALLBACK_LLM_NAME
self.fallback_llm_api_key = settings.FALLBACK_LLM_API_KEY
self._fallback_llm = None
def _apply_decorator(self, method, decorators, *args, **kwargs):
for decorator in decorators:
method = decorator(method)
return method(self, *args, **kwargs)
@property
def fallback_llm(self):
"""Lazy-loaded fallback LLM instance."""
if (
self._fallback_llm is None
and self.fallback_provider
and self.fallback_model_name
):
try:
from application.llm.llm_creator import LLMCreator
self._fallback_llm = LLMCreator.create_llm(
self.fallback_provider,
self.fallback_llm_api_key,
None,
self.decoded_token,
)
except Exception as e:
logger.error(
f"Failed to initialize fallback LLM: {str(e)}", exc_info=True
)
return self._fallback_llm
def _execute_with_fallback(
self, method_name: str, decorators: list, *args, **kwargs
):
"""
Unified method execution with fallback support.
Args:
method_name: Name of the raw method ('_raw_gen' or '_raw_gen_stream')
decorators: List of decorators to apply
*args: Positional arguments
**kwargs: Keyword arguments
"""
def decorated_method():
method = getattr(self, method_name)
for decorator in decorators:
method = decorator(method)
return method(self, *args, **kwargs)
try:
return decorated_method()
except Exception as e:
if not self.fallback_llm:
logger.error(f"Primary LLM failed and no fallback available: {str(e)}")
raise
logger.warning(
f"Falling back to {self.fallback_provider}/{self.fallback_model_name}. Error: {str(e)}"
)
fallback_method = getattr(
self.fallback_llm, method_name.replace("_raw_", "")
)
return fallback_method(*args, **kwargs)
def gen(self, model, messages, stream=False, tools=None, *args, **kwargs):
decorators = [gen_token_usage, gen_cache]
return self._execute_with_fallback(
"_raw_gen",
decorators,
model=model,
messages=messages,
stream=stream,
tools=tools,
*args,
**kwargs,
)
def gen_stream(self, model, messages, stream=True, tools=None, *args, **kwargs):
decorators = [stream_cache, stream_token_usage]
return self._execute_with_fallback(
"_raw_gen_stream",
decorators,
model=model,
messages=messages,
stream=stream,
tools=tools,
*args,
**kwargs,
)
@abstractmethod
def _raw_gen(self, model, messages, stream, tools, *args, **kwargs):
pass
def gen(self, model, messages, stream=False, tools=None, *args, **kwargs):
decorators = [gen_token_usage, gen_cache]
return self._apply_decorator(
self._raw_gen,
decorators=decorators,
model=model,
messages=messages,
stream=stream,
tools=tools,
*args,
**kwargs
)
@abstractmethod
def _raw_gen_stream(self, model, messages, stream, *args, **kwargs):
pass
def gen_stream(self, model, messages, stream=True, tools=None, *args, **kwargs):
decorators = [stream_cache, stream_token_usage]
return self._apply_decorator(
self._raw_gen_stream,
decorators=decorators,
model=model,
messages=messages,
stream=stream,
tools=tools,
*args,
**kwargs
)
def supports_tools(self):
return hasattr(self, "_supports_tools") and callable(
getattr(self, "_supports_tools")
@@ -55,11 +119,11 @@ class BaseLLM(ABC):
def _supports_tools(self):
raise NotImplementedError("Subclass must implement _supports_tools method")
def get_supported_attachment_types(self):
"""
Return a list of MIME types supported by this LLM for file uploads.
Returns:
list: List of supported MIME types
"""

View File

View File

@@ -0,0 +1,335 @@
import logging
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any, Dict, Generator, List, Optional, Union
from application.logging import build_stack_data
logger = logging.getLogger(__name__)
@dataclass
class ToolCall:
"""Represents a tool/function call from the LLM."""
id: str
name: str
arguments: Union[str, Dict]
index: Optional[int] = None
@classmethod
def from_dict(cls, data: Dict) -> "ToolCall":
"""Create ToolCall from dictionary."""
return cls(
id=data.get("id", ""),
name=data.get("name", ""),
arguments=data.get("arguments", {}),
index=data.get("index"),
)
@dataclass
class LLMResponse:
"""Represents a response from the LLM."""
content: str
tool_calls: List[ToolCall]
finish_reason: str
raw_response: Any
@property
def requires_tool_call(self) -> bool:
"""Check if the response requires tool calls."""
return bool(self.tool_calls) and self.finish_reason == "tool_calls"
class LLMHandler(ABC):
"""Abstract base class for LLM handlers."""
def __init__(self):
self.llm_calls = []
self.tool_calls = []
@abstractmethod
def parse_response(self, response: Any) -> LLMResponse:
"""Parse raw LLM response into standardized format."""
pass
@abstractmethod
def create_tool_message(self, tool_call: ToolCall, result: Any) -> Dict:
"""Create a tool result message for the conversation history."""
pass
@abstractmethod
def _iterate_stream(self, response: Any) -> Generator:
"""Iterate through streaming response chunks."""
pass
def process_message_flow(
self,
agent,
initial_response,
tools_dict: Dict,
messages: List[Dict],
attachments: Optional[List] = None,
stream: bool = False,
) -> Union[str, Generator]:
"""
Main orchestration method for processing LLM message flow.
Args:
agent: The agent instance
initial_response: Initial LLM response
tools_dict: Dictionary of available tools
messages: Conversation history
attachments: Optional attachments
stream: Whether to use streaming
Returns:
Final response or generator for streaming
"""
messages = self.prepare_messages(agent, messages, attachments)
if stream:
return self.handle_streaming(agent, initial_response, tools_dict, messages)
else:
return self.handle_non_streaming(
agent, initial_response, tools_dict, messages
)
def prepare_messages(
self, agent, messages: List[Dict], attachments: Optional[List] = None
) -> List[Dict]:
"""
Prepare messages with attachments and provider-specific formatting.
Args:
agent: The agent instance
messages: Original messages
attachments: List of attachments
Returns:
Prepared messages list
"""
if not attachments:
return messages
logger.info(f"Preparing messages with {len(attachments)} attachments")
supported_types = agent.llm.get_supported_attachment_types()
supported_attachments = [
a for a in attachments if a.get("mime_type") in supported_types
]
unsupported_attachments = [
a for a in attachments if a.get("mime_type") not in supported_types
]
# Process supported attachments with the LLM's custom method
if supported_attachments:
logger.info(
f"Processing {len(supported_attachments)} supported attachments"
)
messages = agent.llm.prepare_messages_with_attachments(
messages, supported_attachments
)
# Process unsupported attachments with default method
if unsupported_attachments:
logger.info(
f"Processing {len(unsupported_attachments)} unsupported attachments"
)
messages = self._append_unsupported_attachments(
messages, unsupported_attachments
)
return messages
def _append_unsupported_attachments(
self, messages: List[Dict], attachments: List[Dict]
) -> List[Dict]:
"""
Default method to append unsupported attachment content to system prompt.
Args:
messages: Current messages
attachments: List of unsupported attachments
Returns:
Updated messages list
"""
prepared_messages = messages.copy()
attachment_texts = []
for attachment in attachments:
logger.info(f"Adding attachment {attachment.get('id')} to context")
if "content" in attachment:
attachment_texts.append(
f"Attached file content:\n\n{attachment['content']}"
)
if attachment_texts:
combined_text = "\n\n".join(attachment_texts)
system_msg = next(
(msg for msg in prepared_messages if msg.get("role") == "system"),
{"role": "system", "content": ""},
)
if system_msg not in prepared_messages:
prepared_messages.insert(0, system_msg)
system_msg["content"] += f"\n\n{combined_text}"
return prepared_messages
def handle_tool_calls(
self, agent, tool_calls: List[ToolCall], tools_dict: Dict, messages: List[Dict]
) -> Generator:
"""
Execute tool calls and update conversation history.
Args:
agent: The agent instance
tool_calls: List of tool calls to execute
tools_dict: Available tools dictionary
messages: Current conversation history
Returns:
Updated messages list
"""
updated_messages = messages.copy()
for call in tool_calls:
try:
self.tool_calls.append(call)
tool_executor_gen = agent._execute_tool_action(tools_dict, call)
while True:
try:
yield next(tool_executor_gen)
except StopIteration as e:
tool_response, call_id = e.value
break
updated_messages.append(
{
"role": "assistant",
"content": [
{
"function_call": {
"name": call.name,
"args": call.arguments,
"call_id": call_id,
}
}
],
}
)
updated_messages.append(self.create_tool_message(call, tool_response))
except Exception as e:
logger.error(f"Error executing tool: {str(e)}", exc_info=True)
updated_messages.append(
{
"role": "tool",
"content": f"Error executing tool: {str(e)}",
"tool_call_id": call.id,
}
)
return updated_messages
def handle_non_streaming(
self, agent, response: Any, tools_dict: Dict, messages: List[Dict]
) -> Generator:
"""
Handle non-streaming response flow.
Args:
agent: The agent instance
response: Current LLM response
tools_dict: Available tools dictionary
messages: Conversation history
Returns:
Final response after processing all tool calls
"""
parsed = self.parse_response(response)
self.llm_calls.append(build_stack_data(agent.llm))
while parsed.requires_tool_call:
tool_handler_gen = self.handle_tool_calls(
agent, parsed.tool_calls, tools_dict, messages
)
while True:
try:
yield next(tool_handler_gen)
except StopIteration as e:
messages = e.value
break
response = agent.llm.gen(
model=agent.gpt_model, messages=messages, tools=agent.tools
)
parsed = self.parse_response(response)
self.llm_calls.append(build_stack_data(agent.llm))
return parsed.content
def handle_streaming(
self, agent, response: Any, tools_dict: Dict, messages: List[Dict]
) -> Generator:
"""
Handle streaming response flow.
Args:
agent: The agent instance
response: Current LLM response
tools_dict: Available tools dictionary
messages: Conversation history
Yields:
Streaming response chunks
"""
buffer = ""
tool_calls = {}
for chunk in self._iterate_stream(response):
if isinstance(chunk, str):
yield chunk
continue
parsed = self.parse_response(chunk)
if parsed.tool_calls:
for call in parsed.tool_calls:
if call.index not in tool_calls:
tool_calls[call.index] = call
else:
existing = tool_calls[call.index]
if call.id:
existing.id = call.id
if call.name:
existing.name = call.name
if call.arguments:
existing.arguments += call.arguments
if parsed.finish_reason == "tool_calls":
tool_handler_gen = self.handle_tool_calls(
agent, list(tool_calls.values()), tools_dict, messages
)
while True:
try:
yield next(tool_handler_gen)
except StopIteration as e:
messages = e.value
break
tool_calls = {}
response = agent.llm.gen_stream(
model=agent.gpt_model, messages=messages, tools=agent.tools
)
self.llm_calls.append(build_stack_data(agent.llm))
yield from self.handle_streaming(agent, response, tools_dict, messages)
return
if parsed.content:
buffer += parsed.content
yield buffer
buffer = ""
if parsed.finish_reason == "stop":
return

View File

@@ -0,0 +1,78 @@
import uuid
from typing import Any, Dict, Generator
from application.llm.handlers.base import LLMHandler, LLMResponse, ToolCall
class GoogleLLMHandler(LLMHandler):
"""Handler for Google's GenAI API."""
def parse_response(self, response: Any) -> LLMResponse:
"""Parse Google response into standardized format."""
if isinstance(response, str):
return LLMResponse(
content=response,
tool_calls=[],
finish_reason="stop",
raw_response=response,
)
if hasattr(response, "candidates"):
parts = response.candidates[0].content.parts if response.candidates else []
tool_calls = [
ToolCall(
id=str(uuid.uuid4()),
name=part.function_call.name,
arguments=part.function_call.args,
)
for part in parts
if hasattr(part, "function_call") and part.function_call is not None
]
content = " ".join(
part.text
for part in parts
if hasattr(part, "text") and part.text is not None
)
return LLMResponse(
content=content,
tool_calls=tool_calls,
finish_reason="tool_calls" if tool_calls else "stop",
raw_response=response,
)
else:
tool_calls = []
if hasattr(response, "function_call"):
tool_calls.append(
ToolCall(
id=str(uuid.uuid4()),
name=response.function_call.name,
arguments=response.function_call.args,
)
)
return LLMResponse(
content=response.text if hasattr(response, "text") else "",
tool_calls=tool_calls,
finish_reason="tool_calls" if tool_calls else "stop",
raw_response=response,
)
def create_tool_message(self, tool_call: ToolCall, result: Any) -> Dict:
"""Create Google-style tool message."""
from google.genai import types
return {
"role": "tool",
"content": [
types.Part.from_function_response(
name=tool_call.name, response={"result": result}
).to_json_dict()
],
}
def _iterate_stream(self, response: Any) -> Generator:
"""Iterate through Google streaming response."""
for chunk in response:
yield chunk

View File

@@ -0,0 +1,18 @@
from application.llm.handlers.base import LLMHandler
from application.llm.handlers.google import GoogleLLMHandler
from application.llm.handlers.openai import OpenAILLMHandler
class LLMHandlerCreator:
handlers = {
"openai": OpenAILLMHandler,
"google": GoogleLLMHandler,
"default": OpenAILLMHandler,
}
@classmethod
def create_handler(cls, llm_type: str, *args, **kwargs) -> LLMHandler:
handler_class = cls.handlers.get(llm_type.lower())
if not handler_class:
raise ValueError(f"No LLM handler class found for type {llm_type}")
return handler_class(*args, **kwargs)

View File

@@ -0,0 +1,57 @@
from typing import Any, Dict, Generator
from application.llm.handlers.base import LLMHandler, LLMResponse, ToolCall
class OpenAILLMHandler(LLMHandler):
"""Handler for OpenAI API."""
def parse_response(self, response: Any) -> LLMResponse:
"""Parse OpenAI response into standardized format."""
if isinstance(response, str):
return LLMResponse(
content=response,
tool_calls=[],
finish_reason="stop",
raw_response=response,
)
message = getattr(response, "message", None) or getattr(response, "delta", None)
tool_calls = []
if hasattr(message, "tool_calls"):
tool_calls = [
ToolCall(
id=getattr(tc, "id", ""),
name=getattr(tc.function, "name", ""),
arguments=getattr(tc.function, "arguments", ""),
index=getattr(tc, "index", None),
)
for tc in message.tool_calls or []
]
return LLMResponse(
content=getattr(message, "content", ""),
tool_calls=tool_calls,
finish_reason=getattr(response, "finish_reason", ""),
raw_response=response,
)
def create_tool_message(self, tool_call: ToolCall, result: Any) -> Dict:
"""Create OpenAI-style tool message."""
return {
"role": "tool",
"content": [
{
"function_response": {
"name": tool_call.name,
"response": {"result": result},
"call_id": tool_call.id,
}
}
],
}
def _iterate_stream(self, response: Any) -> Generator:
"""Iterate through OpenAI streaming response."""
for chunk in response:
yield chunk

View File

@@ -2,6 +2,7 @@ from application.llm.base import BaseLLM
from application.core.settings import settings
import threading
class LlamaSingleton:
_instances = {}
_lock = threading.Lock() # Add a lock for thread synchronization
@@ -29,7 +30,7 @@ class LlamaCpp(BaseLLM):
self,
api_key=None,
user_api_key=None,
llm_name=settings.MODEL_PATH,
llm_name=settings.LLM_PATH,
*args,
**kwargs,
):
@@ -42,14 +43,18 @@ class LlamaCpp(BaseLLM):
context = messages[0]["content"]
user_question = messages[-1]["content"]
prompt = f"### Instruction \n {user_question} \n ### Context \n {context} \n ### Answer \n"
result = LlamaSingleton.query_model(self.llama, prompt, max_tokens=150, echo=False)
result = LlamaSingleton.query_model(
self.llama, prompt, max_tokens=150, echo=False
)
return result["choices"][0]["text"].split("### Answer \n")[-1]
def _raw_gen_stream(self, baseself, model, messages, stream=True, **kwargs):
context = messages[0]["content"]
user_question = messages[-1]["content"]
prompt = f"### Instruction \n {user_question} \n ### Context \n {context} \n ### Answer \n"
result = LlamaSingleton.query_model(self.llama, prompt, max_tokens=150, echo=False, stream=stream)
result = LlamaSingleton.query_model(
self.llama, prompt, max_tokens=150, echo=False, stream=stream
)
for item in result:
for choice in item["choices"]:
yield choice["text"]
yield choice["text"]

View File

@@ -1,10 +1,13 @@
You are an AI assistant and talk like you're thinking out loud. Given the following query, outline a concise thought process that includes key steps and considerations necessary for effective analysis and response. Avoid pointwise formatting. The goal is to break down the query into manageable components without excessive detail, focusing on clarity and logical progression.
Include the following elements in your thought process:
Include the following elements in your thought and execution process:
1. Identify the main objective of the query.
2. Determine any relevant context or background information needed to understand the query.
3. List potential approaches or methods to address the query.
4. Highlight any critical factors or constraints that may influence the outcome.
5. Plan with available tools to help you with the analysis but dont execute them. Tools will be executed by another AI.
Query: {query}
Summaries: {summaries}
Summaries: {summaries}
Prompt: {prompt}
Observations(potentially previous tool calls): {observations}

View File

@@ -46,7 +46,7 @@ pandas==2.2.3
openpyxl==3.1.5
pathable==0.4.4
pillow==11.1.0
portalocker==3.1.1
portalocker>=2.7.0,<3.0.0
prance==23.6.21.0
prompt-toolkit==3.0.51
protobuf==5.29.3
@@ -62,7 +62,7 @@ python-dotenv==1.0.1
python-jose==3.4.0
python-pptx==1.0.2
redis==5.2.1
referencing==0.36.2
referencing>=0.28.0,<0.31.0
regex==2024.11.6
requests==2.32.3
retry==0.9.2

View File

@@ -29,10 +29,10 @@ class BraveRetSearch(BaseRetriever):
self.token_limit = (
token_limit
if token_limit
< settings.MODEL_TOKEN_LIMITS.get(
< settings.LLM_TOKEN_LIMITS.get(
self.gpt_model, settings.DEFAULT_MAX_HISTORY
)
else settings.MODEL_TOKEN_LIMITS.get(
else settings.LLM_TOKEN_LIMITS.get(
self.gpt_model, settings.DEFAULT_MAX_HISTORY
)
)
@@ -59,7 +59,7 @@ class BraveRetSearch(BaseRetriever):
docs.append({"text": snippet, "title": title, "link": link})
except IndexError:
pass
if settings.LLM_NAME == "llama.cpp":
if settings.LLM_PROVIDER == "llama.cpp":
docs = [docs[0]]
return docs
@@ -84,7 +84,7 @@ class BraveRetSearch(BaseRetriever):
messages_combine.append({"role": "user", "content": self.question})
llm = LLMCreator.create_llm(
settings.LLM_NAME,
settings.LLM_PROVIDER,
api_key=settings.API_KEY,
user_api_key=self.user_api_key,
decoded_token=self.decoded_token,

View File

@@ -16,7 +16,7 @@ class ClassicRAG(BaseRetriever):
token_limit=150,
gpt_model="docsgpt",
user_api_key=None,
llm_name=settings.LLM_NAME,
llm_name=settings.LLM_PROVIDER,
api_key=settings.API_KEY,
decoded_token=None,
):
@@ -28,10 +28,10 @@ class ClassicRAG(BaseRetriever):
self.token_limit = (
token_limit
if token_limit
< settings.MODEL_TOKEN_LIMITS.get(
< settings.LLM_TOKEN_LIMITS.get(
self.gpt_model, settings.DEFAULT_MAX_HISTORY
)
else settings.MODEL_TOKEN_LIMITS.get(
else settings.LLM_TOKEN_LIMITS.get(
self.gpt_model, settings.DEFAULT_MAX_HISTORY
)
)
@@ -44,8 +44,8 @@ class ClassicRAG(BaseRetriever):
user_api_key=self.user_api_key,
decoded_token=decoded_token,
)
self.question = self._rephrase_query()
self.vectorstore = source["active_docs"] if "active_docs" in source else None
self.question = self._rephrase_query()
self.decoded_token = decoded_token
def _rephrase_query(self):
@@ -53,6 +53,8 @@ class ClassicRAG(BaseRetriever):
not self.original_question
or not self.chat_history
or self.chat_history == []
or self.chunks == 0
or self.vectorstore is None
):
return self.original_question
@@ -77,7 +79,7 @@ class ClassicRAG(BaseRetriever):
return self.original_question
def _get_data(self):
if self.chunks == 0:
if self.chunks == 0 or self.vectorstore is None:
docs = []
else:
docsearch = VectorCreator.create_vectorstore(

View File

@@ -28,10 +28,10 @@ class DuckDuckSearch(BaseRetriever):
self.token_limit = (
token_limit
if token_limit
< settings.MODEL_TOKEN_LIMITS.get(
< settings.LLM_TOKEN_LIMITS.get(
self.gpt_model, settings.DEFAULT_MAX_HISTORY
)
else settings.MODEL_TOKEN_LIMITS.get(
else settings.LLM_TOKEN_LIMITS.get(
self.gpt_model, settings.DEFAULT_MAX_HISTORY
)
)
@@ -58,7 +58,7 @@ class DuckDuckSearch(BaseRetriever):
)
except IndexError:
pass
if settings.LLM_NAME == "llama.cpp":
if settings.LLM_PROVIDER == "llama.cpp":
docs = [docs[0]]
return docs
@@ -83,7 +83,7 @@ class DuckDuckSearch(BaseRetriever):
messages_combine.append({"role": "user", "content": self.question})
llm = LLMCreator.create_llm(
settings.LLM_NAME,
settings.LLM_PROVIDER,
api_key=settings.API_KEY,
user_api_key=self.user_api_key,
decoded_token=self.decoded_token,

View File

@@ -3,18 +3,18 @@ from application.retriever.duckduck_search import DuckDuckSearch
from application.retriever.brave_search import BraveRetSearch
class RetrieverCreator:
retrievers = {
'classic': ClassicRAG,
'duckduck_search': DuckDuckSearch,
'brave_search': BraveRetSearch,
'default': ClassicRAG
"classic": ClassicRAG,
"duckduck_search": DuckDuckSearch,
"brave_search": BraveRetSearch,
"default": ClassicRAG,
}
@classmethod
def create_retriever(cls, type, *args, **kwargs):
retiever_class = cls.retrievers.get(type.lower())
retriever_type = (type or "default").lower()
retiever_class = cls.retrievers.get(retriever_type)
if not retiever_class:
raise ValueError(f"No retievers class found for type {type}")
return retiever_class(*args, **kwargs)
return retiever_class(*args, **kwargs)

View File

@@ -74,8 +74,8 @@ def limit_chat_history(history, max_token_limit=None, gpt_model="docsgpt"):
max_token_limit
if max_token_limit
and max_token_limit
< settings.MODEL_TOKEN_LIMITS.get(gpt_model, settings.DEFAULT_MAX_HISTORY)
else settings.MODEL_TOKEN_LIMITS.get(gpt_model, settings.DEFAULT_MAX_HISTORY)
< settings.LLM_TOKEN_LIMITS.get(gpt_model, settings.DEFAULT_MAX_HISTORY)
else settings.LLM_TOKEN_LIMITS.get(gpt_model, settings.DEFAULT_MAX_HISTORY)
)
if not history:

View File

@@ -32,22 +32,26 @@ class FaissStore(BaseVectorStore):
with tempfile.TemporaryDirectory() as temp_dir:
faiss_path = f"{self.path}/index.faiss"
pkl_path = f"{self.path}/index.pkl"
if not self.storage.file_exists(faiss_path) or not self.storage.file_exists(pkl_path):
raise FileNotFoundError(f"Index files not found in storage at {self.path}")
if not self.storage.file_exists(
faiss_path
) or not self.storage.file_exists(pkl_path):
raise FileNotFoundError(
f"Index files not found in storage at {self.path}"
)
faiss_file = self.storage.get_file(faiss_path)
pkl_file = self.storage.get_file(pkl_path)
local_faiss_path = os.path.join(temp_dir, "index.faiss")
local_pkl_path = os.path.join(temp_dir, "index.pkl")
with open(local_faiss_path, 'wb') as f:
with open(local_faiss_path, "wb") as f:
f.write(faiss_file.read())
with open(local_pkl_path, 'wb') as f:
with open(local_pkl_path, "wb") as f:
f.write(pkl_file.read())
self.docsearch = FAISS.load_local(
temp_dir, self.embeddings, allow_dangerous_deserialization=True
)

View File

@@ -143,8 +143,8 @@ def run_agent_logic(agent_config, input_data):
agent = AgentCreator.create_agent(
agent_type,
endpoint="webhook",
llm_name=settings.LLM_NAME,
gpt_model=settings.MODEL_NAME,
llm_name=settings.LLM_PROVIDER,
gpt_model=settings.LLM_NAME,
api_key=settings.API_KEY,
user_api_key=user_api_key,
prompt=prompt,
@@ -159,7 +159,7 @@ def run_agent_logic(agent_config, input_data):
prompt=prompt,
chunks=chunks,
token_limit=settings.DEFAULT_MAX_HISTORY,
gpt_model=settings.MODEL_NAME,
gpt_model=settings.LLM_NAME,
user_api_key=user_api_key,
decoded_token=decoded_token,
)
@@ -449,7 +449,7 @@ def attachment_worker(self, file_info, user):
try:
self.update_state(state="PROGRESS", meta={"current": 10})
storage = StorageCreator.get_storage()
self.update_state(
state="PROGRESS", meta={"current": 30, "status": "Processing content"}
)
@@ -458,9 +458,11 @@ def attachment_worker(self, file_info, user):
relative_path,
lambda local_path, **kwargs: SimpleDirectoryReader(
input_files=[local_path], exclude_hidden=True, errors="ignore"
).load_data()[0].text
)
.load_data()[0]
.text,
)
token_count = num_tokens_from_string(content)
self.update_state(
@@ -475,6 +477,7 @@ def attachment_worker(self, file_info, user):
"_id": doc_id,
"user": user,
"path": relative_path,
"filename": filename,
"content": content,
"token_count": token_count,
"mime_type": mime_type,
@@ -487,9 +490,7 @@ def attachment_worker(self, file_info, user):
f"Stored attachment with ID: {attachment_id}", extra={"user": user}
)
self.update_state(
state="PROGRESS", meta={"current": 100, "status": "Complete"}
)
self.update_state(state="PROGRESS", meta={"current": 100, "status": "Complete"})
return {
"filename": filename,

View File

@@ -1,3 +1,4 @@
name: docsgpt-oss
services:
redis:

View File

@@ -1,3 +1,4 @@
name: docsgpt-oss
services:
frontend:
build: ../frontend
@@ -17,13 +18,13 @@ services:
environment:
- API_KEY=$API_KEY
- EMBEDDINGS_KEY=$API_KEY
- LLM_PROVIDER=$LLM_PROVIDER
- LLM_NAME=$LLM_NAME
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/1
- MONGO_URI=mongodb://mongo:27017/docsgpt
- CACHE_REDIS_URL=redis://redis:6379/2
- OPENAI_BASE_URL=$OPENAI_BASE_URL
- MODEL_NAME=$MODEL_NAME
ports:
- "7091:7091"
volumes:
@@ -41,6 +42,7 @@ services:
environment:
- API_KEY=$API_KEY
- EMBEDDINGS_KEY=$API_KEY
- LLM_PROVIDER=$LLM_PROVIDER
- LLM_NAME=$LLM_NAME
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/1

View File

@@ -4,7 +4,7 @@ metadata:
name: docsgpt-secrets
type: Opaque
data:
LLM_NAME: ZG9jc2dwdA==
LLM_PROVIDER: ZG9jc2dwdA==
INTERNAL_KEY: aW50ZXJuYWw=
CELERY_BROKER_URL: cmVkaXM6Ly9yZWRpcy1zZXJ2aWNlOjYzNzkvMA==
CELERY_RESULT_BACKEND: cmVkaXM6Ly9yZWRpcy1zZXJ2aWNlOjYzNzkvMA==

View File

@@ -0,0 +1,117 @@
import Image from 'next/image';
const iconMap = {
'API Tool': '/toolIcons/tool_api_tool.svg',
'Brave Search Tool': '/toolIcons/tool_brave.svg',
'Cryptoprice Tool': '/toolIcons/tool_cryptoprice.svg',
'Ntfy Tool': '/toolIcons/tool_ntfy.svg',
'PostgreSQL Tool': '/toolIcons/tool_postgres.svg',
'Read Webpage Tool': '/toolIcons/tool_read_webpage.svg',
'Telegram Tool': '/toolIcons/tool_telegram.svg'
};
export function ToolCards({ items }) {
return (
<>
<div className="tool-cards">
{items.map(({ title, link, description }) => {
const isExternal = link.startsWith('https://');
const iconSrc = iconMap[title] || '/default-icon.png'; // Default icon if not found
return (
<div
key={title}
className={`card${isExternal ? ' external' : ''}`}
>
<a href={link} target={isExternal ? '_blank' : undefined} rel="noopener noreferrer" className="card-link-wrapper">
<div className="card-icon-container">
{iconSrc && <div className="card-icon"><Image src={iconSrc} alt={title} width={32} height={32} /></div>} {/* Reduced icon size */}
</div>
<h3 className="card-title">{title}</h3>
{description && <p className="card-description">{description}</p>}
{/* Card URL element removed from here */}
</a>
</div>
);
})}
</div>
<style jsx>{`
.tool-cards {
margin-top: 24px;
display: grid;
grid-template-columns: 1fr;
gap: 16px;
}
@media (min-width: 768px) {
.tool-cards {
grid-template-columns: 1fr 1fr; /* Keeps two columns on wider screens */
}
}
.card {
background-color: #222222;
border-radius: 8px;
padding: 16px; /* Existing padding */
transition: background-color 0.3s;
position: relative;
color: #ffffff;
display: flex; /* Using flex to help with alignment */
flex-direction: column;
/* align-items: center; // Alignment for items inside card-link-wrapper is better */
/* justify-content: center; // We want content to flow from top */
height: 100%; /* Fill the height of the grid cell, ensures cards in a row are same height */
}
.card:hover {
background-color: #333333;
}
.card.external::after {
content: "↗";
position: absolute;
top: 12px;
right: 12px;
color: #ffffff;
font-size: 0.7em;
opacity: 0.8;
}
.card-link-wrapper {
display: flex;
flex-direction: column;
align-items:center; /* Centers icon, title, description horizontally */
text-align: center; /* Ensures text within p and h3 is centered */
color: inherit;
text-decoration: none;
width:100%;
height: 100%; /* Make the link wrapper take full card height */
justify-content: flex-start; /* Align content to the top */
}
.card-icon-container{
display:flex;
justify-content:center;
width: 100%;
margin-top: 8px; /* Added some margin at the top if needed */
margin-bottom: 12px; /* Increased space between icon and title */
}
.card-icon {
display: block;
/* margin: 0 auto; // Center handled by card-icon-container */
}
.card-title {
font-weight: 600;
margin-bottom: 8px; /* Increased space below title */
font-size: 16px; /* Consider increasing slightly if descriptions are longer e.g. 17px or 18px */
color: #f0f0f0;
}
.card-description {
/* margin-bottom: 0; // Original value */
font-size: 14px; /* Slightly increased font size for better readability */
color: #aaaaaa;
line-height: 1.5; /* Slightly increased line height */
flex-grow: 1; /* Allows description to take available space */
overflow-y: auto; /* Adds scroll if description is too long, though ideally content fits */
padding-bottom: 8px; /* Add some padding at the bottom of the description area */
}
`}</style>
</>
);
}

7038
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,10 +7,10 @@
"license": "MIT",
"dependencies": {
"@vercel/analytics": "^1.1.1",
"docsgpt-react": "^0.5.0",
"next": "^14.2.26",
"nextra": "^2.13.2",
"nextra-theme-docs": "^2.13.2",
"docsgpt-react": "^0.5.1",
"next": "^15.3.3",
"nextra": "^4.2.17",
"nextra-theme-docs": "^4.2.17",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}

View File

@@ -0,0 +1,6 @@
{
"basics": {
"title": "🤖 Agent Basics",
"href": "/Agents/basics"
}
}

View File

@@ -0,0 +1,109 @@
---
title: Understanding DocsGPT Agents
description: Learn about DocsGPT Agents, their types, how to create and manage them, and how they can enhance your interaction with documents and tools.
---
import { Callout } from 'nextra/components';
import Image from 'next/image'; // Assuming you might want to embed images later, like the ones you uploaded.
# Understanding DocsGPT Agents 🤖
DocsGPT Agents are advanced, configurable AI entities designed to go beyond simple question-answering. They act as specialized assistants or workers that combine instructions (prompts), knowledge (document sources), and capabilities (tools) to perform a wide range of tasks, automate workflows, and provide tailored interactions.
Think of an Agent as a pre-configured version of DocsGPT, fine-tuned for a specific purpose, such as classifying documents, responding to new form submissions, or validating emails.
## Why Use Agents?
* **Personalization:** Create AI assistants that behave and respond according to specific roles or personas.
* **Task Specialization:** Design agents focused on particular tasks, like customer support, data extraction, or content generation.
* **Knowledge Integration:** Equip agents with specific document sources, making them experts in particular domains.
* **Tool Utilization:** Grant agents access to various tools, allowing them to interact with external services, fetch live data, or perform actions.
* **Automation:** Automate repetitive tasks by defining an agent's behavior and integrating it via webhooks or other means.
* **Shareability:** Share your custom-configured agents with others or use agents shared with you.
Agents provide a more structured and powerful way to leverage LLMs compared to a standard chat interface, as they come with a pre-defined context, instruction set, and set of capabilities.
## Core Components of an Agent
When you create or configure an agent, you'll work with these key components:
**Meta:**
* **Agent Name:** A user-friendly name to identify the agent (e.g., "Support Ticket Classifier," "Product Spec Expert").
* **Describe your agent:** A brief description for you or users to understand the agent's purpose.
**Source:**
* **Select source:** The knowledge base for the agent. You can select from previously uploaded documents or data sources. This is what the agent will "know."
* **Chunks per query:** A numerical value determining how many relevant text chunks from the selected source are sent to the LLM with each query. This helps manage context length and relevance.
**Prompt:**
The main set of instructions or system [prompt](/Guides/Customising-prompts) that defines the agent's persona, objectives, constraints, and how it should behave or respond.
**Tools:** A selection of available [DocsGPT Tools](/Tools/basics) that the agent can use to perform actions or access external information.
**Agent type:** The underlying operational logic or architecture the agent uses. DocsGPT supports different types of agents, each suited for different kinds of tasks.
## Understanding Agent Types
DocsGPT allows for different "types" of agents, each with a distinct way of processing information and generating responses. The code for these agent types can be found in the `application/agents/` directory.
### 1. Classic Agent (`classic_agent.py`)
**How it works:** The Classic Agent follows a traditional Retrieval Augmented Generation (RAG) approach.
1. **Retrieve:** When a query is made, it first searches the selected Source documents for relevant information.
2. **Augment:** This retrieved data is then added to the context, along with the main Prompt and the user's query.
3. **Generate:** The LLM generates a response based on this augmented context. It can also utilize any configured tools if the LLM decides they are necessary.
**Best for:**
* Direct question-answering over a specific set of documents.
* Tasks where the primary goal is to extract and synthesize information from the provided sources.
* Simpler tool integrations where the decision to use a tool is straightforward.
### 2. ReAct Agent (`react_agent.py`)
**How it works:** The ReAct Agent employs a more sophisticated "Reason and Act" framework. This involves a multi-step process:
1. **Plan (Thought):** Based on the query, its prompt, and available tools/sources, the LLM first generates a plan or a sequence of thoughts on how to approach the problem. You might see this output as a "thought" process during generation.
2. **Act:** The agent then executes actions based on this plan. This might involve querying its sources, using a tool, or performing internal reasoning.
3. **Observe:** It gathers observations from the results of its actions (e.g., data from a tool, snippets from documents).
4. **Repeat (if necessary):** Steps 2 and 3 can be repeated as the agent refines its approach or gathers more information.
5. **Conclude:** Finally, it generates the final answer based on the initial query and all accumulated observations.
**Best for:**
* More complex tasks that require multi-step reasoning or problem-solving.
* Scenarios where the agent needs to dynamically decide which tools to use and in what order, based on intermediate results.
* Interactive tasks where the agent needs to "think" through a problem.
<Callout type="info">
Developers looking to introduce new agent architectures can explore the `application/agents/` directory. `classic_agent.py` and `react_agent.py` serve as excellent starting points, demonstrating how to inherit from `BaseAgent` and structure agent logic.
</Callout>
## Navigating and Managing Agents in DocsGPT
You can easily access and manage your agents through the DocsGPT user interface. Recently used agents appear at the top of the left sidebar for quick access. Below these, the "Manage Agents" button will take you to the main Agents page.
### Creating a New Agent
1. Navigate to the "Agents" page.
2. Click the **"New Agent"** button.
3. You will be presented with the "New Agent" configuration screen:
<Image
src="/new-agent.png"
alt="API Tool configuration example for phone validation"
width={800}
height={450}
style={{ margin: '1em auto', display: 'block', borderRadius: '8px' }}
/>
4. Fill in the fields as described in the "Core Components of an Agent" section.
5. Once configured, you can **"Save Draft"** to continue editing later or **"Publish"** to make the agent active.
## Interacting with and Editing Agents
Once an agent is created, you can:
* **Chat with it:** Select the agent to start an interaction.
* **View Logs:** Access usage statistics, monitor token consumption per interaction, and review user message feedbacks. This is crucial for understanding how your agent is being used and performing.
* **Edit an Agent:**
* Modify any of its configuration settings (name, description, source, prompt, tools, type).
* **Generate a Public Link:** From the edit screen, you can create a shareable public link that allows others to import and use your agent.
* **Get a Webhook URL:** You can also obtain a Webhook URL for the agent. This allows external applications or services to trigger the agent and receive responses programmatically, enabling powerful integrations and automations.

View File

@@ -37,7 +37,7 @@ The fastest way to try out DocsGPT is by using the public API endpoint. This req
Open the `.env` file and add the following lines:
```
LLM_NAME=docsgpt
LLM_PROVIDER=docsgpt
VITE_API_STREAMING=true
```
@@ -93,16 +93,16 @@ There are two Ollama optional files:
3. **Pull the Ollama Model:**
**Crucially, after launching with Ollama, you need to pull the desired model into the Ollama container.** Find the `MODEL_NAME` you configured in your `.env` file (e.g., `llama3.2:1b`). Then execute the following command to pull the model *inside* the running Ollama container:
**Crucially, after launching with Ollama, you need to pull the desired model into the Ollama container.** Find the `LLM_NAME` you configured in your `.env` file (e.g., `llama3.2:1b`). Then execute the following command to pull the model *inside* the running Ollama container:
```bash
docker compose -f deployment/docker-compose.yaml -f deployment/optional/docker-compose.optional.ollama-cpu.yaml exec -it ollama ollama pull <MODEL_NAME>
docker compose -f deployment/docker-compose.yaml -f deployment/optional/docker-compose.optional.ollama-cpu.yaml exec -it ollama ollama pull <LLM_NAME>
```
or (for GPU):
```bash
docker compose -f deployment/docker-compose.yaml -f deployment/optional/docker-compose.optional.ollama-gpu.yaml exec -it ollama ollama pull <MODEL_NAME>
docker compose -f deployment/docker-compose.yaml -f deployment/optional/docker-compose.optional.ollama-gpu.yaml exec -it ollama ollama pull <LLM_NAME>
```
Replace `<MODEL_NAME>` with the actual model name from your `.env` file.
Replace `<LLM_NAME>` with the actual model name from your `.env` file.
4. **Access DocsGPT in your browser:**

View File

@@ -20,9 +20,9 @@ The easiest and recommended way to configure basic settings is by using a `.env`
**Example `.env` file structure:**
```
LLM_NAME=openai
LLM_PROVIDER=openai
API_KEY=YOUR_OPENAI_API_KEY
MODEL_NAME=gpt-4o
LLM_NAME=gpt-4o
```
### 2. Configuration via `settings.py` file (Advanced)
@@ -37,7 +37,7 @@ While modifying `settings.py` offers more flexibility, it's generally recommende
Here are some of the most fundamental settings you'll likely want to configure:
- **`LLM_NAME`**: This setting determines which Large Language Model (LLM) provider DocsGPT will use. It tells DocsGPT which API to interact with.
- **`LLM_PROVIDER`**: This setting determines which Large Language Model (LLM) provider DocsGPT will use. It tells DocsGPT which API to interact with.
- **Common values:**
- `docsgpt`: Use the DocsGPT Public API Endpoint (simple and free, as offered in `setup.sh` option 1).
@@ -49,11 +49,11 @@ Here are some of the most fundamental settings you'll likely want to configure:
- `azure_openai`: Use Azure OpenAI Service.
- `openai` (when using local inference engines like Ollama, Llama.cpp, TGI, etc.): This signals DocsGPT to use an OpenAI-compatible API format, even if the actual LLM is running locally.
- **`MODEL_NAME`**: Specifies the specific model to use from the chosen LLM provider. The available models depend on the `LLM_NAME` you've selected.
- **`LLM_NAME`**: Specifies the specific model to use from the chosen LLM provider. The available models depend on the `LLM_PROVIDER` you've selected.
- **Examples:**
- For `LLM_NAME=openai`: `gpt-4o`
- For `LLM_NAME=google`: `gemini-2.0-flash`
- For `LLM_PROVIDER=openai`: `gpt-4o`
- For `LLM_PROVIDER=google`: `gemini-2.0-flash`
- For local models (e.g., Ollama): `llama3.2:1b` (or any model name available in your setup).
- **`EMBEDDINGS_NAME`**: This setting defines which embedding model DocsGPT will use to generate vector embeddings for your documents. Embeddings are numerical representations of text that allow DocsGPT to understand the semantic meaning of your documents for efficient search and retrieval.
@@ -63,7 +63,7 @@ Here are some of the most fundamental settings you'll likely want to configure:
- **`API_KEY`**: Required for most cloud-based LLM providers. This is your authentication key to access the LLM provider's API. You'll need to obtain this key from your chosen provider's platform.
- **`OPENAI_BASE_URL`**: Specifically used when `LLM_NAME` is set to `openai` but you are connecting to a local inference engine (like Ollama, Llama.cpp, etc.) that exposes an OpenAI-compatible API. This setting tells DocsGPT where to find your local LLM server.
- **`OPENAI_BASE_URL`**: Specifically used when `LLM_PROVIDER` is set to `openai` but you are connecting to a local inference engine (like Ollama, Llama.cpp, etc.) that exposes an OpenAI-compatible API. This setting tells DocsGPT where to find your local LLM server.
## Configuration Examples
@@ -74,9 +74,9 @@ Let's look at some concrete examples of how to configure these settings in your
To use OpenAI's `gpt-4o` model, you would configure your `.env` file like this:
```
LLM_NAME=openai
LLM_PROVIDER=openai
API_KEY=YOUR_OPENAI_API_KEY # Replace with your actual OpenAI API key
MODEL_NAME=gpt-4o
LLM_NAME=gpt-4o
```
Make sure to replace `YOUR_OPENAI_API_KEY` with your actual OpenAI API key.
@@ -86,14 +86,57 @@ Make sure to replace `YOUR_OPENAI_API_KEY` with your actual OpenAI API key.
To use a local Ollama server with the `llama3.2:1b` model, you would configure your `.env` file like this:
```
LLM_NAME=openai # Using OpenAI compatible API format for local models
LLM_PROVIDER=openai # Using OpenAI compatible API format for local models
API_KEY=None # API Key is not needed for local Ollama
MODEL_NAME=llama3.2:1b
LLM_NAME=llama3.2:1b
OPENAI_BASE_URL=http://host.docker.internal:11434/v1 # Default Ollama API URL within Docker
EMBEDDINGS_NAME=huggingface_sentence-transformers/all-mpnet-base-v2 # You can also run embeddings locally if needed
```
In this case, even though you are using Ollama locally, `LLM_NAME` is set to `openai` because Ollama (and many other local inference engines) are designed to be API-compatible with OpenAI. `OPENAI_BASE_URL` points DocsGPT to the local Ollama server.
In this case, even though you are using Ollama locally, `LLM_PROVIDER` is set to `openai` because Ollama (and many other local inference engines) are designed to be API-compatible with OpenAI. `OPENAI_BASE_URL` points DocsGPT to the local Ollama server.
## Authentication Settings
DocsGPT includes a JWT (JSON Web Token) based authentication feature for managing sessions or securing local deployments while allowing access.
- **`AUTH_TYPE`**: This setting in your `.env` file or `settings.py` determines the authentication method.
- **Possible values:**
- `None` (or not set): No authentication is used.
- `simple_jwt`: A single, long-lived JWT token is generated and used for all authenticated requests. This is useful for securing a local deployment with a shared secret.
- `session_jwt`: Unique JWT tokens are generated for sessions, typically for individual users or temporary access.
- If `AUTH_TYPE` is set to `simple_jwt` or `session_jwt`, then a `JWT_SECRET_KEY` is required.
- **`JWT_SECRET_KEY`**: This is a crucial secret key used to sign and verify JWTs.
- It can be set directly in your `.env` file or `settings.py`.
- **Automatic Key Generation**: If `AUTH_TYPE` is `simple_jwt` or `session_jwt` and `JWT_SECRET_KEY` is _not_ set in your environment variables or `settings.py`, DocsGPT will attempt to:
1. Read the key from a file named `.jwt_secret_key` in the project's root directory.
2. If the file doesn't exist, it will generate a new 32-byte random key, save it to `.jwt_secret_key`, and use it for the session. This ensures that the key persists across application restarts.
- **Security Note**: It's vital to keep this key secure. If you set it manually, choose a strong, random string.
**How it works:**
- When `AUTH_TYPE` is set to `simple_jwt`, a token is generated at startup (if not already present or configured) and printed to the console. This token should be included in the `Authorization` header of your API requests as a Bearer token (e.g., `Authorization: Bearer YOUR_SIMPLE_JWT_TOKEN`).
- When `AUTH_TYPE` is set to `session_jwt`:
- Clients can request a new token from the `/api/generate_token` endpoint.
- This token should then be included in the `Authorization` header for subsequent requests.
- The backend verifies the JWT token provided in the `Authorization` header for protected routes.
- The `/api/config` endpoint can be used to check the current `auth_type` and whether authentication is required.
**Frontend Token Input for `simple_jwt`:**
<img
src="/jwt-input.png"
alt="Frontend prompt for JWT Token"
style={{
width: '500px',
maxWidth: '100%',
display: 'block',
margin: '1em auto'
}}
/>
If you have configured `AUTH_TYPE=simple_jwt`, the DocsGPT frontend will prompt you to enter the JWT token if it's not already set or is invalid. You'll need to paste the `SIMPLE_JWT_TOKEN` (which is printed to your console when the backend starts) into this field to access the application.
## Exploring More Settings

View File

@@ -32,9 +32,9 @@ Choose the LLM of your choice.
### For Open source llm change:
<Steps>
### Step 1
For open source version please edit `LLM_NAME`, `MODEL_NAME` and others in the .env file. Refer to [⚙️ App Configuration](/Deploying/DocsGPT-Settings) for more information.
For open source version please edit `LLM_PROVIDER`, `LLM_NAME` and others in the .env file. Refer to [⚙️ App Configuration](/Deploying/DocsGPT-Settings) for more information.
### Step 2
Visit [☁️ Cloud Providers](/Models/cloud-providers) for the updated list of online models. Make sure you have the right API_KEY and correct LLM_NAME.
Visit [☁️ Cloud Providers](/Models/cloud-providers) for the updated list of online models. Make sure you have the right API_KEY and correct LLM_PROVIDER.
For self-hosted please visit [🖥️ Local Inference](/Models/local-inference).
</Steps>

View File

@@ -13,15 +13,15 @@ The primary method for configuring your LLM provider in DocsGPT is through the `
To connect to a cloud LLM provider, you will typically need to configure the following basic settings in your `.env` file:
* **`LLM_NAME`**: This setting is essential and identifies the specific cloud provider you wish to use (e.g., `openai`, `google`, `anthropic`).
* **`MODEL_NAME`**: Specifies the exact model you want to utilize from your chosen provider (e.g., `gpt-4o`, `gemini-2.0-flash`, `claude-3-5-sonnet-latest`). Refer to your provider's documentation for a list of available models.
* **`LLM_PROVIDER`**: This setting is essential and identifies the specific cloud provider you wish to use (e.g., `openai`, `google`, `anthropic`).
* **`LLM_NAME`**: Specifies the exact model you want to utilize from your chosen provider (e.g., `gpt-4o`, `gemini-2.0-flash`, `claude-3-5-sonnet-latest`). Refer to your provider's documentation for a list of available models.
* **`API_KEY`**: Almost all cloud LLM providers require an API key for authentication. Obtain your API key from your chosen provider's platform and securely store it in your `.env` file.
## Explicitly Supported Cloud Providers
DocsGPT offers direct, streamlined support for the following cloud LLM providers, making configuration straightforward. The table below outlines the `LLM_NAME` and example `MODEL_NAME` values to use for each provider in your `.env` file.
DocsGPT offers direct, streamlined support for the following cloud LLM providers, making configuration straightforward. The table below outlines the `LLM_PROVIDER` and example `LLM_NAME` values to use for each provider in your `.env` file.
| Provider | `LLM_NAME` | Example `MODEL_NAME` |
| Provider | `LLM_PROVIDER` | Example `LLM_NAME` |
| :--------------------------- | :------------- | :-------------------------- |
| DocsGPT Public API | `docsgpt` | `None` |
| OpenAI | `openai` | `gpt-4o` |
@@ -35,16 +35,16 @@ DocsGPT offers direct, streamlined support for the following cloud LLM providers
DocsGPT's flexible architecture allows you to connect to any cloud provider that offers an API compatible with the OpenAI API standard. This opens up a vast ecosystem of LLM services.
To connect to an OpenAI-compatible cloud provider, you will still use `LLM_NAME=openai` in your `.env` file. However, you will also need to specify the API endpoint of your chosen provider using the `OPENAI_BASE_URL` setting. You will also likely need to provide an `API_KEY` and `MODEL_NAME` as required by that provider.
To connect to an OpenAI-compatible cloud provider, you will still use `LLM_PROVIDER=openai` in your `.env` file. However, you will also need to specify the API endpoint of your chosen provider using the `OPENAI_BASE_URL` setting. You will also likely need to provide an `API_KEY` and `LLM_NAME` as required by that provider.
**Example for DeepSeek (OpenAI-Compatible API):**
To connect to DeepSeek, which offers an OpenAI-compatible API, your `.env` file could be configured as follows:
```
LLM_NAME=openai
LLM_PROVIDER=openai
API_KEY=YOUR_API_KEY # Your DeepSeek API key
MODEL_NAME=deepseek-chat # Or your desired DeepSeek model name
LLM_NAME=deepseek-chat # Or your desired DeepSeek model name
OPENAI_BASE_URL=https://api.deepseek.com/v1 # DeepSeek's OpenAI API URL
```

View File

@@ -60,7 +60,7 @@ To use OpenAI's `text-embedding-ada-002` embedding model, you need to set `EMBED
**Example `.env` configuration for OpenAI Embeddings:**
```
LLM_NAME=openai
LLM_PROVIDER=openai
API_KEY=YOUR_OPENAI_API_KEY # Your OpenAI API Key
EMBEDDINGS_NAME=openai_text-embedding-ada-002
```

View File

@@ -15,8 +15,8 @@ Setting up a local inference engine with DocsGPT is configured through environme
To connect to a local inference engine, you will generally need to configure these settings in your `.env` file:
* **`LLM_NAME`**: Crucially set this to `openai`. This tells DocsGPT to use the OpenAI-compatible API format for communication, even though the LLM is local.
* **`MODEL_NAME`**: Specify the model name as recognized by your local inference engine. This might be a model identifier or left as `None` if the engine doesn't require explicit model naming in the API request.
* **`LLM_PROVIDER`**: Crucially set this to `openai`. This tells DocsGPT to use the OpenAI-compatible API format for communication, even though the LLM is local.
* **`LLM_NAME`**: Specify the model name as recognized by your local inference engine. This might be a model identifier or left as `None` if the engine doesn't require explicit model naming in the API request.
* **`OPENAI_BASE_URL`**: This is essential. Set this to the base URL of your local inference engine's API endpoint. This tells DocsGPT where to find your local LLM server.
* **`API_KEY`**: Generally, for local inference engines, you can set `API_KEY=None` as authentication is usually not required in local setups.
@@ -24,16 +24,16 @@ To connect to a local inference engine, you will generally need to configure the
DocsGPT is readily configurable to work with the following local inference engines, all communicating via the OpenAI API format. Here are example `OPENAI_BASE_URL` values for each, based on default setups:
| Inference Engine | `LLM_NAME` | `OPENAI_BASE_URL` |
| :---------------------------- | :--------- | :------------------------- |
| LLaMa.cpp | `openai` | `http://localhost:8000/v1` |
| Ollama | `openai` | `http://localhost:11434/v1` |
| Text Generation Inference (TGI)| `openai` | `http://localhost:8080/v1` |
| SGLang | `openai` | `http://localhost:30000/v1` |
| vLLM | `openai` | `http://localhost:8000/v1` |
| Aphrodite | `openai` | `http://localhost:2242/v1` |
| FriendliAI | `openai` | `http://localhost:8997/v1` |
| LMDeploy | `openai` | `http://localhost:23333/v1` |
| Inference Engine | `LLM_PROVIDER` | `OPENAI_BASE_URL` |
| :---------------------------- | :------------- | :------------------------- |
| LLaMa.cpp | `openai` | `http://localhost:8000/v1` |
| Ollama | `openai` | `http://localhost:11434/v1` |
| Text Generation Inference (TGI)| `openai` | `http://localhost:8080/v1` |
| SGLang | `openai` | `http://localhost:30000/v1` |
| vLLM | `openai` | `http://localhost:8000/v1` |
| Aphrodite | `openai` | `http://localhost:2242/v1` |
| FriendliAI | `openai` | `http://localhost:8997/v1` |
| LMDeploy | `openai` | `http://localhost:23333/v1` |
**Important Note on `localhost` vs `host.docker.internal`:**

View File

@@ -0,0 +1,14 @@
{
"basics": {
"title": "🔧 Tools Basics",
"href": "/Tools/basics"
},
"api-tool": {
"title": "🗝️ API Tool",
"href": "/Tools/api-tool"
},
"creating-a-tool": {
"title": "🛠️ Creating a Custom Tool",
"href": "/Tools/creating-a-tool"
}
}

View File

@@ -0,0 +1,153 @@
---
title: 🗝️ Generic API Tool
description: Learn how to configure and use the API Tool in DocsGPT to connect with any RESTful API without writing custom code.
---
import { Callout } from 'nextra/components';
import Image from 'next/image';
# Using the Generic API Tool
The API Tool provides a no-code/low-code solution to make DocsGPT interact with third-party or internal RESTful APIs. It acts as a bridge, allowing the Large Language Model (LLM) to leverage external services based on your chat interactions.
This guide will walk you through its capabilities, configuration, and best practices.
## Introduction to the Generic API Tool
**When to Use It:**
* Ideal for quickly integrating existing APIs where the interaction involves standard HTTP requests (GET, POST, PUT, DELETE).
* Suitable for fetching data to enrich answers (e.g., current weather, stock prices, product details).
* Useful for triggering simple actions in other systems (e.g., sending a notification, creating a basic task).
**Contrast with Custom Python Tools:**
* **API Tool:** Best for straightforward API calls. Configuration is done through the DocsGPT UI.
* **Custom Python Tools:** Preferable when you need complex logic before or after the API call, handle non-standard authentication (like complex OAuth flows), manage multi-step API interactions, or require intricate data processing not easily managed by the LLM alone. See [Creating a Custom Tool](/Tools/creating-a-tool) for more.
## Capabilities of the API Tool
**Supported HTTP Methods:** You can configure actions using standard HTTP methods such as:
* `GET`: To retrieve data.
* `POST`: To submit data to create a new resource.
* `PUT`: To update an existing resource.
* `DELETE`: To remove a resource.
**Request Configuration:**
* **Headers:** Define static or dynamic HTTP headers for authentication (e.g., API keys), content type specification, etc.
* **Query Parameters:** Specify URL query parameters, which can be static or dynamically filled by the LLM based on user input.
* **Request Body:** Define the structure of the request body (e.g., JSON), with fields that can be static or dynamically populated by the LLM.
**Response Handling:**
* The API Tool executes the request and receives the raw response from the API (typically JSON or plain text).
* This raw response is then passed back to the LLM.
* The LLM uses this response, along with the context of your query and the description of the API tool action, to formulate an answer or decide on follow-up actions. The API tool itself doesn't deeply parse or transform the response beyond basic content type detection (e.g., loading JSON into a parsable object).
## Configuring an API as a Tool
You can configure the API Tool through the DocsGPT user interface, found in **Settings -> Tools**. When you add or modify an API Tool, you'll define specific actions that DocsGPT can perform.
<Callout type="info">
The configuration involves defining how DocsGPT should call an API endpoint. Each configured API call essentially becomes a distinct "action" the LLM can choose to use.
</Callout>
Below is an example of how you might configure an API action, inspired by setting up a phone number validation service:
<Image
src="/toolIcons/api-tool-example.png"
alt="API Tool configuration example for phone validation"
width={800}
height={450}
style={{ margin: '1em auto', display: 'block', borderRadius: '8px' }}
/>
_Figure 1: Example configuration for an API Tool action to validate phone numbers._
**Defining an API Endpoint/Action:**
When you configure a new API action, you'll fill in the following fields:
- **`Name`:** A user-friendly name for this specific API action (e.g., "Phone-check" as in the image, or more specific like "ValidateUSPhoneNumber"). This helps in managing your tools.
- **`Description`:** This is a **critical field**. Provide a clear and concise description of what the API action does, what kind of input it expects (implicitly), and what kind of output it provides. The LLM uses this description to understand when and how to use this action.
- **`URL`:** The full endpoint URL for the API request.
- **`HTTP Method`:** Select the appropriate HTTP method (e.g., GET, POST) from a dropdown.
- **`Headers`:** You can add custom HTTP headers as key-value pairs (Name, Value). Indicate if the value should be `Filled by LLM` or is static. If filled by LLM, provide a `Description` for the LLM.
- **`Query Parameters`:** For `GET` requests or when parameters are sent in the URL.
* **`Name`:** The name of the query parameter (e.g., `api_key`, `phone`).
* **`Type`:** The data type of the parameter (e.g., `string`).
* **`Filled by LLM` (Checkbox):**
- **Unchecked (Static):** The `Value` you provide will be used for every call (e.g., for an `api_key` that doesn't change).
- **Checked (Dynamic):** The LLM will extract the appropriate value from the user's chat query based on the `Description` you provide for this parameter. The `Value` field is typically left empty or contains a placeholder if `Filled by LLM` is checked.
* `Description`: Context for the LLM if the parameter is to be filled dynamically, or for your own reference if static.
* `Value`: The static value if not filled by LLM.
- **`Request Body`:** Used to send data (commonly JSON) to the API. Similar to Query Parameters, you define fields with `Name`, `Type`, whether it's `Filled by LLM`, a `Description` for dynamic fields, and a static `Value` if applicable.
**Response Handling Guidance for the LLM:**
While the API Tool configuration UI doesn't have explicit fields for defining response parsing rules (like JSONPath extractors), you significantly influence how the LLM handles the response through:
* **Tool Action `Description`:** Clearly state what kind of information the API returns (e.g., "This API returns a JSON object with 'status' and 'location' fields for the phone number."). This helps the LLM know what to look for in the API's output.
* **Prompt Engineering:** For more complex scenarios, you might need to adjust your global or agent-specific prompts to guide DocsGPT on how to interpret and present information from API tool responses. See [Customising Prompts](/Guides/Customising-prompts).
## Using the Configured API Tool in Chat
Once an API action is configured and enabled, DocsGPT's LLM can decide to use it based on your natural language queries.
**Example (based on the phone validation tool in Figure 1):**
1. **User Query:** "Hey DocsGPT, can you check if +14155555555 is a valid phone number?"
2. **DocsGPT (LLM Orchestration):**
* The LLM analyzes the query.
* It matches the intent ("check if ... is a valid phone number") with the description of the "Phone-check" API action.
* It identifies `+14155555555` as the value for the `phone` parameter (which was marked as `Filled by LLM` with the description "Phone number to check").
* DocsGPT constructs the GET API request.
3. **API Tool Execution:**
* The API Tool makes the HTTP GET request.
* The external API (AbstractAPI) processes the request and returns a JSON response, e.g.:
```json
{
"phone": "+14155555555",
"valid": true,
"format": {
"international": "+1 415-555-5555",
"national": "(415) 555-5555"
},
"country": {
"code": "US",
"name": "United States",
"prefix": "+1"
},
"location": "California",
"type": "Landline"
}
```
4. **DocsGPT Response Formulation:**
* The API Tool passes this JSON response back to the LLM.
* The LLM, guided by the tool's description and the user's original query, extracts relevant information and formulates a user-friendly answer.
* **DocsGPT Chat Response:** "Yes, +14155555555 appears to be a valid landline phone number in California, United States."
## Advanced Tips and Best Practices
**Clear Description is the Key:** The LLM relies heavily on the `Description` field of the API action and its parameters. Make them unambiguous and action-oriented. Clearly state what the tool does and what kind of input it expects (even if implicitly through parameter descriptions).
**Iterative Testing:** After configuring an API tool, test it with various phrasings of user queries to ensure the LLM triggers it correctly and interprets the response as expected.
**Error Handling:**
* If an API call fails, the API Tool will return an error message and status code from the `requests` library or the API itself. The LLM may relay this error or try to explain it.
* Check DocsGPT's backend logs for more detailed error information if you encounter issues.
**Security Considerations:**
* **API Keys:** Be mindful of API keys and other sensitive credentials. The example image shows an API key directly in the configuration. For production or shared environments avoid exposing configurations with sensitive keys.
* **Rate Limits:** Be aware of the rate limits of the APIs you are integrating. Frequent calls from DocsGPT could exceed these limits.
* **Data Privacy:** Consider the data privacy implications of sending user query data to third-party APIs.
- **Idempotency:** For tools that modify data (POST, PUT, DELETE), be aware of whether the API operations are idempotent to avoid unintended consequences from repeated calls if the LLM retries an action.
## Limitations
While powerful, the Generic API Tool has some limitations:
- **Complex Authentication:** Advanced authentication flows like OAuth 2.0 (especially 3-legged OAuth requiring user redirection) or custom signature-based authentication often require custom Python tools.
- **Multi-Step API Interactions:** If a task requires multiple API calls that depend on each other (e.g., fetch a list, then for each item, fetch details), this kind of complex chaining and logic is better handled by a custom Python tool.
- **Complex Data Transformations:** If the API response needs significant transformation or processing before being useful to the LLM, a custom Python tool offers more flexibility.
- **Real-time Streaming (SSE, WebSockets):** The tool is designed for request-response interactions, not for maintaining persistent streaming connections.
For scenarios that exceed these limitations, developing a [Custom Python Tool](/Tools/creating-a-tool) is the recommended approach.

View File

@@ -0,0 +1,92 @@
---
title: Tools Basics - Enhancing DocsGPT Capabilities
description: Understand what DocsGPT Tools are, how they work, and explore the built-in tools available to extend DocsGPT's functionality.
---
import { Callout } from 'nextra/components';
import Image from 'next/image';
import { ToolCards } from '../../components/ToolCards';
# Understanding DocsGPT Tools
DocsGPT Tools are powerful extensions that significantly enhance the capabilities of your DocsGPT application.
They allow DocsGPT to move beyond its core function of retrieving information from your documents and enable it to perform actions,
interact with external data sources, and integrate with other services. You can find and configure available tools within
the "Tools" section of the DocsGPT application settings in the user interface.
## What are Tools?
- **Purpose:** The primary purpose of Tools is to bridge the gap between understanding a user's request (natural language processing by the LLM) and executing a tangible action. This could involve fetching live data from the web, sending notifications, running code snippets, querying databases, or interacting with third-party APIs.
- **LLM as an Orchestrator:** The Large Language Model (LLM) at the heart of DocsGPT is designed to act as an intelligent orchestrator. Based on your query and the declared capabilities of the available tools (defined in their metadata), the LLM decides if a tool is needed, which tool to use, and what parameters to pass to it.
- **Action-Oriented Interactions:** Tools enable more dynamic and action-oriented interactions. For example:
* *"What's the latest news on renewable energy?"* - This might trigger a web search tool to fetch current articles.
* *"Fetch the order status for customer ID 12345 from our database."* - This could use a database tool.
* *"Summarize the content of this webpage and send the summary to the #general channel on Telegram."* - This might involve a web scraping tool followed by a Telegram notification tool.
## Overview of Built-in Tools
DocsGPT includes a suite of pre-built tools designed to expand its capabilities out-of-the-box. Below is an overview of the currently available tools.
<ToolCards
items={[
{
title: 'API Tool',
link: '/Tools/api-tool',
description: 'A highly flexible tool that allows DocsGPT to interact with virtually any API without needing to write custom Python code.'
},
{
title: 'Brave Search Tool',
link: 'https://github.com/arc53/DocsGPT/blob/main/application/agents/tools/brave.py',
description: 'Enables DocsGPT to perform real-time web and image searches using the Brave Search API for up-to-date information.'
},
{
title: 'Cryptoprice Tool',
link: 'https://github.com/arc53/DocsGPT/blob/main/application/agents/tools/cryptoprice.py',
description: 'Fetches the current price of specified cryptocurrencies.'
},
{
title: 'Ntfy Tool',
link: 'https://github.com/arc53/DocsGPT/blob/main/application/agents/tools/ntfy.py',
description: 'Allows DocsGPT to send push notifications to Ntfy.sh channels, ideal for alerts and updates.'
},
{
title: 'PostgreSQL Tool',
link: 'https://github.com/arc53/DocsGPT/blob/main/application/agents/tools/postgres.py',
description: 'Provides capabilities to connect to a PostgreSQL database, execute SQL queries, and retrieve schema information.'
},
{
title: 'Read Webpage Tool', // Renamed from Scraper Tool
link: 'https://github.com/arc53/DocsGPT/blob/main/application/agents/tools/read_webpage.py',
description: 'Enables DocsGPT to fetch and extract (scrape) textual content from specified web page URLs.'
},
{
title: 'Telegram Tool',
link: 'https://github.com/arc53/DocsGPT/blob/main/application/agents/tools/telegram.py',
description: 'Allows DocsGPT to send messages or images to Telegram chats via a Telegram Bot.'
}
]}
/>
## Using Tools in DocsGPT (User Perspective)
Interacting with tools in DocsGPT is designed to be intuitive:
1. **Natural Language Interaction:** As a user, you typically interact with DocsGPT using natural language queries or commands. The LLM within DocsGPT analyzes your input to determine if a specific task can or should be handled by one of the available and configured tools.
2. **Configuration in UI:**
* Tools are generally managed and configured within the DocsGPT application's settings, found under a "Tools" section in the GUI.
* For tools that interact with external services (like Brave Search, Telegram, or any service via the API Tool), you might need to provide authentication credentials (e.g., API keys, tokens) or specific endpoint information during the tool's setup in the UI.
3. **Prompt Engineering for Tools:** While the LLM aims to intelligently use tools, for more complex or reliable agent-like behaviors, you might need to customize the system prompts. Modifying the prompt can guide the LLM on when and how to prioritize or chain tools to achieve specific outcomes, especially if you're building an agent designed to perform a certain sequence of actions every time. For more on this, see [Customising Prompts](/Guides/Customising-prompts).
## Advancing with Tools
Understanding the basics of DocsGPT Tools opens up many possibilities:
* **Leverage the API Tool:** For quick integrations with numerous external services, explore the [API Tool Detailed Guide](/Tools/api-tool).
* **Develop Custom Tools:** If you have specific needs not covered by built-in tools or the generic API tool, you can develop your own. See our guide on `[Developing Custom Tools](/Tools/creating-a-tool)` (placeholder for now).
* **Build AI Agents:** Tools are the fundamental building blocks for creating sophisticated AI agents within DocsGPT. Explore how these can be combined by looking into the `[Agents section/tab concept - link to be added once available]`.
By harnessing the power of Tools, you can transform DocsGPT into a more versatile and proactive assistant tailored to your unique workflows.

View File

@@ -0,0 +1,186 @@
---
title: 🛠️ Creating a Custom Tool
description: Learn how to create custom Python tools to extend DocsGPT's functionality and integrate with various services or perform specific actions.
---
import { Callout } from 'nextra/components';
import { Steps } from 'nextra/components';
# 🛠️ Creating a Custom Python Tool
This guide provides developers with a comprehensive, step-by-step approach to creating their own custom tools for DocsGPT. By developing custom tools, you can significantly extend DocsGPT's capabilities, enabling it to interact with new data sources, services, and perform specialized actions tailored to your unique needs.
## Introduction to Custom Tool Development
### Why Create Custom Tools?
While DocsGPT offers a range of built-in tools and a versatile API Tool, there are many scenarios where a custom Python tool is the best solution:
* **Integrating with Proprietary Systems:** Connect to internal APIs, databases, or services that are not publicly accessible or require complex authentication.
* **Adding Domain-Specific Functionalities:** Implement logic specific to your industry or use case that isn't covered by general-purpose tools.
* **Automating Unique Workflows:** Create tools that orchestrate multiple steps or interact with systems in a way unique to your operational needs.
* **Connecting to Any System with an Accessible Interface:** If you can interact with a system programmatically using Python (e.g., through libraries, SDKs, or direct HTTP requests), you can likely build a DocsGPT tool for it.
* **Complex Logic or Data Transformation:** When API interactions require intricate logic before sending a request or after receiving a response, or when data needs significant transformation that is difficult for an LLM to handle directly.
### Prerequisites
Before you begin, ensure you have:
* A solid understanding of Python programming.
* Familiarity with the DocsGPT project structure, particularly the `application/agents/tools/` directory where custom tools reside.
* Basic knowledge of how APIs work, as many tools involve interacting with external or internal APIs.
* Your DocsGPT development environment set up. If not, please refer to the [Setting Up a Development Environment](/Deploying/Development-Environment) guide.
## The Anatomy of a DocsGPT Tool
Custom tools in DocsGPT are Python classes that inherit from a base `Tool` class and implement specific methods to define their behavior, capabilities, and configuration needs.
The **foundation** for all custom tools is the abstract base class, located in `application/agents/tools/base.py`. Your custom tool class **must** inherit from this class.
### Essential Methods to Implement
Your custom tool class needs to implement the following methods:
1. **`__init__(self, config: dict)`**
- **Purpose:** The constructor for your tool. It's called when DocsGPT initializes the tool.
- **Usage:** This method is typically used to receive and store tool-specific configurations passed via the `config` dictionary. This dictionary is populated based on the tool's settings, often configured through the DocsGPT UI or environment variables. For example, you would store API keys, base URLs, or database connection strings here.
- **Example** (`brave.py`)**:**
``` python
class BraveSearchTool(Tool):
def __init__(self, config):
self.config = config
self.token = config.get("token", "") # API Key for Brave Search
self.base_url = "https://api.search.brave.com/res/v1"
```
2. **`execute_action(self, action_name: str, **kwargs) -> dict`**
- **Purpose:** This is the workhorse of your tool. The LLM, acting as an agent, calls this method when it decides to use one of the actions your tool provides.
- **Parameters:**
- `action_name` (str): A string specifying which of the tool's actions to run (e.g., "brave_web_search").
- `**kwargs` (dict): A dictionary containing the parameters for that specific action. These parameters are defined in the tool's metadata (`get_actions_metadata()`) and are extracted or inferred by the LLM from the user's query.
- **Return Value:** A dictionary containing the result of the action. It's good practice to include keys like:
- `status_code` (int): An HTTP-like status code (e.g., 200 for success, 500 for error).
- `message` (str): A human-readable message describing the outcome.
- `data` (any): The actual data payload returned by the action (if applicable).
- `error` (str): An error message if the action failed.
- **Example (`read_webpage.py`):**
``` python
def execute_action(self, action_name: str, **kwargs) -> str:
if action_name != "read_webpage":
return f"Error: Unknown action '{action_name}'. This tool only supports 'read_webpage'."
url = kwargs.get("url")
if not url:
return "Error: URL parameter is missing."
# ... (logic to fetch and parse webpage) ...
try:
# ...
return markdown_content
except Exception as e:
return f"Error processing URL {url}: {e}"
```
A more structured return:
``` python
# ... inside execute_action
try:
# ... logic ...
return {"status_code": 200, "message": "Webpage read successfully", "data": markdown_content}
except Exception as e:
return {"status_code": 500, "message": f"Error processing URL {url}", "error": str(e)}
```
3. **`get_actions_metadata(self) -> list`**
- **Purpose:** This method is **critical** for the LLM to understand what your tool can do, when to use it, and what parameters it needs. It effectively advertises your tool's capabilities.
- **Return Value:** A list of dictionaries. Each dictionary describes one distinct action the tool can perform and must follow a specific JSON schema structure.
- `name` (str): A unique and descriptive name for the action (e.g., `mytool_get_user_details`). It's a common convention to prefix with the tool name to avoid collisions.
- `description` (str): A clear, concise, and unambiguous description of what the action does. **Write this for the LLM.** The LLM uses this description to decide if this action is appropriate for a given user query.
- `parameters` (dict): A JSON Schema object defining the parameters that the action expects. This schema tells the LLM what arguments are needed, their types, and which are required.
- `type`: Should always be `"object"`.
- `properties`: A dictionary where each key is a parameter name, and the value is an object defining its `type` (e.g., "string", "integer", "boolean") and `description`.
- `required`: A list of strings, where each string is the name of a parameter that is mandatory for the action.
- **Example (`postgres.py` - partial):**
``` python
def get_actions_metadata(self):
return [
{
"name": "postgres_execute_sql",
"description": "Execute an SQL query against the PostgreSQL database...",
"parameters": {
"type": "object",
"properties": {
"sql_query": {
"type": "string",
"description": "The SQL query to execute.",
},
},
"required": ["sql_query"],
"additionalProperties": False, # Good practice to prevent unexpected params
},
},
# ... other actions like postgres_get_schema
]
```
4. **`get_config_requirements(self) -> dict`**
- **Purpose:** Defines the configuration parameters that your tool needs to function (e.g., API keys, specific base URLs, connection strings, default settings). This information can be used by the DocsGPT UI to dynamically render configuration fields for your tool or for validation.
- **Return Value:** A dictionary where keys are the configuration item names (which will be keys in the `config` dict passed to `__init__`) and values are dictionaries describing each requirement:
- `type` (str): The expected data type of the config value (e.g., "string", "boolean", "integer").
- `description` (str): A human-readable description of what this configuration item is for.
- `secret` (bool, optional): Set to `True` if the value is sensitive (e.g., an API key) and should be masked or handled specially in UIs. Defaults to `False`.
- **Example (`brave.py`):**
``` python
def get_config_requirements(self):
return {
"token": { # This 'token' will be a key in the config dict for __init__
"type": "string",
"description": "Brave Search API key for authentication",
"secret": True
},
}
```
## Tool Registration and Discovery
DocsGPT's ToolManager (located in application/agents/tools/tool_manager.py) automatically discovers and loads tools.
As long as your custom tool:
1. Is placed in a Python file within the `application/agents/tools/` directory (and the filename is not `base.py` or starts with `__`).
2. Correctly inherits from the `Tool` base class.
3. Implements all the abstract methods (`execute_action`, `get_actions_metadata`, `get_config_requirements`).
The `ToolManager` should be able to load it when DocsGPT starts.
## Configuration & Secrets Management
- **Configuration Source:** The `config` dictionary passed to your tool's `__init__` method is typically populated from settings defined in the DocsGPT UI (if available for the tool) or from environment variables/configuration files that DocsGPT loads (see [⚙️ App Configuration](/Deploying/DocsGPT-Settings)). The keys in this dictionary should match the names you define in `get_config_requirements()`.
- **Secrets:** Never hardcode secrets (like API keys or passwords) directly into your tool's Python code. Instead, define them as configuration requirements (using `secret: True` in `get_config_requirements()`) and let DocsGPT's configuration system inject them via the `config` dictionary at runtime. This ensures that secrets are managed securely and are not exposed in your codebase.
## Best Practices for Tool Development
- **Atomicity:** Design tool actions to be as atomic (single, well-defined purpose) as possible. This makes them easier for the LLM to understand and combine.
- **Clarity in Metadata:** Ensure action names and descriptions in `get_actions_metadata()` are extremely clear, specific, and unambiguous. This is the primary way the LLM understands your tool.
- **Robust Error Handling:** Implement comprehensive error handling within your `execute_action` logic (and the private methods it calls). Return informative error messages in the result dictionary so the LLM or user can understand what went wrong.
- **Security:**
- Be mindful of the security implications of your tool, especially if it interacts with sensitive systems or can execute arbitrary code/queries.
- Validate and sanitize any inputs, especially if they are used to construct database queries or shell commands, to prevent injection attacks.
- **Performance:** Consider the performance implications of your tool's actions. If an action is slow, it will impact the user experience. Optimize where possible.
## (Optional) Contributing Your Tool
If you develop a custom tool that you believe could be valuable to the broader DocsGPT community and is general-purpose:
1. Ensure it's well-documented (both in code and with clear metadata).
2. Make sure it adheres to the best practices outlined above.
3. Consider opening a Pull Request to the [DocsGPT GitHub repository](https://github.com/arc53/DocsGPT) with your new tool, including any necessary documentation updates.
By following this guide, you can create powerful custom tools that extend DocsGPT's capabilities to your specific operational environment.

View File

@@ -4,6 +4,8 @@
"quickstart": "Quickstart",
"Deploying": "Deploying",
"Models": "Models",
"Tools": "Tools",
"Agents": "Agents",
"Extensions": "Extensions",
"https://gptcloud.arc53.com/": {
"title": "API",

BIN
docs/public/jwt-input.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
docs/public/new-agent.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="1 6 38 28" xmlns="http://www.w3.org/2000/svg">
<path d="M3,33.5c-0.827,0-1.5-0.673-1.5-1.5V8c0-0.827,0.673-1.5,1.5-1.5h34c0.827,0,1.5,0.673,1.5,1.5v24 c0,0.827-0.673,1.5-1.5,1.5H3z" style="fill: rgb(7, 106, 255);"/>
<path d="M37,7c0.551,0,1,0.449,1,1v24c0,0.551-0.449,1-1,1H3c-0.551,0-1-0.449-1-1V8c0-0.551,0.449-1,1-1 H37 M37,6H3C1.895,6,1,6.895,1,8v24c0,1.105,0.895,2,2,2h34c1.105,0,2-0.895,2-2V8C39,6.895,38.105,6,37,6L37,6z" style="fill: rgb(7, 106, 255);"/>
<path d="M 19.296 13.226 C 20.066 13.06 21.108 12.955 22.147 12.955 C 23.772 12.955 25.153 13.185 26.047 14.038 C 26.88 14.766 27.255 15.931 27.255 17.118 C 27.255 18.638 26.798 19.718 26.07 20.489 C 25.196 21.426 23.801 21.842 22.656 21.842 C 22.47 21.842 22.302 21.842 22.115 21.821 L 22.115 27.045 L 19.297 27.045 L 19.297 13.226 L 19.296 13.226 Z M 22.114 19.616 C 22.259 19.637 22.405 19.637 22.571 19.637 C 23.945 19.637 24.55 18.657 24.55 17.347 C 24.55 16.119 24.049 15.162 22.78 15.162 C 22.532 15.162 22.281 15.203 22.114 15.266 L 22.114 19.616 Z M 29.158 12.955 L 31.976 12.955 L 31.976 27.045 L 29.158 27.045 L 29.158 12.955 Z M 15.001 27.045 L 17.887 27.045 L 14.91 12.955 L 11.342 12.955 L 8.024 27.045 L 10.91 27.045 L 11.524 24.227 L 14.408 24.227 L 15.001 27.045 Z M 13 15.547 L 13.068 15.547 C 13.205 16.467 13.409 17.888 13.568 18.745 L 14.021 21.409 L 11.942 21.409 L 12.457 18.746 C 12.614 17.93 12.841 16.488 13 15.547 Z" style="fill: rgb(255, 255, 255);"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 194.18 227.53"><defs><style>.cls-1{fill-rule:evenodd;fill:url(#linear-gradient);}.cls-2{fill:#fff;}</style><linearGradient id="linear-gradient" y1="116.23" x2="194.18" y2="116.23" gradientTransform="matrix(1, 0, 0, -1, 0, 230)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ff5601"/><stop offset="0.5" stop-color="#ff4000"/><stop offset="1" stop-color="#ff1f01"/></linearGradient></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M187.39,54.58l5.34-13.1s-6.8-7.27-15-15.52S152,22.56,152,22.56L132,0H62.14L42.23,22.56S24.76,17.71,16.51,26s-15,15.52-15,15.52L6.8,54.58,0,74s20,75.65,22.33,84.89c4.61,18.19,7.77,25.22,20.88,34.44S80.1,218.55,84,221s8.74,6.56,13.11,6.56,9.22-4.13,13.11-6.56,27.67-18.43,40.78-27.65,16.26-16.25,20.87-34.44C174.19,149.64,194.18,74,194.18,74Z"/><path class="cls-2" d="M121.85,41c2.91,0,24.51-4.12,24.51-4.12S172,67.8,172,74.41c0,5.47-2.21,7.6-4.8,10.12-.54.53-1.1,1.08-1.66,1.67l-19.2,20.37-.63.64c-1.91,1.92-4.73,4.76-2.74,9.47l.41,1c2.18,5.1,4.87,11.39,1.44,17.78-3.64,6.78-9.89,11.31-13.9,10.56s-13.41-5.66-16.87-7.9S99.6,126.8,99.6,123.35c0-2.89,7.88-7.68,11.71-10,.77-.47,1.37-.83,1.71-1.07l1.88-1.18c3.49-2.17,9.8-6.09,10-7.83.2-2.14.12-2.77-2.69-8.06-.6-1.13-1.3-2.33-2-3.58-2.68-4.61-5.69-9.78-5-13.48.75-4.18,7.3-6.57,12.85-8.6l2-.75,5.78-2.17c5.54-2.07,11.69-4.37,12.71-4.84,1.4-.65,1-1.27-3.22-1.67l-2.06-.21c-5.27-.56-15-1.59-19.71-.28l-3.06.84c-5.31,1.43-11.81,3.19-12.44,4.21-.11.18-.22.33-.32.47-.6.85-1,1.41-.32,5,.19,1.08.6,3.19,1.1,5.81,1.46,7.65,3.75,19.58,4,22.26,0,.38.08.74.13,1.09.36,3,.61,5-2.87,5.77l-.91.21c-3.92.9-9.67,2.22-11.75,2.22s-7.83-1.32-11.76-2.22l-.9-.21c-3.48-.79-3.23-2.78-2.87-5.77,0-.35.09-.71.13-1.09.29-2.68,2.58-14.65,4-22.3.5-2.59.9-4.7,1.1-5.77.66-3.6.27-4.16-.33-5-.1-.14-.21-.29-.32-.47-.62-1-7.13-2.78-12.43-4.21l-3.07-.84C66,58.31,56.25,59.34,51,59.9l-2.06.21c-4.26.4-4.62,1-3.22,1.67,1,.47,7.17,2.77,12.71,4.84l5.78,2.17,2,.75c5.55,2,12.1,4.42,12.85,8.6.67,3.7-2.34,8.87-5,13.48-.72,1.25-1.43,2.45-2,3.58-2.82,5.29-2.9,5.92-2.7,8.06.16,1.74,6.47,5.66,10,7.83.82.5,1.48.92,1.88,1.18s.94.6,1.71,1.06c3.83,2.33,11.71,7.13,11.71,10,0,3.45-11,12.49-14.42,14.73S67.3,145.24,63.29,146,53,142.2,49.39,135.42c-3.43-6.38-.74-12.68,1.44-17.78l.41-1c2-4.71-.83-7.55-2.74-9.47l-.63-.64L28.67,86.2c-.56-.59-1.12-1.14-1.66-1.67-2.59-2.52-4.79-4.65-4.79-10.12,0-6.61,25.6-37.53,25.6-37.53S69.42,41,72.33,41c2.33,0,6.82-1.55,11.49-3.16l3.56-1.21a34.33,34.33,0,0,1,9.71-2,34.33,34.33,0,0,1,9.71,2c1.18.39,2.37.81,3.56,1.21C115,39.45,119.52,41,121.85,41Z"/><path class="cls-2" d="M118.14,150.39c4.57,2.35,7.81,4,9,4.78,1.59,1,.62,2.86-.82,3.88s-20.85,16-22.73,17.69l-.76.68c-1.82,1.64-4.13,3.72-5.77,3.72s-4-2.08-5.77-3.72l-.76-.68c-1.88-1.66-21.28-16.67-22.73-17.69s-2.41-2.89-.82-3.88c1.23-.77,4.47-2.44,9-4.79l4.34-2.24c6.84-3.54,15.37-6.54,16.7-6.54s9.86,3,16.7,6.54Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 122.88 122.88"><path d="M17.89 0h88.9c8.85 0 16.1 7.24 16.1 16.1v90.68c0 8.85-7.24 16.1-16.1 16.1H16.1c-8.85 0-16.1-7.24-16.1-16.1v-88.9C0 8.05 8.05 0 17.89 0zm57.04 66.96l16.46 4.96c-1.1 4.61-2.84 8.47-5.23 11.56-2.38 3.1-5.32 5.43-8.85 7-3.52 1.57-8.01 2.36-13.45 2.36-6.62 0-12.01-.96-16.21-2.87-4.19-1.92-7.79-5.3-10.83-10.13-3.04-4.82-4.57-11.02-4.57-18.54 0-10.04 2.67-17.76 8.02-23.17 5.36-5.39 12.93-8.09 22.71-8.09 7.65 0 13.68 1.54 18.06 4.64 4.37 3.1 7.64 7.85 9.76 14.27l-16.55 3.66c-.58-1.84-1.19-3.18-1.82-4.03-1.06-1.43-2.35-2.53-3.86-3.3-1.53-.78-3.22-1.16-5.11-1.16-4.27 0-7.54 1.71-9.8 5.12-1.71 2.53-2.57 6.52-2.57 11.94 0 6.73 1.02 11.33 3.07 13.83 2.05 2.49 4.92 3.73 8.63 3.73 3.59 0 6.31-1 8.15-3.03 1.83-1.99 3.16-4.92 3.99-8.75z" fill-rule="evenodd" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 855 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="-8.78 0 70 70" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg">
<metadata>
<rdf:RDF>
<cc:Work>
<dc:subject>
Data
</dc:subject>
<dc:identifier>
sql-database-generic
</dc:identifier>
<dc:title>
SQL Database (Generic)
</dc:title>
<dc:format>
image/svg+xml
</dc:format>
<dc:publisher>
Amido Limited
</dc:publisher>
<dc:creator>
Richard Slater
</dc:creator>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
</cc:Work>
</rdf:RDF>
</metadata>
<path d="m 852.97077,1013.9363 c -6.55238,-0.4723 -13.02857,-2.1216 -17.00034,-4.3296 -2.26232,-1.2576 -3.98589,-2.8032 -4.66223,-4.1807 l -0.4024,-0.8196 0,-25.70807 0,-25.7081 0.31843,-0.6465 c 1.42297,-2.889 5.96432,-5.4935 12.30378,-7.0562 2.15195,-0.5305 5.2586,-1.0588 7.79304,-1.3252 2.58797,-0.2721 9.44765,-0.2307 12.02919,0.073 6.86123,0.8061 12.69967,2.6108 16.29768,5.0377 1.38756,0.9359 2.81137,2.4334 3.29371,3.4642 l 0.41358,0.8838 -0.0354,25.6303 -0.0354,25.63047 -0.33195,0.6744 c -0.18257,0.3709 -0.73406,1.1007 -1.22553,1.6216 -2.99181,3.1715 -9.40919,5.5176 -17.8267,6.5172 -1.71567,0.2038 -9.16916,0.3686 -10.92937,0.2417 z m 12.07501,-22.02839 c -0.0252,-0.0657 -1.00472,-0.93831 -2.17671,-1.93922 -1.17199,-1.00091 -2.18138,-1.86687 -2.24309,-1.92436 -0.0617,-0.0575 0.15481,-0.26106 0.48117,-0.45237 0.32635,-0.19131 0.95163,-0.7235 1.3895,-1.18265 1.2805,-1.34272 1.88466,-3.00131 1.88466,-5.17388 0,-2.1388 -0.65162,-3.8645 -1.95671,-5.1818 -1.31533,-1.3278 -2.82554,-1.8983 -5.02486,-1.8983 -3.39007,0 -5.99368,1.9781 -6.82468,5.1851 -0.28586,1.1031 -0.28432,3.33211 0.003,4.31023 0.74941,2.55136 2.79044,4.40434 5.33062,4.83946 0.8596,0.14724 0.97605,0.21071 1.5621,0.85144 0.34829,0.38078 1.06301,1.14085 1.58827,1.68904 l 0.95501,0.9967 2.53878,0 c 1.39633,0 2.51816,-0.0537 2.49296,-0.11939 z m -8.70653,-7.10848 c -0.61119,-0.31868 -0.84225,-0.56599 -1.19079,-1.27453 -0.26919,-0.54724 -0.31522,-0.85851 -0.31824,-2.15197 -0.003,-1.3143 0.0388,-1.5983 0.31987,-2.169 0.45985,-0.9339 1.09355,-1.376 2.07384,-1.4469 1.36454,-0.099 2.15217,0.5707 2.56498,2.1801 0.50612,1.97321 -0.0504,4.07107 -1.26471,4.76729 -0.63707,0.36527 -1.58737,0.40659 -2.18495,0.095 z m -11.25315,3.66269 c 2.66179,-0.5048 4.1728,-2.0528 4.1728,-4.27495 0,-1.97137 -0.97548,-3.12004 -3.6716,-4.32364 -1.54338,-0.689 -2.10241,-1.1215 -2.10241,-1.6268 0,-0.4188 0.53052,-0.8777 1.14813,-0.993 0.60302,-0.1126 2.20237,0.1652 3.14683,0.5467 l 0.79167,0.3198 0,-1.7524 0,-1.7525 -0.85923,-0.1906 c -0.53103,-0.1178 -1.64689,-0.1885 -2.92137,-0.1849 -1.80528,0 -2.15881,0.044 -2.83818,0.3138 -1.98445,0.7878 -2.92613,2.1298 -2.91107,4.1485 0.0141,1.8898 1.01108,3.06864 3.49227,4.12912 1.46399,0.62572 2.05076,1.10218 2.05076,1.66522 0,1.1965 -1.99362,1.34375 -4.10437,0.30315 -0.57805,-0.28498 -1.09739,-0.54137 -1.1541,-0.56976 -0.0567,-0.0284 -0.10311,0.79023 -0.10311,1.81917 0,1.86239 0.002,1.87137 0.33919,1.99974 1.26979,0.48278 4.07626,0.69787 5.52379,0.42335 z m 30.4308,-1.72766 0,-1.58098 -2.40584,0 -2.40583,0 0,-5.43035 0,-5.4303 -2.13089,0 -2.13088,0 0,7.0113 0,7.01131 4.53672,0 4.53672,0 0,-1.58098 z m -14.84745,-27.70503 c 4.23447,-0.2937 7.4086,-0.8482 10.20178,-1.7821 2.78264,-0.9304 4.42643,-2.0562 4.79413,-3.2834 0.14166,-0.4729 0.13146,-0.6523 -0.0665,-1.1708 -0.88775,-2.3245 -5.84694,-4.1104 -13.42493,-4.8345 -3.24154,-0.3098 -9.13671,-0.2094 -12.22745,0.2081 -4.71604,0.6372 -8.54333,1.8208 -10.2451,3.1683 -3.44251,2.726 0.19793,5.7242 8.66397,7.1354 3.67084,0.6119 8.42674,0.828 12.30414,0.559 z" fill="#00bcf2" transform="translate(-830.906 -943.981)"/>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-155.5t86-127Q252-817 325-848.5T480-880q83 0 155.5 31.5t127 86q54.5 54.5 86 127T880-480q0 82-31.5 155t-86 127.5q-54.5 54.5-127 86T480-80Zm0-82q26-36 45-75t31-83H404q12 44 31 83t45 75Zm-104-16q-18-33-31.5-68.5T322-320H204q29 50 72.5 87t99.5 55Zm208 0q56-18 99.5-55t72.5-87H638q-9 38-22.5 73.5T584-178ZM170-400h136q-3-20-4.5-39.5T300-480q0-21 1.5-40.5T306-560H170q-5 20-7.5 39.5T160-480q0 21 2.5 40.5T170-400Zm216 0h188q3-20 4.5-39.5T580-480q0-21-1.5-40.5T574-560H386q-3 20-4.5 39.5T380-480q0 21 1.5 40.5T386-400Zm268 0h136q5-20 7.5-39.5T800-480q0-21-2.5-40.5T790-560H654q3 20 4.5 39.5T660-480q0 21-1.5 40.5T654-400Zm-16-240h118q-29-50-72.5-87T584-782q18 33 31.5 68.5T638-640Zm-234 0h152q-12-44-31-83t-45-75q-26 36-45 75t-31 83Zm-200 0h118q9-38 22.5-73.5T376-782q-56 18-99.5 55T204-640Z"/></svg>

After

Width:  |  Height:  |  Size: 976 B

View File

@@ -0,0 +1,10 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 0.5C8.81812 0.5 5.76375 1.76506 3.51562 4.01469C1.2652 6.26522 0.000643966 9.31734 0 12.5C0 15.6813 1.26562 18.7357 3.51562 20.9853C5.76375 23.2349 8.81812 24.5 12 24.5C15.1819 24.5 18.2362 23.2349 20.4844 20.9853C22.7344 18.7357 24 15.6813 24 12.5C24 9.31869 22.7344 6.26431 20.4844 4.01469C18.2362 1.76506 15.1819 0.5 12 0.5Z" fill="url(#paint0_linear_5586_9958)"/>
<path d="M5.43282 12.373C8.93157 10.849 11.2641 9.8443 12.4303 9.3588C15.7641 7.97261 16.4559 7.73186 16.9078 7.7237C17.0072 7.72211 17.2284 7.74667 17.3728 7.86339C17.4928 7.96183 17.5266 8.09495 17.5434 8.18842C17.5584 8.2818 17.5791 8.49461 17.5622 8.66074C17.3822 10.5582 16.6003 15.1629 16.2028 17.2882C16.0359 18.1874 15.7041 18.4889 15.3834 18.5184C14.6859 18.5825 14.1572 18.0579 13.4822 17.6155C12.4266 16.9231 11.8303 16.4922 10.8047 15.8167C9.6197 15.0359 10.3884 14.6067 11.0634 13.9055C11.2397 13.7219 14.3109 10.9291 14.3691 10.6758C14.3766 10.6441 14.3841 10.526 14.3128 10.4637C14.2434 10.4013 14.1403 10.4227 14.0653 10.4395C13.9584 10.4635 12.2728 11.5788 9.00282 13.7851C8.52469 14.114 8.09157 14.2743 7.70157 14.2659C7.27407 14.2567 6.44907 14.0236 5.83595 13.8245C5.08595 13.5802 4.48782 13.451 4.54032 13.036C4.56657 12.82 4.8647 12.599 5.43282 12.373Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_5586_9958" x1="1200" y1="0.5" x2="1200" y2="2400.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#2AABEE"/>
<stop offset="1" stop-color="#229ED9"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "docsgpt",
"version": "0.5.0",
"version": "0.5.1",
"private": false,
"description": "DocsGPT 🦖 is an innovative open-source tool designed to simplify the retrieval of information from project documentation using advanced GPT models 🤖.",
"source": "./src/index.html",

View File

@@ -4,19 +4,19 @@ import { WidgetCore } from './DocsGPTWidget';
import { SearchBarProps } from '@/types';
import { getSearchResults } from '../requests/searchAPI';
import { Result } from '@/types';
import MarkdownIt from 'markdown-it';
import { getOS, processMarkdownString } from '../utils/helper';
import DOMPurify from 'dompurify';
import {
CodeIcon,
import {
CodeIcon,
TextAlignLeftIcon,
HeadingIcon,
ReaderIcon,
ListBulletIcon,
QuoteIcon
ReaderIcon,
ListBulletIcon,
QuoteIcon
} from '@radix-ui/react-icons';
const themes = {
dark: {
name: 'dark',
bg: '#202124',
text: '#EDEDED',
primary: {
@@ -29,6 +29,7 @@ const themes = {
}
},
light: {
name: 'light',
bg: '#EAEAEA',
text: '#171717',
primary: {
@@ -44,15 +45,16 @@ const themes = {
const GlobalStyle = createGlobalStyle`
.highlight {
color:#007EE6;
color: ${props => props.theme.name === 'dark' ? '#4B9EFF' : '#0066CC'};
font-weight: 500;
}
`;
const loadGeistFont = () => {
const link = document.createElement('link');
link.href = 'https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap';
link.rel = 'stylesheet';
document.head.appendChild(link);
const link = document.createElement('link');
link.href = 'https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap';
link.rel = 'stylesheet';
document.head.appendChild(link);
};
const Main = styled.div`
@@ -81,12 +83,27 @@ const Container = styled.div`
position: relative;
display: inline-block;
`
const SearchOverlay = styled.div`
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #0000001A;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
z-index: 99;
`;
const SearchResults = styled.div`
position: fixed;
display: flex;
flex-direction: column;
background-color: ${props => props.theme.primary.bg};
border: 1px solid ${props => props.theme.bg};
background-color: ${props => props.theme.name === 'dark' ?
'rgba(0, 0, 0, 0.15)' :
'rgba(255, 255, 255, 0.4)'};
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 15px;
padding: 8px 0px 8px 0px;
width: 792px;
@@ -97,8 +114,12 @@ const SearchResults = styled.div`
top: 50%;
transform: translate(-50%, -50%);
color: ${props => props.theme.primary.text};
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(16px);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
backdrop-filter: blur(82px);
-webkit-backdrop-filter: blur(82px);
border-radius: 10px;
box-sizing: border-box;
@media only screen and (max-width: 768px) {
@@ -142,6 +163,33 @@ const ContentWrapper = styled.div`
flex-direction: column;
gap: 12px;
`;
const ResultWrapper = styled.div`
display: flex;
align-items: flex-start;
width: 100%;
box-sizing: border-box;
padding: 8px 16px;
cursor: pointer;
background-color: transparent;
font-family: 'Geist', sans-serif;
border-radius: 8px;
word-wrap: break-word;
overflow-wrap: break-word;
word-break: break-word;
white-space: normal;
overflow: hidden;
text-overflow: ellipsis;
&:hover {
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
`;
const Content = styled.div`
display: flex;
margin-left: 8px;
@@ -151,9 +199,10 @@ const Content = styled.div`
font-size: 15px;
color: ${props => props.theme.primary.text};
line-height: 1.6;
border-left: 2px solid #585858;
border-left: 2px solid ${props => props.theme.primary.text}CC;
overflow: hidden;
`
`;
const ContentSegment = styled.div`
display: flex;
align-items: flex-start;
@@ -165,80 +214,6 @@ const ContentSegment = styled.div`
text-overflow: ellipsis;
`
const ResultWrapper = styled.div`
display: flex;
align-items: flex-start;
width: 100%;
box-sizing: border-box;
padding: 8px 16px;
cursor: pointer;
background-color: ${props => props.theme.primary.bg};
font-family: 'Geist', sans-serif;
transition: background-color 0.2s;
border-radius: 8px;
word-wrap: break-word;
overflow-wrap: break-word;
word-break: break-word;
white-space: normal;
overflow: hidden;
text-overflow: ellipsis;
&:hover {
background-color: ${props => props.theme.bg};
}
`
const Markdown = styled.div`
line-height:18px;
font-size: 11px;
white-space: pre-wrap;
pre {
padding: 8px;
width: 90%;
font-size: 11px;
border-radius: 6px;
overflow-x: auto;
background-color: #1B1C1F;
color: #fff ;
}
h1,h2 {
font-size: 14px;
font-weight: 600;
color: ${(props) => props.theme.text};
opacity: 0.8;
}
h3 {
font-size: 12px;
}
p {
margin: 0px;
line-height: 1.35rem;
font-size: 11px;
}
code:not(pre code) {
border-radius: 6px;
padding: 2px 2px;
margin: 2px;
font-size: 9px;
display: inline;
background-color: #646464;
color: #fff ;
}
img{
max-width: 50%;
}
code {
overflow-x: auto;
}
a{
color: #007ee6;
}
`
const Toolkit = styled.kbd`
position: absolute;
right: 4px;
@@ -259,8 +234,8 @@ const Toolkit = styled.kbd`
`
const Loader = styled.div`
margin: 2rem auto;
border: 4px solid ${props => props.theme.secondary.text};
border-top: 4px solid ${props => props.theme.primary.bg};
border: 4px solid ${props => props.theme.name === 'dark' ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.1)'};
border-top: 4px solid ${props => props.theme.name === 'dark' ? '#FFFFFF' : props.theme.primary.bg};
border-radius: 50%;
width: 12px;
height: 12px;
@@ -280,7 +255,8 @@ const NoResults = styled.div`
margin-top: 2rem;
text-align: center;
font-size: 14px;
color: #888;
color: ${props => props.theme.name === 'dark' ? '#E0E0E0' : '#505050'};
font-weight: 500;
`;
const AskAIButton = styled.button`
display: flex;
@@ -293,25 +269,35 @@ const AskAIButton = styled.button`
height: 50px;
padding: 8px 24px;
border: none;
border-radius: 6px;
background-color: ${props => props.theme.bg};
border-radius: 8px;
color: ${props => props.theme.text};
cursor: pointer;
transition: background-color 0.2s, box-shadow 0.2s;
font-size: 16px;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
background-color: ${props => props.theme.name === 'dark' ?
'rgba(255, 255, 255, 0.05)' :
'rgba(0, 0, 0, 0.03)'};
&:hover {
opacity: 0.8;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
background-color: ${props => props.theme.name === 'dark' ?
'rgba(255, 255, 255, 0.1)' :
'rgba(0, 0, 0, 0.06)'};
}
`
`;
const SearchHeader = styled.div`
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid ${props => props.theme.bg};
`
border-bottom: 1px solid ${props => props.theme.name === 'dark' ? '#FFFFFF24' : 'rgba(0, 0, 0, 0.14)'};
`;
const TextField = styled.input`
width: calc(100% - 32px);
@@ -327,8 +313,16 @@ const TextField = styled.input`
&:focus {
border-color: none;
}
&::placeholder {
color: ${props => props.theme.name === 'dark' ? 'rgba(255, 255, 255, 0.6)' : 'rgba(0, 0, 0, 0.5)'} !important;
opacity: 100%; /* Force opacity to ensure placeholder is visible */
font-weight: 500;
}
`
const EscapeInstruction = styled.kbd`
display: flex;
align-items: center;
@@ -337,17 +331,21 @@ const EscapeInstruction = styled.kbd`
padding: 4px 8px;
border-radius: 4px;
background-color: transparent;
border: 1px solid ${props => props.theme.secondary.text};
color: ${props => props.theme.text};
border: 1px solid ${props => props.theme.name === 'dark' ?
'rgba(237, 237, 237, 0.6)' :
'rgba(23, 23, 23, 0.6)'};
color: ${props => props.theme.name === 'dark' ? '#EDEDED' : '#171717'};
font-size: 12px;
font-family: 'Geist', sans-serif;
white-space: nowrap;
cursor: pointer;
width: fit-content;
&:hover {
background-color: rgba(255, 255, 255, 0.1);
}
`
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
`;
export const SearchBar = ({
apiKey = "74039c6d-bff7-44ce-ae55-2973cbf13837",
apiHost = "https://gptcloud.arc53.com",
@@ -367,7 +365,7 @@ export const SearchBar = ({
const abortControllerRef = React.useRef<AbortController | null>(null);
const browserOS = getOS();
const isTouch = 'ontouchstart' in window;
const getKeyboardInstruction = () => {
if (isResultVisible) return "Enter";
return browserOS === 'mac' ? '⌘ + K' : 'Ctrl + K';
@@ -394,7 +392,7 @@ export const SearchBar = ({
}
};
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('keydown', handleKeyDown);
return () => {
@@ -404,33 +402,34 @@ export const SearchBar = ({
}, []);
React.useEffect(() => {
if (!input) {
setResults([]);
return;
}
setLoading(true);
if (debounceTimeout.current) {
clearTimeout(debounceTimeout.current);
}
if (!input) {
setResults([]);
setLoading(false);
return;
}
setLoading(true);
if (debounceTimeout.current) {
clearTimeout(debounceTimeout.current);
}
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
const abortController = new AbortController();
abortControllerRef.current = abortController;
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
const abortController = new AbortController();
abortControllerRef.current = abortController;
debounceTimeout.current = setTimeout(() => {
getSearchResults(input, apiKey, apiHost, abortController.signal)
.then((data) => setResults(data))
.catch((err) => !abortController.signal.aborted && console.log(err))
.finally(() => setLoading(false));
}, 500);
debounceTimeout.current = setTimeout(() => {
getSearchResults(input, apiKey, apiHost, abortController.signal)
.then((data) => setResults(data))
.catch((err) => !abortController.signal.aborted && console.log(err))
.finally(() => setLoading(false));
}, 500);
return () => {
abortController.abort();
clearTimeout(debounceTimeout.current ?? undefined);
};
}, [input])
return () => {
abortController.abort();
clearTimeout(debounceTimeout.current ?? undefined);
};
}, [input])
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
@@ -462,6 +461,8 @@ export const SearchBar = ({
</SearchButton>
{
isResultVisible && (
<>
<SearchOverlay onClick={() => setIsResultVisible(false)} />
<SearchResults>
<SearchHeader>
<TextField
@@ -477,8 +478,8 @@ export const SearchBar = ({
</EscapeInstruction>
</SearchHeader>
<AskAIButton onClick={openWidget}>
<img
src="https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png"
<img
src="https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png"
alt="DocsGPT"
width={24}
height={24}
@@ -539,6 +540,7 @@ export const SearchBar = ({
)}
</SearchResultsScroll>
</SearchResults>
</>
)
}
{

File diff suppressed because it is too large Load Diff

View File

@@ -34,9 +34,9 @@
"react-helmet": "^6.1.0",
"react-i18next": "^15.4.0",
"react-markdown": "^9.0.1",
"react-redux": "^8.0.5",
"react-router-dom": "^7.1.1",
"react-syntax-highlighter": "^15.5.0",
"react-redux": "^9.2.0",
"react-router-dom": "^7.6.1",
"react-syntax-highlighter": "^15.6.1",
"rehype-katex": "^7.0.1",
"remark-gfm": "^4.0.0",
"remark-math": "^6.0.0"
@@ -52,13 +52,13 @@
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.13",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
"eslint-config-prettier": "^10.1.5",
"eslint-config-standard-with-typescript": "^34.0.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-n": "^15.7.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-promise": "^6.6.0",
"eslint-plugin-react": "^7.37.3",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-unused-imports": "^4.1.4",
"husky": "^8.0.0",
"lint-staged": "^15.3.0",
@@ -66,8 +66,8 @@
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.2",
"vite": "^5.4.14",
"vite-plugin-svgr": "^4.2.0"
"typescript": "^5.8.3",
"vite": "^6.3.5",
"vite-plugin-svgr": "^4.3.0"
}
}

View File

@@ -1,99 +0,0 @@
//TODO - Add hyperlinks to text
//TODO - Styling
import DocsGPT3 from './assets/cute_docsgpt3.svg';
export default function About() {
return (
<div className="mx-5 grid min-h-screen md:mx-36">
<article className="place-items-left mx-auto my-auto flex w-full max-w-6xl flex-col gap-4 rounded-3xl bg-gray-100 p-6 text-jet dark:bg-gun-metal dark:text-bright-gray lg:p-6 xl:p-10">
<div className="flex items-center">
<p className="mr-2 text-3xl">About DocsGPT</p>
<img className="h14 mb-2" src={DocsGPT3} alt="DocsGPT" />
</div>
<p className="mt-4">
Find the information in your documentation through AI-powered
<a
className="text-blue-500"
href="https://github.com/arc53/DocsGPT"
target="_blank"
rel="noreferrer"
>
{' '}
open-source{' '}
</a>
chatbot. Powered by GPT-3, Faiss and LangChain.
</p>
<div>
<p>
If you want to add your own documentation, please follow the
instruction below:
</p>
<p className="ml-2 mt-4">
1. Navigate to{' '}
<span className="bg-gray-200 italic dark:bg-outer-space">
{' '}
/application
</span>{' '}
folder
</p>
<p className="ml-2 mt-4">
2. Install dependencies from{' '}
<span className="bg-gray-200 italic dark:bg-outer-space">
pip install -r requirements.txt
</span>
</p>
<p className="ml-2 mt-4">
3. Prepare a{' '}
<span className="bg-gray-200 italic dark:bg-outer-space">.env</span>{' '}
file. Copy{' '}
<span className="bg-gray-200 italic dark:bg-outer-space">
.env_sample
</span>{' '}
and create{' '}
<span className="bg-gray-200 italic dark:bg-outer-space">.env</span>{' '}
with your OpenAI API token
</p>
<p className="ml-2 mt-4">
4. Run the app with{' '}
<span className="bg-gray-200 italic dark:bg-outer-space">
python app.py
</span>
</p>
</div>
<p>
Currently It uses{' '}
<span className="font-medium text-blue-950">DocsGPT</span>{' '}
documentation, so it will respond to information relevant to{' '}
<span className="font-medium text-blue-950">DocsGPT</span>. If you
want to train it on different documentation - please follow
<a
className="text-blue-500"
href="https://github.com/arc53/DocsGPT/wiki/How-to-train-on-other-documentation"
target="_blank"
rel="noreferrer"
>
{' '}
this guide
</a>
.
</p>
<p className="mt-4 text-left">
If you want to launch it on your own server - follow
<a
className="text-blue-500"
href="https://github.com/arc53/DocsGPT/wiki/Hosting-the-app"
target="_blank"
rel="noreferrer"
>
{' '}
this guide
</a>
.
</p>
</article>
</div>
);
}

View File

@@ -3,7 +3,9 @@ import './locale/i18n';
import { useState } from 'react';
import { Outlet, Route, Routes } from 'react-router-dom';
import About from './About';
import Agents from './agents';
import SharedAgentGate from './agents/SharedAgentGate';
import ActionButtons from './components/ActionButtons';
import Spinner from './components/Spinner';
import Conversation from './conversation/Conversation';
import { SharedConversation } from './conversation/SharedConversation';
@@ -12,7 +14,6 @@ import useTokenAuth from './hooks/useTokenAuth';
import Navigation from './Navigation';
import PageNotFound from './PageNotFound';
import Setting from './settings';
import Agents from './agents';
function AuthWrapper({ children }: { children: React.ReactNode }) {
const { isAuthLoading } = useTokenAuth();
@@ -28,17 +29,18 @@ function AuthWrapper({ children }: { children: React.ReactNode }) {
}
function MainLayout() {
const { isMobile } = useMediaQuery();
const [navOpen, setNavOpen] = useState(!isMobile);
const { isMobile, isTablet } = useMediaQuery();
const [navOpen, setNavOpen] = useState(!(isMobile || isTablet));
return (
<div className="relative h-screen overflow-auto dark:bg-raisin-black">
<div className="relative h-screen overflow-hidden dark:bg-raisin-black">
<Navigation navOpen={navOpen} setNavOpen={setNavOpen} />
<ActionButtons showNewChat={true} showShare={true} />
<div
className={`h-[calc(100dvh-64px)] md:h-screen ${
!isMobile
? `ml-0 ${!navOpen ? 'md:mx-auto lg:mx-auto' : 'md:ml-72'}`
: 'ml-0 md:ml-16'
className={`h-[calc(100dvh-64px)] overflow-auto lg:h-screen ${
!(isMobile || isTablet)
? `ml-0 ${!navOpen ? 'lg:mx-auto' : 'lg:ml-72'}`
: 'ml-0 lg:ml-16'
}`}
>
<Outlet />
@@ -46,14 +48,13 @@ function MainLayout() {
</div>
);
}
export default function App() {
const [, , componentMounted] = useDarkTheme();
if (!componentMounted) {
return <div />;
}
return (
<div className="relative h-full overflow-auto">
<div className="relative h-full overflow-hidden">
<Routes>
<Route
element={
@@ -63,11 +64,11 @@ export default function App() {
}
>
<Route index element={<Conversation />} />
<Route path="/about" element={<About />} />
<Route path="/settings/*" element={<Setting />} />
<Route path="/agents/*" element={<Agents />} />
</Route>
<Route path="/share/:identifier" element={<SharedConversation />} />
<Route path="/shared/agent/:agentId" element={<SharedAgentGate />} />
<Route path="/*" element={<PageNotFound />} />
</Routes>
</div>

View File

@@ -42,11 +42,13 @@ import {
selectConversations,
selectModalStateDeleteConv,
selectSelectedAgent,
selectSharedAgents,
selectToken,
setAgents,
setConversations,
setModalStateDeleteConv,
setSelectedAgent,
setSharedAgents,
} from './preferences/preferenceSlice';
import Upload from './upload/Upload';
@@ -67,9 +69,10 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
const conversationId = useSelector(selectConversationId);
const modalStateDeleteConv = useSelector(selectModalStateDeleteConv);
const agents = useSelector(selectAgents);
const sharedAgents = useSelector(selectSharedAgents);
const selectedAgent = useSelector(selectSelectedAgent);
const { isMobile } = useMediaQuery();
const { isMobile, isTablet } = useMediaQuery();
const [isDarkTheme] = useDarkTheme();
const { showTokenModal, handleTokenSubmit } = useTokenAuth();
@@ -129,7 +132,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
useEffect(() => {
fetchRecentAgents();
}, [agents, token, dispatch]);
}, [agents, sharedAgents, token, dispatch]);
useEffect(() => {
if (!conversations?.data) fetchConversations();
@@ -160,59 +163,72 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
const handleAgentClick = (agent: Agent) => {
resetConversation();
dispatch(setSelectedAgent(agent));
if (isMobile) setNavOpen(!navOpen);
if (isMobile || isTablet) setNavOpen(!navOpen);
navigate('/');
};
const handleTogglePin = (agent: Agent) => {
userService.togglePinAgent(agent.id ?? '', token).then((response) => {
if (response.ok) {
const updatedAgents = agents?.map((a) =>
a.id === agent.id ? { ...a, pinned: !a.pinned } : a,
);
dispatch(setAgents(updatedAgents));
const updatePinnedStatus = (a: Agent) =>
a.id === agent.id ? { ...a, pinned: !a.pinned } : a;
dispatch(setAgents(agents?.map(updatePinnedStatus)));
dispatch(setSharedAgents(sharedAgents?.map(updatePinnedStatus)));
}
});
};
const handleConversationClick = (index: string) => {
dispatch(setSelectedAgent(null));
conversationService
.getConversation(index, token)
.then((response) => response.json())
.then((data) => {
dispatch(setConversation(data.queries));
dispatch(
updateConversationId({
query: { conversationId: index },
}),
const handleConversationClick = async (index: string) => {
try {
dispatch(setSelectedAgent(null));
const response = await conversationService.getConversation(index, token);
if (!response.ok) {
navigate('/');
return;
}
const data = await response.json();
if (!data) return;
dispatch(setConversation(data.queries));
dispatch(updateConversationId({ query: { conversationId: index } }));
if (!data.agent_id) {
navigate('/');
return;
}
let agent: Agent;
if (data.is_shared_usage) {
const sharedResponse = await userService.getSharedAgent(
data.shared_token,
token,
);
if (data.agent_id) {
if (data.is_shared_usage) {
userService
.getSharedAgent(data.shared_token, token)
.then((response) => {
if (response.ok) {
response.json().then((agent: Agent) => {
navigate(`/agents/shared/${agent.shared_token}`);
});
}
});
} else {
userService.getAgent(data.agent_id, token).then((response) => {
if (response.ok) {
response.json().then((agent: Agent) => {
navigate('/');
dispatch(setSelectedAgent(agent));
});
}
});
}
} else {
if (!sharedResponse.ok) {
navigate('/');
dispatch(setSelectedAgent(null));
return;
}
});
agent = await sharedResponse.json();
navigate(`/agents/shared/${agent.shared_token}`);
} else {
const agentResponse = await userService.getAgent(data.agent_id, token);
if (!agentResponse.ok) {
navigate('/');
return;
}
agent = await agentResponse.json();
if (agent.shared_token) {
navigate(`/agents/shared/${agent.shared_token}`);
} else {
await Promise.resolve(dispatch(setSelectedAgent(agent)));
navigate('/');
}
}
} catch (error) {
console.error('Error handling conversation click:', error);
navigate('/');
}
};
const resetConversation = () => {
@@ -251,14 +267,14 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
}
useEffect(() => {
setNavOpen(!isMobile);
}, [isMobile]);
setNavOpen(!(isMobile || isTablet));
}, [isMobile, isTablet]);
useDefaultDocument();
return (
<>
{!navOpen && (
<div className="duration-25 absolute left-3 top-3 z-20 hidden transition-all md:block">
<div className="duration-25 absolute left-3 top-3 z-20 hidden transition-all lg:block">
<div className="flex items-center gap-3">
<button
onClick={() => {
@@ -332,7 +348,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
<NavLink
to={'/'}
onClick={() => {
if (isMobile) {
if (isMobile || isTablet) {
setNavOpen(!navOpen);
}
resetConversation();
@@ -395,7 +411,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
</p>
</div>
<div
className={`${isMobile ? 'flex' : 'invisible flex group-hover:visible'} items-center px-3`}
className={`${isMobile || isTablet ? 'flex' : 'invisible flex group-hover:visible'} items-center px-3`}
>
<button
className="rounded-full hover:opacity-75"
@@ -417,6 +433,9 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
className="mx-4 my-auto mt-2 flex h-9 cursor-pointer items-center gap-2 rounded-3xl pl-4 hover:bg-bright-gray dark:hover:bg-dark-charcoal"
onClick={() => {
dispatch(setSelectedAgent(null));
if (isMobile || isTablet) {
setNavOpen(false);
}
navigate('/agents');
}}
>
@@ -428,7 +447,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
/>
</div>
<p className="overflow-hidden overflow-ellipsis whitespace-nowrap text-sm leading-6 text-eerie-black dark:text-bright-gray">
Manage Agents
{t('manageAgents')}
</p>
</div>
</div>
@@ -436,7 +455,13 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
) : (
<div
className="mx-4 my-auto mt-2 flex h-9 cursor-pointer items-center gap-2 rounded-3xl pl-4 hover:bg-bright-gray dark:hover:bg-dark-charcoal"
onClick={() => navigate('/agents')}
onClick={() => {
if (isMobile || isTablet) {
setNavOpen(false);
}
dispatch(setSelectedAgent(null));
navigate('/agents');
}}
>
<div className="flex w-6 justify-center">
<img
@@ -446,7 +471,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
/>
</div>
<p className="overflow-hidden overflow-ellipsis whitespace-nowrap text-sm leading-6 text-eerie-black dark:text-bright-gray">
Manage Agents
{t('manageAgents')}
</p>
</div>
)}
@@ -482,8 +507,8 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
<div className="flex flex-col gap-2 border-b-[1px] py-2 dark:border-b-purple-taupe">
<NavLink
onClick={() => {
if (isMobile) {
setNavOpen(!navOpen);
if (isMobile || isTablet) {
setNavOpen(false);
}
resetConversation();
}}
@@ -553,10 +578,10 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
</div>
</div>
</div>
<div className="sticky z-10 h-16 w-full border-b-2 bg-gray-50 dark:border-b-purple-taupe dark:bg-chinese-black md:hidden">
<div className="sticky z-10 h-16 w-full border-b-2 bg-gray-50 dark:border-b-purple-taupe dark:bg-chinese-black lg:hidden">
<div className="ml-6 flex h-full items-center gap-6">
<button
className="h-6 w-6 md:hidden"
className="h-6 w-6 lg:hidden"
onClick={() => setNavOpen(true)}
>
<img

View File

@@ -65,22 +65,18 @@ export default function AgentPreview() {
);
const handleQuestionSubmission = (
updatedQuestion?: string,
question?: string,
updated?: boolean,
indx?: number,
) => {
if (
updated === true &&
updatedQuestion !== undefined &&
indx !== undefined
) {
if (updated === true && question !== undefined && indx !== undefined) {
handleQuestion({
question: updatedQuestion,
question,
index: indx,
isRetry: false,
});
} else if (input.trim() && status !== 'loading') {
const currentInput = input.trim();
} else if (question && status !== 'loading') {
const currentInput = question.trim();
if (lastQueryReturnedErr && queries.length > 0) {
const lastQueryIndex = queries.length - 1;
handleQuestion({
@@ -95,14 +91,6 @@ export default function AgentPreview() {
index: undefined,
});
}
setInput('');
}
};
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
handleQuestionSubmission();
}
};
@@ -135,9 +123,7 @@ export default function AgentPreview() {
</div>
<div className="flex w-[95%] max-w-[1500px] flex-col items-center gap-4 pb-2 md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12">
<MessageInput
value={input}
onChange={(e) => setInput(e.target.value)}
onSubmit={() => handleQuestionSubmission()}
onSubmit={(text) => handleQuestionSubmission(text)}
loading={status === 'loading'}
showSourceButton={selectedAgent ? false : true}
showToolButton={selectedAgent ? false : true}

View File

@@ -21,6 +21,7 @@ import {
import { useDarkTheme } from '../hooks';
import { selectToken, setSelectedAgent } from '../preferences/preferenceSlice';
import { AppDispatch } from '../store';
import SharedAgentCard from './SharedAgentCard';
import { Agent } from './types';
export default function SharedAgent() {
@@ -91,22 +92,18 @@ export default function SharedAgent() {
);
const handleQuestionSubmission = (
updatedQuestion?: string,
question?: string,
updated?: boolean,
indx?: number,
) => {
if (
updated === true &&
updatedQuestion !== undefined &&
indx !== undefined
) {
if (updated === true && question !== undefined && indx !== undefined) {
handleQuestion({
question: updatedQuestion,
question,
index: indx,
isRetry: false,
});
} else if (input.trim() && status !== 'loading') {
const currentInput = input.trim();
} else if (question && status !== 'loading') {
const currentInput = question.trim();
if (lastQueryReturnedErr && queries.length > 0) {
const lastQueryIndex = queries.length - 1;
handleQuestion({
@@ -181,11 +178,9 @@ export default function SharedAgent() {
}
/>
</div>
<div className="flex w-[95%] max-w-[1500px] flex-col items-center gap-4 pb-2 md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12">
<div className="flex w-[95%] max-w-[1500px] flex-col items-center pb-2 md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12">
<MessageInput
value={input}
onChange={(e) => setInput(e.target.value)}
onSubmit={() => handleQuestionSubmission()}
onSubmit={(text) => handleQuestionSubmission(text)}
loading={status === 'loading'}
showSourceButton={sharedAgent ? false : true}
showToolButton={sharedAgent ? false : true}
@@ -199,65 +194,3 @@ export default function SharedAgent() {
</div>
);
}
function SharedAgentCard({ agent }: { agent: Agent }) {
return (
<div className="flex w-full max-w-[720px] flex-col rounded-3xl border border-dark-gray p-6 shadow-sm dark:border-grey sm:w-fit sm:min-w-[480px]">
<div className="flex items-center gap-3">
<div className="flex h-12 w-12 items-center justify-center overflow-hidden rounded-full p-1">
<img src={Robot} className="h-full w-full object-contain" />
</div>
<div className="flex max-h-[92px] w-[80%] flex-col gap-px">
<h2 className="text-base font-semibold text-[#212121] dark:text-[#E0E0E0] sm:text-lg">
{agent.name}
</h2>
<p className="overflow-y-auto text-wrap break-all text-xs text-[#71717A] dark:text-[#949494] sm:text-sm">
{agent.description}
</p>
</div>
</div>
<div className="mt-4 flex items-center gap-8">
{agent.shared_metadata?.shared_by && (
<p className="text-xs font-light text-[#212121] dark:text-[#E0E0E0] sm:text-sm">
by {agent.shared_metadata.shared_by}
</p>
)}
{agent.shared_metadata?.shared_at && (
<p className="text-xs font-light text-[#71717A] dark:text-[#949494] sm:text-sm">
Shared on{' '}
{new Date(agent.shared_metadata.shared_at).toLocaleString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: true,
})}
</p>
)}
</div>
{agent.tools.length > 0 && (
<div className="mt-8">
<p className="text-sm font-semibold text-[#212121] dark:text-[#E0E0E0] sm:text-base">
Connected Tools
</p>
<div className="mt-2 flex flex-wrap gap-2">
{agent.tools.map((tool, index) => (
<span
key={index}
className="flex items-center gap-1 rounded-full bg-bright-gray px-3 py-1 text-xs font-light text-[#212121] dark:bg-dark-charcoal dark:text-[#E0E0E0]"
>
<img
src={`/toolIcons/tool_${tool}.svg`}
alt={`${tool} icon`}
className="h-3 w-3"
/>{' '}
{tool}
</span>
))}
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,69 @@
import Robot from '../assets/robot.svg';
import { Agent } from './types';
export default function SharedAgentCard({ agent }: { agent: Agent }) {
return (
<div className="flex w-full max-w-[720px] flex-col rounded-3xl border border-dark-gray p-6 shadow-sm dark:border-grey sm:w-fit sm:min-w-[480px]">
<div className="flex items-center gap-3">
<div className="flex h-12 w-12 items-center justify-center overflow-hidden rounded-full p-1">
<img src={Robot} className="h-full w-full object-contain" />
</div>
<div className="flex max-h-[92px] w-[80%] flex-col gap-px">
<h2 className="text-base font-semibold text-[#212121] dark:text-[#E0E0E0] sm:text-lg">
{agent.name}
</h2>
<p className="overflow-y-auto text-wrap break-all text-xs text-[#71717A] dark:text-[#949494] sm:text-sm">
{agent.description}
</p>
</div>
</div>
{agent.shared_metadata && (
<div className="mt-4 flex items-center gap-8">
{agent.shared_metadata?.shared_by && (
<p className="text-xs font-light text-[#212121] dark:text-[#E0E0E0] sm:text-sm">
by {agent.shared_metadata.shared_by}
</p>
)}
{agent.shared_metadata?.shared_at && (
<p className="text-xs font-light text-[#71717A] dark:text-[#949494] sm:text-sm">
Shared on{' '}
{new Date(agent.shared_metadata.shared_at).toLocaleString(
'en-US',
{
month: 'long',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: true,
},
)}
</p>
)}
</div>
)}
{agent.tool_details && agent.tool_details.length > 0 && (
<div className="mt-8">
<p className="text-sm font-semibold text-[#212121] dark:text-[#E0E0E0] sm:text-base">
Connected Tools
</p>
<div className="mt-2 flex flex-wrap gap-2">
{agent.tool_details.map((tool, index) => (
<span
key={index}
className="flex items-center gap-1 rounded-full bg-bright-gray px-3 py-1 text-xs font-light text-[#212121] dark:bg-dark-charcoal dark:text-[#E0E0E0]"
>
<img
src={`/toolIcons/tool_${tool.name}.svg`}
alt={`${tool.name} icon`}
className="h-3 w-3"
/>{' '}
{tool.name}
</span>
))}
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,7 @@
import { Navigate, useParams } from 'react-router-dom';
export default function SharedAgentGate() {
const { agentId } = useParams();
return <Navigate to={`/agents/shared/${agentId}`} replace />;
}

View File

@@ -4,11 +4,11 @@ import { Route, Routes, useNavigate } from 'react-router-dom';
import userService from '../api/services/userService';
import Edit from '../assets/edit.svg';
import Link from '../assets/link-gray.svg';
import Monitoring from '../assets/monitoring.svg';
import Pin from '../assets/pin.svg';
import Trash from '../assets/red-trash.svg';
import Robot from '../assets/robot.svg';
import Link from '../assets/link-gray.svg';
import ThreeDots from '../assets/three-dots.svg';
import UnPin from '../assets/unpin.svg';
import ContextMenu, { MenuOption } from '../components/ContextMenu';
@@ -22,9 +22,11 @@ import { ActiveState } from '../models/misc';
import {
selectAgents,
selectSelectedAgent,
selectSharedAgents,
selectToken,
setAgents,
setSelectedAgent,
setSharedAgents,
} from '../preferences/preferenceSlice';
import AgentLogs from './AgentLogs';
import NewAgent from './NewAgent';
@@ -59,13 +61,12 @@ const sectionConfig = {
};
function AgentsList() {
const navigate = useNavigate();
const dispatch = useDispatch();
const token = useSelector(selectToken);
const agents = useSelector(selectAgents);
const sharedAgents = useSelector(selectSharedAgents);
const selectedAgent = useSelector(selectSelectedAgent);
const [sharedAgents, setSharedAgents] = useState<Agent[]>([]);
const [loadingUserAgents, setLoadingUserAgents] = useState<boolean>(true);
const [loadingSharedAgents, setLoadingSharedAgents] = useState<boolean>(true);
@@ -89,7 +90,7 @@ function AgentsList() {
const response = await userService.getSharedAgents(token);
if (!response.ok) throw new Error('Failed to fetch shared agents');
const data = await response.json();
setSharedAgents(data);
dispatch(setSharedAgents(data));
setLoadingSharedAgents(false);
} catch (error) {
console.error('Error:', error);
@@ -162,11 +163,17 @@ function AgentsList() {
</div> */}
<AgentSection
agents={agents ?? []}
updateAgents={(updatedAgents) => {
dispatch(setAgents(updatedAgents));
}}
loading={loadingUserAgents}
section="user"
/>
<AgentSection
agents={sharedAgents ?? []}
updateAgents={(updatedAgents) => {
dispatch(setSharedAgents(updatedAgents));
}}
loading={loadingSharedAgents}
section="shared"
/>
@@ -176,10 +183,12 @@ function AgentsList() {
function AgentSection({
agents,
updateAgents,
loading,
section,
}: {
agents: Agent[];
updateAgents?: (agents: Agent[]) => void;
loading: boolean;
section: keyof typeof sectionConfig;
}) {
@@ -204,20 +213,23 @@ function AgentSection({
</button>
)}
</div>
<div className="grid w-full grid-cols-2 gap-2 md:flex md:flex-wrap md:gap-4">
<div>
{loading ? (
<div className="flex h-72 w-full items-center justify-center">
<Spinner />
</div>
) : agents && agents.length > 0 ? (
agents.map((agent) => (
<AgentCard
key={agent.id}
agent={agent}
agents={agents}
section={section}
/>
))
<div className="grid grid-cols-1 gap-4 sm:flex sm:flex-wrap">
{agents.map((agent, idx) => (
<AgentCard
key={agent.id}
agent={agent}
agents={agents}
updateAgents={updateAgents}
section={section}
/>
))}
</div>
) : (
<div className="flex h-72 w-full flex-col items-center justify-center gap-3 text-base text-[#18181B] dark:text-[#E0E0E0]">
<p>{sectionConfig[section].emptyStateDescription}</p>
@@ -239,10 +251,12 @@ function AgentSection({
function AgentCard({
agent,
agents,
updateAgents,
section,
}: {
agent: Agent;
agents: Agent[];
updateAgents?: (agents: Agent[]) => void;
section: keyof typeof sectionConfig;
}) {
const navigate = useNavigate();
@@ -264,7 +278,23 @@ function AgentCard({
return { ...prevAgent, pinned: !prevAgent.pinned };
return prevAgent;
});
dispatch(setAgents(updatedAgents));
updateAgents?.(updatedAgents);
} catch (error) {
console.error('Error:', error);
}
};
const handleHideSharedAgent = async () => {
try {
const response = await userService.removeSharedAgent(
agent.id ?? '',
token,
);
if (!response.ok) throw new Error('Failed to hide shared agent');
const updatedAgents = agents.filter(
(prevAgent) => prevAgent.id !== agent.id,
);
updateAgents?.(updatedAgents);
} catch (error) {
console.error('Error:', error);
}
@@ -326,8 +356,30 @@ function AgentCard({
navigate(`/agents/shared/${agent.shared_token}`);
},
variant: 'primary',
iconWidth: 14,
iconHeight: 14,
iconWidth: 12,
iconHeight: 12,
},
{
icon: agent.pinned ? UnPin : Pin,
label: agent.pinned ? 'Unpin' : 'Pin agent',
onClick: (e: SyntheticEvent) => {
e.stopPropagation();
togglePin();
},
variant: 'primary',
iconWidth: 18,
iconHeight: 18,
},
{
icon: Trash,
label: 'Remove',
onClick: (e: SyntheticEvent) => {
e.stopPropagation();
handleHideSharedAgent();
},
variant: 'danger',
iconWidth: 13,
iconHeight: 13,
},
],
};

View File

@@ -1,3 +1,9 @@
export type ToolSummary = {
id: string;
name: string;
display_name: string;
};
export type Agent = {
id?: string;
name: string;
@@ -8,6 +14,7 @@ export type Agent = {
retriever: string;
prompt_id: string;
tools: string[];
tool_details?: ToolSummary[];
agent_type: string;
status: string;
key?: string;

View File

@@ -18,6 +18,7 @@ const endpoints = {
SHARED_AGENT: (id: string) => `/api/shared_agent?token=${id}`,
SHARED_AGENTS: '/api/shared_agents',
SHARE_AGENT: `/api/share_agent`,
REMOVE_SHARED_AGENT: (id: string) => `/api/remove_shared_agent?id=${id}`,
AGENT_WEBHOOK: (id: string) => `/api/agent_webhook?id=${id}`,
PROMPTS: '/api/get_prompts',
CREATE_PROMPT: '/api/create_prompt',

View File

@@ -41,6 +41,8 @@ const userService = {
apiClient.get(endpoints.USER.SHARED_AGENTS, token),
shareAgent: (data: any, token: string | null): Promise<any> =>
apiClient.put(endpoints.USER.SHARE_AGENT, data, token),
removeSharedAgent: (id: string, token: string | null): Promise<any> =>
apiClient.delete(endpoints.USER.REMOVE_SHARED_AGENT(id), token),
getAgentWebhook: (id: string, token: string | null): Promise<any> =>
apiClient.get(endpoints.USER.AGENT_WEBHOOK(id), token),
getPrompts: (token: string | null): Promise<any> =>

View File

@@ -1,3 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.16669 11.5H9.83335V13.1666H8.16669V11.5ZM8.16669 4.83329H9.83335V9.83329H8.16669V4.83329ZM8.99169 0.666626C4.39169 0.666626 0.666687 4.39996 0.666687 8.99996C0.666687 13.6 4.39169 17.3333 8.99169 17.3333C13.6 17.3333 17.3334 13.6 17.3334 8.99996C17.3334 4.39996 13.6 0.666626 8.99169 0.666626ZM9.00002 15.6666C5.31669 15.6666 2.33335 12.6833 2.33335 8.99996C2.33335 5.31663 5.31669 2.33329 9.00002 2.33329C12.6834 2.33329 15.6667 5.31663 15.6667 8.99996C15.6667 12.6833 12.6834 15.6666 9.00002 15.6666Z" fill="#F44336"/>
<path d="M8.16669 11.5H9.83335V13.1666H8.16669V11.5ZM8.16669 4.83329H9.83335V9.83329H8.16669V4.83329ZM8.99169 0.666626C4.39169 0.666626 0.666687 4.39996 0.666687 8.99996C0.666687 13.6 4.39169 17.3333 8.99169 17.3333C13.6 17.3333 17.3334 13.6 17.3334 8.99996C17.3334 4.39996 13.6 0.666626 8.99169 0.666626ZM9.00002 15.6666C5.31669 15.6666 2.33335 12.6833 2.33335 8.99996C2.33335 5.31663 5.31669 2.33329 9.00002 2.33329C12.6834 2.33329 15.6667 5.31663 15.6667 8.99996C15.6667 12.6833 12.6834 15.6666 9.00002 15.6666Z" fill="#ECECF1"/>
</svg>

Before

Width:  |  Height:  |  Size: 636 B

After

Width:  |  Height:  |  Size: 636 B

View File

@@ -1,3 +1,3 @@
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 1.5H3C2.46957 1.5 1.96086 1.71071 1.58579 2.08579C1.21071 2.46086 1 2.96957 1 3.5V15.5C1 16.0304 1.21071 16.5391 1.58579 16.9142C1.96086 17.2893 2.46957 17.5 3 17.5H15C15.5304 17.5 16.0391 17.2893 16.4142 16.9142C16.7893 16.5391 17 16.0304 17 15.5V11.5M9 9.5L17 1.5M17 1.5V6.5M17 1.5H12" stroke="#949494" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 1.5H3C2.46957 1.5 1.96086 1.71071 1.58579 2.08579C1.21071 2.46086 1 2.96957 1 3.5V15.5C1 16.0304 1.21071 16.5391 1.58579 16.9142C1.96086 17.2893 2.46957 17.5 3 17.5H15C15.5304 17.5 16.0391 17.2893 16.4142 16.9142C16.7893 16.5391 17 16.0304 17 15.5V11.5M9 9.5L17 1.5M17 1.5V6.5M17 1.5H12" stroke="#747474" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 486 B

After

Width:  |  Height:  |  Size: 486 B

View File

@@ -0,0 +1,89 @@
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import newChatIcon from '../assets/openNewChat.svg';
import ShareIcon from '../assets/share.svg';
import { ShareConversationModal } from '../modals/ShareConversationModal';
import { useState } from 'react';
import { selectConversationId } from '../preferences/preferenceSlice';
import { useDispatch } from 'react-redux';
import { AppDispatch } from '../store';
import {
setConversation,
updateConversationId,
} from '../conversation/conversationSlice';
interface ActionButtonsProps {
className?: string;
showNewChat?: boolean;
showShare?: boolean;
}
import { useNavigate } from 'react-router-dom';
export default function ActionButtons({
className = '',
showNewChat = true,
showShare = true,
}: ActionButtonsProps) {
const { t } = useTranslation();
const dispatch = useDispatch<AppDispatch>();
const conversationId = useSelector(selectConversationId);
const [isShareModalOpen, setShareModalState] = useState<boolean>(false);
const navigate = useNavigate();
const newChat = () => {
dispatch(setConversation([]));
dispatch(
updateConversationId({
query: { conversationId: null },
}),
);
navigate('/');
};
return (
<div className="fixed right-4 top-0 z-10 flex h-16 flex-col justify-center">
<div className={`flex items-center gap-2 sm:gap-4 ${className}`}>
{showNewChat && (
<button
title="Open New Chat"
onClick={newChat}
className="flex items-center gap-1 rounded-full p-2 hover:bg-bright-gray dark:hover:bg-[#28292E] lg:hidden"
>
<img
className="filter dark:invert"
alt="NewChat"
width={21}
height={21}
src={newChatIcon}
/>
</button>
)}
{showShare && conversationId && (
<>
<button
title="Share"
onClick={() => setShareModalState(true)}
className="rounded-full p-2 hover:bg-bright-gray dark:hover:bg-[#28292E]"
>
<img
className="filter dark:invert"
alt="share"
width={16}
height={16}
src={ShareIcon}
/>
</button>
{isShareModalOpen && (
<ShareConversationModal
close={() => setShareModalState(false)}
conversationId={conversationId}
/>
)}
</>
)}
<div>{/* <UserButton /> */}</div>
</div>
</div>
);
}

View File

@@ -30,7 +30,17 @@ export default function ContextMenu({
offset = { x: 0, y: 8 },
}: ContextMenuProps) {
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isOpen && menuRef.current) {
const positionStyle = getMenuPosition();
if (menuRef.current) {
Object.assign(menuRef.current.style, {
top: positionStyle.top,
left: positionStyle.left,
});
}
}
}, [isOpen]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
@@ -61,20 +71,45 @@ export default function ContextMenu({
let top = rect.bottom + scrollY + offset.y;
let left = rect.right + scrollX + offset.x;
// Get menu dimensions (need ref to be available)
const menuWidth = menuRef.current?.offsetWidth || 144; // Default min-width
const menuHeight = menuRef.current?.offsetHeight || 0;
// Get viewport dimensions
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// Adjust position based on specified position
switch (position) {
case 'bottom-left':
left = rect.left + scrollX - offset.x;
break;
case 'top-right':
top = rect.top + scrollY - offset.y;
top = rect.top + scrollY - offset.y - menuHeight;
break;
case 'top-left':
top = rect.top + scrollY - offset.y;
top = rect.top + scrollY - offset.y - menuHeight;
left = rect.left + scrollX - offset.x;
break;
// bottom-right is default
}
if (left + menuWidth > viewportWidth) {
left = Math.max(5, viewportWidth - menuWidth - 5);
}
if (left < 5) {
left = 5;
}
if (top + menuHeight > viewportHeight + scrollY) {
top = rect.top + scrollY - menuHeight - offset.y;
}
if (top < scrollY + 5) {
top = rect.bottom + scrollY + offset.y;
}
return {
position: 'fixed',
top: `${top}px`,
@@ -90,7 +125,7 @@ export default function ContextMenu({
onClick={(e) => e.stopPropagation()}
>
<div
className="flex w-32 flex-col rounded-xl bg-lotion text-sm shadow-xl dark:bg-charleston-green-2 md:w-36"
className="flex flex-col rounded-xl bg-lotion text-sm shadow-xl dark:bg-charleston-green-2"
style={{ minWidth: '144px' }}
>
{options.map((option, index) => (
@@ -109,7 +144,7 @@ export default function ContextMenu({
} `}
>
{option.icon && (
<div className="flex w-4 justify-center">
<div className="flex w-4 min-w-4 flex-shrink-0 justify-center">
<img
width={option.iconWidth || 16}
height={option.iconHeight || 16}
@@ -119,7 +154,7 @@ export default function ContextMenu({
/>
</div>
)}
<span>{option.label}</span>
<span className="hyphens-auto break-words">{option.label}</span>
</button>
))}
</div>

View File

@@ -36,6 +36,8 @@ const Input = ({
const inputRef = useRef<HTMLInputElement>(null);
const hasValue = value !== undefined && value !== null && value !== '';
return (
<div className={`relative ${className}`}>
<input
@@ -58,7 +60,11 @@ const Input = ({
{placeholder && (
<label
htmlFor={id}
className={`absolute -top-2.5 left-3 px-2 ${textSizeStyles[textSize]} transition-all peer-placeholder-shown:left-3 peer-placeholder-shown:top-2.5 peer-placeholder-shown:${textSizeStyles[textSize]} pointer-events-none cursor-none peer-placeholder-shown:text-gray-4000 peer-focus:-top-2.5 peer-focus:left-3 peer-focus:text-xs peer-focus:text-gray-4000 dark:text-silver dark:peer-placeholder-shown:text-gray-400 ${labelBgClassName}`}
className={`absolute select-none ${
hasValue ? '-top-2.5 left-3 text-xs' : ''
} px-2 transition-all peer-placeholder-shown:left-3 peer-placeholder-shown:top-2.5 peer-placeholder-shown:${
textSizeStyles[textSize]
} pointer-events-none cursor-none text-gray-4000 peer-focus:-top-2.5 peer-focus:left-3 peer-focus:text-xs dark:text-gray-400 ${labelBgClassName} max-w-[calc(100%-24px)] overflow-hidden text-ellipsis whitespace-nowrap`}
>
{placeholder}
{required && (

View File

@@ -9,6 +9,7 @@ import ClipIcon from '../assets/clip.svg';
import ExitIcon from '../assets/exit.svg';
import PaperPlane from '../assets/paper_plane.svg';
import SourceIcon from '../assets/source.svg';
import DocumentationDark from '../assets/documentation-dark.svg';
import SpinnerDark from '../assets/spinner-dark.svg';
import Spinner from '../assets/spinner.svg';
import ToolIcon from '../assets/tool.svg';
@@ -17,7 +18,7 @@ import {
removeAttachment,
selectAttachments,
updateAttachment,
} from '../conversation/conversationSlice';
} from '../upload/uploadSlice';
import { useDarkTheme } from '../hooks';
import { ActiveState } from '../models/misc';
import {
@@ -30,9 +31,7 @@ import SourcesPopup from './SourcesPopup';
import ToolsPopup from './ToolsPopup';
type MessageInputProps = {
value: string;
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
onSubmit: () => void;
onSubmit: (text: string) => void;
loading: boolean;
showSourceButton?: boolean;
showToolButton?: boolean;
@@ -40,8 +39,6 @@ type MessageInputProps = {
};
export default function MessageInput({
value,
onChange,
onSubmit,
loading,
showSourceButton = true,
@@ -50,6 +47,7 @@ export default function MessageInput({
}: MessageInputProps) {
const { t } = useTranslation();
const [isDarkTheme] = useDarkTheme();
const [value, setValue] = useState('');
const inputRef = useRef<HTMLTextAreaElement>(null);
const sourceButtonRef = useRef<HTMLButtonElement>(null);
const toolButtonRef = useRef<HTMLButtonElement>(null);
@@ -232,6 +230,11 @@ export default function MessageInput({
handleInput();
}, []);
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setValue(e.target.value);
handleInput();
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
@@ -248,7 +251,10 @@ export default function MessageInput({
};
const handleSubmit = () => {
onSubmit();
if (value.trim() && !loading) {
onSubmit(value);
setValue('');
}
};
return (
<div className="mx-2 flex w-full flex-col">
@@ -257,71 +263,81 @@ export default function MessageInput({
{attachments.map((attachment, index) => (
<div
key={index}
className={`group relative flex items-center rounded-[32px] border border-[#AAAAAA] bg-white px-2 py-1 text-[12px] text-[#5D5D5D] dark:border-purple-taupe dark:bg-[#1F2028] dark:text-bright-gray sm:px-3 sm:py-1.5 sm:text-[14px] ${
className={`group relative flex items-center rounded-xl bg-[#EFF3F4] px-2 py-1 text-[12px] text-[#5D5D5D] dark:bg-[#393B3D] dark:text-bright-gray sm:px-3 sm:py-1.5 sm:text-[14px] ${
attachment.status !== 'completed' ? 'opacity-70' : 'opacity-100'
}`}
title={attachment.fileName}
>
<div className="mr-2 items-center justify-center rounded-lg bg-purple-30 p-[5.5px]">
{attachment.status === 'completed' && (
<img
src={DocumentationDark}
alt="Attachment"
className="h-[15px] w-[15px] object-fill"
/>
)}
{attachment.status === 'failed' && (
<img
src={AlertIcon}
alt="Failed"
className="h-[15px] w-[15px] object-fill"
/>
)}
{(attachment.status === 'uploading' ||
attachment.status === 'processing') && (
<div className="flex h-[15px] w-[15px] items-center justify-center">
<svg className="h-[15px] w-[15px]" viewBox="0 0 24 24">
<circle
className="opacity-0"
cx="12"
cy="12"
r="10"
stroke="transparent"
strokeWidth="4"
fill="none"
/>
<circle
className="text-[#ECECF1]"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
fill="none"
strokeDasharray="62.83"
strokeDashoffset={
62.83 * (1 - attachment.progress / 100)
}
transform="rotate(-90 12 12)"
/>
</svg>
</div>
)}
</div>
<span className="max-w-[120px] truncate font-medium sm:max-w-[150px]">
{attachment.fileName}
</span>
{attachment.status === 'completed' && (
<button
className="absolute right-2 top-1/2 -translate-y-1/2 rounded-full bg-white p-1 opacity-0 transition-opacity hover:bg-white/95 focus:opacity-100 group-hover:opacity-100 dark:bg-[#1F2028] dark:hover:bg-[#1F2028]/95"
onClick={() => {
if (attachment.id) {
dispatch(removeAttachment(attachment.id));
}
}}
aria-label="Remove attachment"
>
<img
src={ExitIcon}
alt="Remove"
className="h-2.5 w-2.5 filter dark:invert"
/>
</button>
)}
{attachment.status === 'failed' && (
<button
className="ml-1.5 flex items-center justify-center rounded-full p-1"
onClick={() => {
if (attachment.id) {
dispatch(removeAttachment(attachment.id));
} else if (attachment.taskId) {
dispatch(removeAttachment(attachment.taskId));
}
}}
aria-label={t('conversation.attachments.remove')}
>
<img
src={AlertIcon}
alt="Upload failed"
className="ml-2 h-3.5 w-3.5"
title="Upload failed"
src={ExitIcon}
alt={t('conversation.attachments.remove')}
className="h-2.5 w-2.5 filter dark:invert"
/>
)}
{(attachment.status === 'uploading' ||
attachment.status === 'processing') && (
<div className="relative ml-2 h-4 w-4">
<svg className="h-4 w-4" viewBox="0 0 24 24">
{/* Background circle */}
<circle
className="text-gray-200 dark:text-gray-700"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
fill="none"
/>
<circle
className="text-blue-600 dark:text-blue-400"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
fill="none"
strokeDasharray="62.83"
strokeDashoffset={62.83 * (1 - attachment.progress / 100)}
transform="rotate(-90 12 12)"
/>
</svg>
</div>
)}
</button>
</div>
))}
</div>
@@ -334,7 +350,7 @@ export default function MessageInput({
id="message-input"
ref={inputRef}
value={value}
onChange={onChange}
onChange={handleChange}
tabIndex={1}
placeholder={t('inputPlaceholder')}
className="inputbox-style no-scrollbar w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-t-[23px] bg-lotion px-4 py-3 text-base leading-tight opacity-100 focus:outline-none dark:bg-transparent dark:text-bright-gray dark:placeholder-bright-gray dark:placeholder-opacity-50 sm:px-6 sm:py-5"
@@ -398,7 +414,7 @@ export default function MessageInput({
className="mr-1 h-3.5 w-3.5 sm:mr-1.5 sm:h-4 sm:w-4"
/>
<span className="xs:text-[12px] text-[10px] font-medium text-[#5D5D5D] dark:text-bright-gray sm:text-[14px]">
Attach
{t('conversation.attachments.attach')}
</span>
<input
type="file"
@@ -406,7 +422,6 @@ export default function MessageInput({
onChange={handleFileAttachment}
/>
</label>
{/* Additional badges can be added here in the future */}
</div>

View File

@@ -81,12 +81,6 @@ export default function SourcesPopup({
return () => window.removeEventListener('resize', updatePosition);
}, [isOpen, anchorRef]);
const handleEmptyDocumentSelect = () => {
dispatch(setSelectedDocs(null));
handlePostDocumentSelect(null);
onClose();
};
const handleClickOutside = (event: MouseEvent) => {
if (
popupRef.current &&
@@ -153,14 +147,24 @@ export default function SourcesPopup({
<>
{filteredOptions?.map((option: any, index: number) => {
if (option.model === embeddingsName) {
const isSelected =
selectedDocs &&
(option.id
? selectedDocs.id === option.id
: selectedDocs.date === option.date);
return (
<div
key={index}
className="flex cursor-pointer items-center border-b border-[#D9D9D9] border-opacity-80 p-3 transition-colors hover:bg-gray-100 dark:border-dim-gray dark:text-[14px] dark:hover:bg-[#2C2E3C]"
onClick={() => {
dispatch(setSelectedDocs(option));
handlePostDocumentSelect(option);
onClose();
if (isSelected) {
dispatch(setSelectedDocs(null));
handlePostDocumentSelect(null);
} else {
dispatch(setSelectedDocs(option));
handlePostDocumentSelect(option);
}
}}
>
<img
@@ -176,44 +180,19 @@ export default function SourcesPopup({
<div
className={`flex h-4 w-4 flex-shrink-0 items-center justify-center border border-[#C6C6C6] p-[0.5px] dark:border-[#757783]`}
>
{selectedDocs &&
(option.id
? selectedDocs.id === option.id // For documents with MongoDB IDs
: selectedDocs.date === option.date) && ( // For preloaded sources
<img
src={CheckIcon}
alt="Selected"
className="h-3 w-3"
/>
)}
{isSelected && (
<img
src={CheckIcon}
alt="Selected"
className="h-3 w-3"
/>
)}
</div>
</div>
);
}
return null;
})}
<div
className="flex cursor-pointer items-center border-b border-[#D9D9D9] border-opacity-80 p-3 transition-colors hover:bg-gray-100 dark:border-dim-gray dark:text-[14px] dark:hover:bg-[#2C2E3C]"
onClick={handleEmptyDocumentSelect}
>
<img
width={14}
height={14}
src={SourceIcon}
alt="Source"
className="mr-3 flex-shrink-0"
/>
<span className="mr-3 flex-grow font-medium text-[#5D5D5D] dark:text-bright-gray">
{t('none')}
</span>
<div
className={`flex h-4 w-4 flex-shrink-0 items-center justify-center border border-[#C6C6C6] p-[0.5px] dark:border-[#757783]`}
>
{selectedDocs === null && (
<img src={CheckIcon} alt="Selected" className="h-3 w-3" />
)}
</div>
</div>
</>
) : (
<div className="p-4 text-center text-gray-500 dark:text-[14px] dark:text-bright-gray">
@@ -228,7 +207,7 @@ export default function SourcesPopup({
className="inline-flex items-center gap-2 text-base font-medium text-violets-are-blue"
onClick={onClose}
>
Go to Documents
{t('settings.documents.goToDocuments')}
<img src={RedirectIcon} alt="Redirect" className="h-3 w-3" />
</a>
</div>
@@ -238,7 +217,7 @@ export default function SourcesPopup({
onClick={handleUploadClick}
className="w-auto rounded-full border border-violets-are-blue px-4 py-2 text-[14px] font-medium text-violets-are-blue transition-colors duration-200 hover:bg-violets-are-blue hover:text-white"
>
Upload new
{t('settings.documents.uploadNew')}
</button>
</div>
</div>

View File

@@ -201,7 +201,7 @@ export default function ToolsPopup({
/>
<div className="overflow-hidden">
<p className="overflow-hidden overflow-ellipsis whitespace-nowrap text-xs font-medium text-gray-900 dark:text-white">
{tool.displayName}
{tool.customName || tool.displayName}
</p>
</div>
</div>

View File

@@ -3,12 +3,10 @@ import { useDropzone } from 'react-dropzone';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import SharedAgentCard from '../agents/SharedAgentCard';
import DragFileUpload from '../assets/DragFileUpload.svg';
import newChatIcon from '../assets/openNewChat.svg';
import ShareIcon from '../assets/share.svg';
import MessageInput from '../components/MessageInput';
import { useMediaQuery } from '../hooks';
import { ShareConversationModal } from '../modals/ShareConversationModal';
import { ActiveState } from '../models/misc';
import {
selectConversationId,
@@ -30,6 +28,10 @@ import {
updateConversationId,
updateQuery,
} from './conversationSlice';
import {
selectCompletedAttachments,
clearAttachments,
} from '../upload/uploadSlice';
export default function Conversation() {
const { t } = useTranslation();
@@ -41,8 +43,8 @@ export default function Conversation() {
const status = useSelector(selectStatus);
const conversationId = useSelector(selectConversationId);
const selectedAgent = useSelector(selectSelectedAgent);
const completedAttachments = useSelector(selectCompletedAttachments);
const [input, setInput] = useState<string>('');
const [uploadModalState, setUploadModalState] =
useState<ActiveState>('INACTIVE');
const [files, setFiles] = useState<File[]>([]);
@@ -110,15 +112,25 @@ export default function Conversation() {
const trimmedQuestion = question.trim();
if (trimmedQuestion === '') return;
const filesAttached = completedAttachments
.filter((a) => a.id)
.map((a) => ({ id: a.id as string, fileName: a.fileName }));
if (index !== undefined) {
if (!isRetry) dispatch(resendQuery({ index, prompt: trimmedQuestion }));
handleFetchAnswer({ question: trimmedQuestion, index });
} else {
if (!isRetry) dispatch(addQuery({ prompt: trimmedQuestion }));
if (!isRetry)
dispatch(
addQuery({
prompt: trimmedQuestion,
attachments: filesAttached,
}),
);
handleFetchAnswer({ question: trimmedQuestion, index });
}
},
[dispatch, handleFetchAnswer],
[dispatch, handleFetchAnswer, completedAttachments],
);
const handleFeedback = (query: Query, feedback: FEEDBACK, index: number) => {
@@ -146,19 +158,19 @@ export default function Conversation() {
};
const handleQuestionSubmission = (
updatedQuestion?: string,
question?: string,
updated?: boolean,
indx?: number,
) => {
if (updated === true) {
handleQuestion({ question: updatedQuestion as string, index: indx });
} else if (input && status !== 'loading') {
handleQuestion({ question: question as string, index: indx });
} else if (question && status !== 'loading') {
if (lastQueryReturnedErr) {
dispatch(
updateQuery({
index: queries.length - 1,
query: {
prompt: input,
prompt: question,
},
}),
);
@@ -168,10 +180,9 @@ export default function Conversation() {
});
} else {
handleQuestion({
question: input,
question,
});
}
setInput('');
}
};
@@ -182,10 +193,7 @@ export default function Conversation() {
query: { conversationId: null },
}),
);
};
const newChat = () => {
if (queries && queries.length > 0) resetConversation();
dispatch(clearAttachments());
};
useEffect(() => {
@@ -196,56 +204,20 @@ export default function Conversation() {
}, [queries[queries.length - 1]]);
return (
<div className="flex h-full flex-col justify-end gap-1">
{conversationId && queries.length > 0 && (
<div className="absolute right-20 top-4">
<div className="mt-2 flex items-center gap-4">
{isMobile && queries.length > 0 && (
<button
title="Open New Chat"
onClick={() => {
newChat();
}}
className="rounded-full p-2 hover:bg-bright-gray dark:hover:bg-[#28292E]"
>
<img
className="h-5 w-5 filter dark:invert"
alt="NewChat"
src={newChatIcon}
/>
</button>
)}
<button
title="Share"
onClick={() => {
setShareModalState(true);
}}
className="rounded-full p-2 hover:bg-bright-gray dark:hover:bg-[#28292E]"
>
<img
className="h-5 w-5 filter dark:invert"
alt="share"
src={ShareIcon}
/>
</button>
</div>
{isShareModalOpen && (
<ShareConversationModal
close={() => {
setShareModalState(false);
}}
conversationId={conversationId}
/>
)}
</div>
)}
<ConversationMessages
handleQuestion={handleQuestion}
handleQuestionSubmission={handleQuestionSubmission}
handleFeedback={handleFeedback}
queries={queries}
status={status}
showHeroOnEmpty={selectedAgent ? false : true}
headerContent={
selectedAgent ? (
<div className="flex w-full items-center justify-center py-4">
<SharedAgentCard agent={selectedAgent} />
</div>
) : undefined
}
/>
<div className="z-3 flex h-auto w-full max-w-[1300px] flex-col items-end self-center rounded-2xl bg-opacity-0 py-1 md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12">
@@ -258,9 +230,9 @@ export default function Conversation() {
</label>
<input {...getInputProps()} id="file-upload" />
<MessageInput
value={input}
onChange={(e) => setInput(e.target.value)}
onSubmit={handleQuestionSubmission}
onSubmit={(text) => {
handleQuestionSubmission(text);
}}
loading={status === 'loading'}
showSourceButton={selectedAgent ? false : true}
showToolButton={selectedAgent ? false : true}

View File

@@ -12,7 +12,7 @@ import {
import rehypeKatex from 'rehype-katex';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import DocumentationDark from '../assets/documentation-dark.svg';
import ChevronDown from '../assets/chevron-down.svg';
import Cloud from '../assets/cloud.svg';
import DocsGPT3 from '../assets/cute_docsgpt3.svg';
@@ -26,7 +26,9 @@ import UserIcon from '../assets/user.svg';
import Accordion from '../components/Accordion';
import Avatar from '../components/Avatar';
import CopyButton from '../components/CopyButton';
import MermaidRenderer from '../components/MermaidRenderer';
import Sidebar from '../components/Sidebar';
import Spinner from '../components/Spinner';
import SpeakButton from '../components/TextToSpeechButton';
import { useDarkTheme, useOutsideAlerter } from '../hooks';
import {
@@ -36,7 +38,6 @@ import {
import classes from './ConversationBubble.module.css';
import { FEEDBACK, MESSAGE_TYPE } from './conversationModels';
import { ToolCallsType } from './types';
import MermaidRenderer from '../components/MermaidRenderer';
const DisableSourceFE = import.meta.env.VITE_DISABLE_SOURCE_FE || false;
@@ -49,7 +50,7 @@ const ConversationBubble = forwardRef<
feedback?: FEEDBACK;
handleFeedback?: (feedback: FEEDBACK) => void;
thought?: string;
sources?: { title: string; text: string; source: string }[];
sources?: { title: string; text: string; link: string }[];
toolCalls?: ToolCallsType[];
retryBtn?: React.ReactElement;
questionNumber?: number;
@@ -59,6 +60,7 @@ const ConversationBubble = forwardRef<
updated?: boolean,
index?: number,
) => void;
filesAttached?: { id: string; fileName: string }[];
}
>(function ConversationBubble(
{
@@ -74,6 +76,7 @@ const ConversationBubble = forwardRef<
questionNumber,
isStreaming,
handleUpdatedQuestionSubmission,
filesAttached,
},
ref,
) {
@@ -105,41 +108,66 @@ const ConversationBubble = forwardRef<
<div
onMouseEnter={() => setIsQuestionHovered(true)}
onMouseLeave={() => setIsQuestionHovered(false)}
className={className}
>
<div
ref={ref}
className={`flex flex-row-reverse justify-items-start ${className}`}
>
<Avatar
size="SMALL"
className="mt-2 flex-shrink-0 text-2xl"
avatar={
<img className="mr-1 rounded-full" width={30} src={UserIcon} />
}
/>
{!isEditClicked && (
<>
<div className="mr-2 flex flex-col">
<div className="flex flex-col items-end">
{filesAttached && filesAttached.length > 0 && (
<div className="mb-4 mr-12 flex flex-wrap justify-end gap-2">
{filesAttached.map((file, index) => (
<div
style={{
wordBreak: 'break-word',
}}
className="ml-2 mr-2 flex max-w-full items-center whitespace-pre-wrap rounded-[28px] bg-gradient-to-b from-medium-purple to-slate-blue px-[19px] py-[14px] text-sm leading-normal text-white sm:text-base"
key={index}
title={file.fileName}
className="flex items-center rounded-xl bg-[#EFF3F4] p-2 text-[14px] text-[#5D5D5D] dark:bg-[#393B3D] dark:text-bright-gray"
>
{message}
<div className="mr-2 items-center justify-center rounded-lg bg-purple-30 p-[5.5px]">
<img
src={DocumentationDark}
alt="Attachment"
className="h-[15px] w-[15px] object-fill"
/>
</div>
<span className="max-w-[150px] truncate font-normal">
{file.fileName}
</span>
</div>
</div>
<button
onClick={() => {
setIsEditClicked(true);
setEditInputBox(message ?? '');
}}
className={`mt-3 flex h-fit flex-shrink-0 cursor-pointer items-center rounded-full p-2 hover:bg-light-silver dark:hover:bg-[#35363B] ${isQuestionHovered || isEditClicked ? 'visible' : 'invisible'}`}
>
<img src={Edit} alt="Edit" className="cursor-pointer" />
</button>
</>
))}
</div>
)}
<div
ref={ref}
className={`flex flex-row-reverse justify-items-start`}
>
<Avatar
size="SMALL"
className="mt-2 flex-shrink-0 text-2xl"
avatar={
<img className="mr-1 rounded-full" width={30} src={UserIcon} />
}
/>
{!isEditClicked && (
<>
<div className="mr-2 flex flex-col">
<div
style={{
wordBreak: 'break-word',
}}
className="ml-2 mr-2 flex max-w-full items-center whitespace-pre-wrap rounded-[28px] bg-gradient-to-b from-medium-purple to-slate-blue px-[19px] py-[14px] text-sm leading-normal text-white sm:text-base"
>
{message}
</div>
</div>
<button
onClick={() => {
setIsEditClicked(true);
setEditInputBox(message ?? '');
}}
className={`mt-3 flex h-fit flex-shrink-0 cursor-pointer items-center rounded-full p-2 hover:bg-light-silver dark:hover:bg-[#35363B] ${isQuestionHovered || isEditClicked ? 'visible' : 'invisible'}`}
>
<img src={Edit} alt="Edit" className="cursor-pointer" />
</button>
</>
)}
</div>
{isEditClicked && (
<div
ref={editableQueryRef}
@@ -233,7 +261,7 @@ const ConversationBubble = forwardRef<
{DisableSourceFE ||
type === 'ERROR' ||
sources?.length === 0 ||
sources?.some((source) => source.source === 'None') ? null : !sources &&
sources?.some((source) => source.link === 'None') ? null : !sources &&
chunks !== '0' &&
selectedDocs ? (
<div className="mb-4 flex flex-col flex-wrap items-start self-start lg:flex-nowrap">
@@ -300,14 +328,14 @@ const ConversationBubble = forwardRef<
</p>
<div
className={`mt-[14px] flex flex-row items-center gap-[6px] underline-offset-2 ${
source.source && source.source !== 'local'
source.link && source.link !== 'local'
? 'hover:text-[#007DFF] hover:underline dark:hover:text-[#48A0FF]'
: ''
}`}
onClick={() =>
source.source && source.source !== 'local'
source.link && source.link !== 'local'
? window.open(
source.source,
source.link,
'_blank',
'noopener, noreferrer',
)
@@ -322,13 +350,13 @@ const ConversationBubble = forwardRef<
<p
className="mt-[2px] truncate text-xs"
title={
source.source && source.source !== 'local'
? source.source
source.link && source.link !== 'local'
? source.link
: source.title
}
>
{source.source && source.source !== 'local'
? source.source
{source.link && source.link !== 'local'
? source.link
: source.title}
</p>
</div>
@@ -339,7 +367,7 @@ const ConversationBubble = forwardRef<
onMouseOver={() => setActiveTooltip(index)}
onMouseOut={() => setActiveTooltip(null)}
>
<p className="max-h-[164px] overflow-y-auto break-words rounded-md text-sm">
<p className="line-clamp-6 max-h-[164px] overflow-hidden text-ellipsis break-words rounded-md text-sm">
{source.text}
</p>
</div>
@@ -649,50 +677,68 @@ const ConversationBubble = forwardRef<
});
type AllSourcesProps = {
sources: { title: string; text: string; source: string }[];
sources: { title: string; text: string; link?: string }[];
};
function AllSources(sources: AllSourcesProps) {
const { t } = useTranslation();
const handleCardClick = (link: string) => {
if (link && link !== 'local') {
window.open(link, '_blank', 'noopener,noreferrer');
}
};
return (
<div className="h-full w-full">
<div className="w-full">
<p className="text-left text-xl">{`${sources.sources.length} Sources`}</p>
<p className="text-left text-xl">{`${sources.sources.length} ${t('conversation.sources.title')}`}</p>
<div className="mx-1 mt-2 h-[0.8px] w-full rounded-full bg-[#C4C4C4]/40 lg:w-[95%]"></div>
</div>
<div className="mt-6 flex h-[90%] w-60 flex-col items-center gap-4 overflow-y-auto sm:w-80">
{sources.sources.map((source, index) => (
<div
key={index}
className="min-h-32 w-full rounded-[20px] bg-gray-1000 p-4 dark:bg-[#28292E]"
>
<span className="flex flex-row">
{sources.sources.map((source, index) => {
const isExternalSource = source.link && source.link !== 'local';
return (
<div
key={index}
className={`group/card relative w-full rounded-[20px] bg-gray-1000 p-4 transition-colors hover:bg-[#F1F1F1] dark:bg-[#28292E] dark:hover:bg-[#2C2E3C] ${
isExternalSource ? 'cursor-pointer' : ''
}`}
onClick={() =>
isExternalSource && source.link && handleCardClick(source.link)
}
>
<p
title={source.title}
className="ellipsis-text break-words text-left text-sm font-semibold"
className={`ellipsis-text break-words text-left text-sm font-semibold ${
isExternalSource
? 'group-hover/card:text-purple-30 dark:group-hover/card:text-[#8C67D7]'
: ''
}`}
>
{`${index + 1}. ${source.title}`}
{isExternalSource && (
<img
src={Link}
alt="External Link"
className={`ml-1 inline h-3 w-3 object-fill dark:invert ${
isExternalSource
? 'group-hover/card:contrast-[50%] group-hover/card:hue-rotate-[235deg] group-hover/card:invert-[31%] group-hover/card:saturate-[752%] group-hover/card:sepia-[80%] group-hover/card:filter'
: ''
}`}
/>
)}
</p>
{source.source && source.source !== 'local' ? (
<img
src={Link}
alt={'Link'}
className="h-3 w-3 cursor-pointer object-fill"
onClick={() =>
window.open(source.source, '_blank', 'noopener, noreferrer')
}
></img>
) : null}
</span>
<p className="mt-3 max-h-16 overflow-y-auto break-words rounded-md text-left text-xs text-black dark:text-chinese-silver">
{source.text}
</p>
</div>
))}
<p className="mt-3 line-clamp-4 break-words rounded-md text-left text-xs text-black dark:text-chinese-silver">
{source.text}
</p>
</div>
);
})}
</div>
</div>
);
}
export default ConversationBubble;
function ToolCalls({ toolCalls }: { toolCalls: ToolCallsType[] }) {
@@ -723,7 +769,7 @@ function ToolCalls({ toolCalls }: { toolCalls: ToolCallsType[] }) {
</button>
</div>
{isToolCallsOpen && (
<div className="fade-in ml-3 mr-5 max-w-[90vw] md:max-w-[70vw] lg:max-w-[50vw]">
<div className="fade-in ml-3 mr-5 w-[90vw] md:w-[70vw] lg:w-full">
<div className="grid grid-cols-1 gap-2">
{toolCalls.map((toolCall, index) => (
<Accordion
@@ -760,14 +806,21 @@ function ToolCalls({ toolCalls }: { toolCalls: ToolCallsType[] }) {
textToCopy={JSON.stringify(toolCall.result, null, 2)}
/>
</p>
<p className="dark:tex break-words rounded-b-2xl p-2 font-mono text-sm dark:bg-[#222327]">
<span
className="leading-[23px] text-black dark:text-gray-400"
style={{ fontFamily: 'IBMPlexMono-Medium' }}
>
{JSON.stringify(toolCall.result, null, 2)}
{toolCall.status === 'pending' && (
<span className="flex w-full items-center justify-center rounded-b-2xl p-2 dark:bg-[#222327]">
<Spinner size="small" />
</span>
</p>
)}
{toolCall.status === 'completed' && (
<p className="break-words rounded-b-2xl p-2 font-mono text-sm dark:bg-[#222327]">
<span
className="leading-[23px] text-black dark:text-gray-400"
style={{ fontFamily: 'IBMPlexMono-Medium' }}
>
{JSON.stringify(toolCall.result, null, 2)}
</span>
</p>
)}
</div>
</div>
</Accordion>

View File

@@ -131,7 +131,7 @@ export default function ConversationMessages({
? LAST_BUBBLE_MARGIN
: DEFAULT_BUBBLE_MARGIN;
if (query.thought || query.response) {
if (query.thought || query.response || query.tool_calls) {
const isCurrentlyStreaming =
status === 'loading' && index === queries.length - 1;
return (
@@ -190,7 +190,7 @@ export default function ConversationMessages({
ref={conversationRef}
onWheel={handleUserScrollInterruption}
onTouchMove={handleUserScrollInterruption}
className="flex h-full w-full justify-center overflow-y-auto sm:pt-12"
className="flex h-full w-full justify-center overflow-y-auto will-change-scroll sm:pt-6 lg:pt-12"
>
{queries.length > 0 && !hasScrolledToLast && (
<button
@@ -223,6 +223,7 @@ export default function ConversationMessages({
handleUpdatedQuestionSubmission={handleQuestionSubmission}
questionNumber={index}
sources={query.sources}
filesAttached={query.attachments}
/>
{renderResponseView(query, index)}
</Fragment>

View File

@@ -23,6 +23,7 @@ import {
setIdentifier,
updateQuery,
} from './sharedConversationSlice';
import { selectCompletedAttachments } from '../upload/uploadSlice';
export const SharedConversation = () => {
const navigate = useNavigate();
@@ -34,8 +35,8 @@ export const SharedConversation = () => {
const date = useSelector(selectDate);
const apiKey = useSelector(selectClientAPIKey);
const status = useSelector(selectStatus);
const completedAttachments = useSelector(selectCompletedAttachments);
const [input, setInput] = useState('');
const { t } = useTranslation();
const dispatch = useDispatch<AppDispatch>();
@@ -76,15 +77,15 @@ export const SharedConversation = () => {
});
};
const handleQuestionSubmission = () => {
if (input && status !== 'loading') {
const handleQuestionSubmission = (question?: string) => {
if (question && status !== 'loading') {
if (lastQueryReturnedErr) {
// update last failed query with new prompt
dispatch(
updateQuery({
index: queries.length - 1,
query: {
prompt: input,
prompt: question,
},
}),
);
@@ -93,9 +94,8 @@ export const SharedConversation = () => {
isRetry: true,
});
} else {
handleQuestion({ question: input });
handleQuestion({ question });
}
setInput('');
}
};
@@ -108,7 +108,19 @@ export const SharedConversation = () => {
}) => {
question = question.trim();
if (question === '') return;
!isRetry && dispatch(addQuery({ prompt: question })); //dispatch only new queries
const filesAttached = completedAttachments
.filter((a) => a.id)
.map((a) => ({ id: a.id as string, fileName: a.fileName }));
!isRetry &&
dispatch(
addQuery({
prompt: question,
attachments: filesAttached,
}),
); //dispatch only new queries
dispatch(fetchSharedAnswer({ question }));
};
useEffect(() => {
@@ -156,10 +168,12 @@ export const SharedConversation = () => {
<div className="flex w-full max-w-[1200px] flex-col items-center gap-4 pb-2 md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12">
{apiKey ? (
<MessageInput
value={input}
onChange={(e) => setInput(e.target.value)}
onSubmit={() => handleQuestionSubmission()}
onSubmit={(text) => {
handleQuestionSubmission(text);
}}
loading={status === 'loading'}
showSourceButton={false}
showToolButton={false}
/>
) : (
<button

View File

@@ -150,10 +150,7 @@ export function handleFetchAnswerSteaming(
done,
value,
}: ReadableStreamReadResult<Uint8Array>) => {
if (done) {
console.log(counterrr);
return;
}
if (done) return;
counterrr += 1;
@@ -163,7 +160,7 @@ export function handleFetchAnswerSteaming(
const events = buffer.split('\n\n');
buffer = events.pop() ?? '';
for (let event of events) {
for (const event of events) {
if (event.trim().startsWith('data:')) {
const dataLine: string = event
.split('\n')
@@ -282,11 +279,12 @@ export function handleSendFeedback(
});
}
export function handleFetchSharedAnswerStreaming( //for shared conversations
export function handleFetchSharedAnswerStreaming(
question: string,
signal: AbortSignal,
apiKey: string,
history: Array<any> = [],
attachments: string[] = [],
onEvent: (event: MessageEvent) => void,
): Promise<Answer> {
history = history.map((item) => {
@@ -303,6 +301,7 @@ export function handleFetchSharedAnswerStreaming( //for shared conversations
history: JSON.stringify(history),
api_key: apiKey,
save_conversation: false,
attachments: attachments.length > 0 ? attachments : undefined,
};
conversationService
.answerStream(payload, null, signal)
@@ -358,6 +357,7 @@ export function handleFetchSharedAnswer(
question: string,
signal: AbortSignal,
apiKey: string,
attachments?: string[],
): Promise<
| {
result: any;
@@ -373,15 +373,15 @@ export function handleFetchSharedAnswer(
title: any;
}
> {
const payload = {
question: question,
api_key: apiKey,
attachments:
attachments && attachments.length > 0 ? attachments : undefined,
};
return conversationService
.answer(
{
question: question,
api_key: apiKey,
},
null,
signal,
)
.answer(payload, null, signal)
.then((response) => {
if (response.ok) {
return response.json();

View File

@@ -22,7 +22,6 @@ export interface ConversationState {
queries: Query[];
status: Status;
conversationId: string | null;
attachments: Attachment[];
}
export interface Answer {
@@ -43,10 +42,10 @@ export interface Query {
conversationId?: string | null;
title?: string | null;
thought?: string;
sources?: { title: string; text: string; source: string }[];
sources?: { title: string; text: string; link: string }[];
tool_calls?: ToolCallsType[];
error?: string;
attachments?: { fileName: string; id: string }[];
attachments?: { id: string; fileName: string }[];
}
export interface RetrievalPayload {

View File

@@ -3,23 +3,27 @@ import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { getConversations } from '../preferences/preferenceApi';
import { setConversations } from '../preferences/preferenceSlice';
import store from '../store';
import {
clearAttachments,
selectCompletedAttachments,
} from '../upload/uploadSlice';
import {
handleFetchAnswer,
handleFetchAnswerSteaming,
} from './conversationHandlers';
import {
Answer,
Attachment,
ConversationState,
Query,
Status,
ConversationState,
Attachment,
} from './conversationModels';
import { ToolCallsType } from './types';
const initialState: ConversationState = {
queries: [],
status: 'idle',
conversationId: null,
attachments: [],
};
const API_STREAMING = import.meta.env.VITE_API_STREAMING === 'true';
@@ -44,9 +48,14 @@ export const fetchAnswer = createAsyncThunk<
let isSourceUpdated = false;
const state = getState() as RootState;
const attachmentIds = state.conversation.attachments
.filter((a) => a.id && a.status === 'completed')
const attachmentIds = selectCompletedAttachments(state)
.filter((a) => a.id)
.map((a) => a.id) as string[];
if (attachmentIds.length > 0) {
dispatch(clearAttachments());
}
const currentConversationId = state.conversation.conversationId;
const conversationIdToSend = isPreview ? null : currentConversationId;
const save_conversation = isPreview ? false : true;
@@ -110,11 +119,11 @@ export const fetchAnswer = createAsyncThunk<
query: { sources: data.source ?? [] },
}),
);
} else if (data.type === 'tool_calls') {
} else if (data.type === 'tool_call') {
dispatch(
updateToolCalls({
updateToolCall({
index: targetIndex,
query: { tool_calls: data.tool_calls },
tool_call: data.data as ToolCallsType,
}),
);
} else if (data.type === 'error') {
@@ -280,12 +289,24 @@ export const conversationSlice = createSlice({
state.queries[index].sources!.push(query.sources![0]);
}
},
updateToolCalls(
state,
action: PayloadAction<{ index: number; query: Partial<Query> }>,
) {
const { index, query } = action.payload;
state.queries[index].tool_calls = query?.tool_calls ?? [];
updateToolCall(state, action) {
const { index, tool_call } = action.payload;
if (!state.queries[index].tool_calls) {
state.queries[index].tool_calls = [];
}
const existingIndex = state.queries[index].tool_calls.findIndex(
(call) => call.call_id === tool_call.call_id,
);
if (existingIndex !== -1) {
const existingCall = state.queries[index].tool_calls[existingIndex];
state.queries[index].tool_calls[existingIndex] = {
...existingCall,
...tool_call,
};
} else state.queries[index].tool_calls.push(tool_call);
},
updateQuery(
state,
@@ -307,39 +328,11 @@ export const conversationSlice = createSlice({
const { index, message } = action.payload;
state.queries[index].error = message;
},
setAttachments: (state, action: PayloadAction<Attachment[]>) => {
state.attachments = action.payload;
},
addAttachment: (state, action: PayloadAction<Attachment>) => {
state.attachments.push(action.payload);
},
updateAttachment: (
state,
action: PayloadAction<{
taskId: string;
updates: Partial<Attachment>;
}>,
) => {
const index = state.attachments.findIndex(
(att) => att.taskId === action.payload.taskId,
);
if (index !== -1) {
state.attachments[index] = {
...state.attachments[index],
...action.payload.updates,
};
}
},
removeAttachment: (state, action: PayloadAction<string>) => {
state.attachments = state.attachments.filter(
(att) => att.taskId !== action.payload && att.id !== action.payload,
);
},
resetConversation: (state) => {
state.queries = initialState.queries;
state.status = initialState.status;
state.conversationId = initialState.conversationId;
state.attachments = initialState.attachments;
handleAbort();
},
},
@@ -365,11 +358,6 @@ export const selectQueries = (state: RootState) => state.conversation.queries;
export const selectStatus = (state: RootState) => state.conversation.status;
export const selectAttachments = (state: RootState) =>
state.conversation.attachments;
export const selectCompletedAttachments = (state: RootState) =>
state.conversation.attachments.filter((att) => att.status === 'completed');
export const {
addQuery,
updateQuery,
@@ -378,12 +366,10 @@ export const {
updateConversationId,
updateThought,
updateStreamingSource,
updateToolCalls,
updateToolCall,
setConversation,
setAttachments,
addAttachment,
updateAttachment,
removeAttachment,
setStatus,
raiseError,
resetConversation,
} = conversationSlice.actions;
export default conversationSlice.reducer;

View File

@@ -7,6 +7,10 @@ import {
handleFetchSharedAnswer,
handleFetchSharedAnswerStreaming,
} from './conversationHandlers';
import {
selectCompletedAttachments,
clearAttachments,
} from '../upload/uploadSlice';
const API_STREAMING = import.meta.env.VITE_API_STREAMING === 'true';
interface SharedConversationsType {
@@ -29,6 +33,14 @@ export const fetchSharedAnswer = createAsyncThunk<Answer, { question: string }>(
async ({ question }, { dispatch, getState, signal }) => {
const state = getState() as RootState;
const attachmentIds = selectCompletedAttachments(state)
.filter((a) => a.id)
.map((a) => a.id) as string[];
if (attachmentIds.length > 0) {
dispatch(clearAttachments());
}
if (state.preference && state.sharedConversation.apiKey) {
if (API_STREAMING) {
await handleFetchSharedAnswerStreaming(
@@ -36,7 +48,7 @@ export const fetchSharedAnswer = createAsyncThunk<Answer, { question: string }>(
signal,
state.sharedConversation.apiKey,
state.sharedConversation.queries,
attachmentIds,
(event) => {
const data = JSON.parse(event.data);
// check if the 'end' event has been received
@@ -72,7 +84,7 @@ export const fetchSharedAnswer = createAsyncThunk<Answer, { question: string }>(
dispatch(sharedConversationSlice.actions.setStatus('failed'));
dispatch(
sharedConversationSlice.actions.raiseError({
index: state.conversation.queries.length - 1,
index: state.sharedConversation.queries.length - 1,
message: data.error,
}),
);
@@ -92,6 +104,7 @@ export const fetchSharedAnswer = createAsyncThunk<Answer, { question: string }>(
question,
signal,
state.sharedConversation.apiKey,
attachmentIds,
);
if (answer) {
let sourcesPrepped = [];

View File

@@ -3,5 +3,6 @@ export type ToolCallsType = {
action_name: string;
call_id: string;
arguments: Record<string, any>;
result: Record<string, any>;
result?: Record<string, any>;
status?: 'pending' | 'completed';
};

View File

@@ -35,21 +35,21 @@ export function useOutsideAlerter<T extends HTMLElement>(
export function useMediaQuery() {
const mobileQuery = '(max-width: 768px)';
const darkModeQuery = '(prefers-color-scheme: dark)'; // Detect dark mode
const desktopQuery = '(min-width: 960px)';
const tabletQuery = '(max-width: 1023px)';
const desktopQuery = '(min-width: 1024px)';
const [isMobile, setIsMobile] = useState(false);
const [isTablet, setIsTablet] = useState(false);
const [isDesktop, setIsDesktop] = useState(false);
const [isDarkMode, setIsDarkMode] = useState(false);
useEffect(() => {
const mobileMedia = window.matchMedia(mobileQuery);
const tabletMedia = window.matchMedia(tabletQuery);
const desktopMedia = window.matchMedia(desktopQuery);
const darkModeMedia = window.matchMedia(darkModeQuery);
const updateMediaQueries = () => {
setIsMobile(mobileMedia.matches);
setIsTablet(tabletMedia.matches && !mobileMedia.matches); // Tablet but not mobile
setIsDesktop(desktopMedia.matches);
setIsDarkMode(darkModeMedia.matches);
};
updateMediaQueries();
@@ -60,9 +60,9 @@ export function useMediaQuery() {
return () => {
window.removeEventListener('resize', listener);
};
}, [mobileQuery, desktopQuery, darkModeQuery]);
}, [mobileQuery, tabletQuery, desktopQuery]);
return { isMobile, isDesktop, isDarkMode };
return { isMobile, isTablet, isDesktop };
}
export function useDarkTheme() {

View File

@@ -46,6 +46,31 @@ body.dark {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
/* Thin scrollbar utility */
.scrollbar-thin::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.scrollbar-thin::-webkit-scrollbar-track {
background: transparent;
}
.scrollbar-thin::-webkit-scrollbar-thumb {
background: rgba(156, 163, 175, 0.5);
border-radius: 3px;
}
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
background: rgba(156, 163, 175, 0.7);
}
/* For Firefox */
.scrollbar-thin {
scrollbar-width: thin;
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
}
}
@layer components {

View File

@@ -11,6 +11,7 @@
"help": "Help",
"emailUs": "Email us",
"documentation": "Documentation",
"manageAgents": "Manage Agents",
"demo": [
{
"header": "Learn about DocsGPT",
@@ -72,7 +73,13 @@
},
"actions": "Actions",
"view": "View",
"deleteWarning": "Are you sure you want to delete \"{{name}}\"?"
"deleteWarning": "Are you sure you want to delete \"{{name}}\"?",
"backToAll": "Back to all documents",
"chunks": "Chunks",
"noChunks": "No chunks found",
"noChunksAlt": "No chunks found",
"goToDocuments": "Go to Documents",
"uploadNew": "Upload new"
},
"apiKeys": {
"label": "Chatbots",
@@ -117,9 +124,44 @@
"noToolsFound": "No tools found",
"selectToolSetup": "Select a tool to set up",
"settingsIconAlt": "Settings icon",
"configureToolAria": "Configure {toolName}",
"toggleToolAria": "Toggle {toolName}",
"manageTools": "Go to Tools"
"configureToolAria": "Configure {{toolName}}",
"toggleToolAria": "Toggle {{toolName}}",
"manageTools": "Go to Tools",
"edit": "Edit",
"delete": "Delete",
"deleteWarning": "Are you sure you want to delete the tool \"{{toolName}}\" ?",
"unsavedChanges": "You have unsaved changes that will be lost if you leave without saving.",
"leaveWithoutSaving": "Leave without Saving",
"saveAndLeave": "Save and Leave",
"customName": "Custom Name",
"customNamePlaceholder": "Enter a custom name (optional)",
"authentication": "Authentication",
"actions": "Actions",
"addAction": "Add action",
"noActionsFound": "No actions found",
"url": "URL",
"urlPlaceholder": "Enter URL",
"method": "Method",
"description": "Description",
"descriptionPlaceholder": "Enter description",
"headers": "Headers",
"queryParameters": "Query Parameters",
"body": "Body",
"deleteActionWarning": "Are you sure you want to delete the action \"{{name}}\"?",
"backToAllTools": "Back to all tools",
"save": "Save",
"fieldName": "Field Name",
"fieldType": "Field Type",
"filledByLLM": "Filled by LLM",
"fieldDescription": "Field description",
"value": "Value",
"addProperty": "Add property",
"propertyName": "New property key",
"add": "Add",
"cancel": "Cancel",
"addNew": "Add New",
"name": "Name",
"type": "Type"
}
},
"modals": {
@@ -203,7 +245,20 @@
"promptName": "Prompt Name",
"promptText": "Prompt Text",
"save": "Save",
"nameExists": "Name already exists"
"nameExists": "Name already exists",
"deleteConfirmation": "Are you sure you want to delete the prompt '{{name}}'?"
},
"chunk": {
"add": "Add Chunk",
"edit": "Edit Chunk",
"title": "Title",
"enterTitle": "Enter title",
"bodyText": "Body text",
"promptText": "Prompt Text",
"update": "Update",
"close": "Close",
"delete": "Delete",
"deleteConfirmation": "Are you sure you want to delete this chunk?"
}
},
"sharedConv": {
@@ -241,6 +296,11 @@
"link": "Source link",
"view_more": "{{count}} more sources"
},
"attachments": {
"attach": "Attach",
"remove": "Remove attachment",
"uploadFailed": "Upload failed"
},
"retry": "Retry"
}
}

View File

@@ -11,6 +11,7 @@
"help": "Asistencia",
"emailUs": "Envíanos un correo",
"documentation": "Documentación",
"manageAgents": "Administrar Agentes",
"demo": [
{
"header": "Aprende sobre DocsGPT",
@@ -48,7 +49,8 @@
"medium": "Medio",
"high": "Alto",
"unlimited": "Ilimitado",
"default": "Predeterminado"
"default": "Predeterminado",
"addNew": "Añadir Nuevo"
},
"documents": {
"title": "Esta tabla contiene todos los documentos que están disponibles para ti y los que has subido",
@@ -71,7 +73,13 @@
},
"actions": "Acciones",
"view": "Ver",
"deleteWarning": "¿Estás seguro de que deseas eliminar \"{{name}}\"?"
"deleteWarning": "¿Estás seguro de que deseas eliminar \"{{name}}\"?",
"backToAll": "Volver a todos los documentos",
"chunks": "Fragmentos",
"noChunks": "No se encontraron fragmentos",
"noChunksAlt": "No se encontraron fragmentos",
"goToDocuments": "Ir a Documentos",
"uploadNew": "Subir nuevo"
},
"apiKeys": {
"label": "Chatbots",
@@ -116,8 +124,44 @@
"noToolsFound": "No se encontraron herramientas",
"selectToolSetup": "Seleccione una herramienta para configurar",
"settingsIconAlt": "Icono de configuración",
"configureToolAria": "Configurar {toolName}",
"toggleToolAria": "Alternar {toolName}"
"configureToolAria": "Configurar {{toolName}}",
"toggleToolAria": "Alternar {{toolName}}",
"manageTools": "Ir a Herramientas",
"edit": "Editar",
"delete": "Eliminar",
"deleteWarning": "¿Estás seguro de que deseas eliminar la herramienta \"{{toolName}}\"?",
"unsavedChanges": "Tienes cambios sin guardar que se perderán si sales sin guardar.",
"leaveWithoutSaving": "Salir sin Guardar",
"saveAndLeave": "Guardar y Salir",
"customName": "Nombre Personalizado",
"customNamePlaceholder": "Ingresa un nombre personalizado (opcional)",
"authentication": "Autenticación",
"actions": "Acciones",
"addAction": "Agregar acción",
"noActionsFound": "No se encontraron acciones",
"url": "URL",
"urlPlaceholder": "Ingresa url",
"method": "Método",
"description": "Descripción",
"descriptionPlaceholder": "Ingresa descripción",
"headers": "Encabezados",
"queryParameters": "Parámetros de Consulta",
"body": "Cuerpo",
"deleteActionWarning": "¿Estás seguro de que deseas eliminar la acción \"{{name}}\"?",
"save": "Guardar",
"name": "Nombre",
"type": "Tipo",
"filledByLLM": "Completado por LLM",
"value": "Valor",
"addProperty": "Agregar propiedad",
"propertyName": "Nueva clave de propiedad",
"backToAllTools": "Volver a todas las herramientas",
"fieldName": "Nombre del campo",
"fieldType": "Tipo de campo",
"fieldDescription": "Descripción del campo",
"add": "Añadir",
"cancel": "Cancelar",
"addNew": "Añadir Nuevo"
}
},
"modals": {
@@ -201,7 +245,20 @@
"promptName": "Nombre del Prompt",
"promptText": "Texto del Prompt",
"save": "Guardar",
"nameExists": "El nombre ya existe"
"nameExists": "El nombre ya existe",
"deleteConfirmation": "¿Estás seguro de que deseas eliminar el prompt '{{name}}'?"
},
"chunk": {
"add": "Agregar Fragmento",
"edit": "Editar Fragmento",
"title": "Título",
"enterTitle": "Ingresar título",
"bodyText": "Texto del cuerpo",
"promptText": "Texto del prompt",
"update": "Actualizar",
"close": "Cerrar",
"delete": "Eliminar",
"deleteConfirmation": "¿Estás seguro de que deseas eliminar este fragmento?"
}
},
"sharedConv": {
@@ -235,9 +292,14 @@
},
"sources": {
"title": "Fuentes",
"text": "Texto fuente",
"link": "Enlace fuente",
"view_more": "Ver {{count}} más fuentes"
"view_more": "Ver {{count}} más fuentes",
"text": "Elegir tus fuentes"
},
"attachments": {
"attach": "Adjuntar",
"remove": "Eliminar adjunto",
"uploadFailed": "Error al subir"
},
"retry": "Reintentar"
}

View File

@@ -11,6 +11,7 @@
"help": "ヘルプ",
"emailUs": "メールを送る",
"documentation": "ドキュメント",
"manageAgents": "エージェント管理",
"demo": [
{
"header": "DocsGPTについて学ぶ",
@@ -52,6 +53,7 @@
"add": "追加"
},
"documents": {
"title": "この表には、利用可能なすべてのドキュメントとアップロードしたドキュメントが含まれています",
"label": "ドキュメント",
"name": "ドキュメント名",
"date": "ベクトル日付",
@@ -71,7 +73,13 @@
},
"actions": "アクション",
"view": "表示",
"deleteWarning": "\"{{name}}\"を削除してもよろしいですか?"
"deleteWarning": "\"{{name}}\"を削除してもよろしいですか?",
"backToAll": "すべてのドキュメントに戻る",
"chunks": "チャンク",
"noChunks": "チャンクが見つかりません",
"noChunksAlt": "チャンクが見つかりません",
"goToDocuments": "ドキュメントへ移動",
"uploadNew": "新規アップロード"
},
"apiKeys": {
"label": "チャットボット",
@@ -96,7 +104,6 @@
},
"messages": "メッセージ",
"tokenUsage": "トークン使用量",
"feedback": "フィードバック",
"filterPlaceholder": "フィルター",
"none": "なし",
"positiveFeedback": "肯定的なフィードバック",
@@ -117,8 +124,44 @@
"noToolsFound": "ツールが見つかりません",
"selectToolSetup": "設定するツールを選択してください",
"settingsIconAlt": "設定アイコン",
"configureToolAria": "{toolName} を設定",
"toggleToolAria": "{toolName} を切り替え"
"configureToolAria": "{{toolName}}を設定",
"toggleToolAria": "{{toolName}}を切り替え",
"manageTools": "ツールへ移動",
"edit": "編集",
"delete": "削除",
"deleteWarning": "ツール \"{{toolName}}\" を削除してもよろしいですか?",
"unsavedChanges": "保存されていない変更があります。保存せずに離れると失われます。",
"leaveWithoutSaving": "保存せずに離れる",
"saveAndLeave": "保存して離れる",
"customName": "カスタム名",
"customNamePlaceholder": "カスタム名を入力(任意)",
"authentication": "認証",
"actions": "アクション",
"addAction": "アクションを追加",
"noActionsFound": "アクションが見つかりません",
"url": "URL",
"urlPlaceholder": "URLを入力",
"method": "メソッド",
"description": "説明",
"descriptionPlaceholder": "説明を入力",
"headers": "ヘッダー",
"queryParameters": "クエリパラメータ",
"body": "ボディ",
"deleteActionWarning": "アクション \"{{name}}\" を削除してもよろしいですか?",
"backToAllTools": "すべてのツールに戻る",
"save": "保存",
"fieldName": "フィールド名",
"fieldType": "フィールドタイプ",
"filledByLLM": "LLMによる入力",
"fieldDescription": "フィールドの説明",
"value": "値",
"addProperty": "プロパティを追加",
"propertyName": "新しいプロパティキー",
"add": "追加",
"cancel": "キャンセル",
"addNew": "新規追加",
"name": "名前",
"type": "タイプ"
}
},
"modals": {
@@ -202,7 +245,20 @@
"promptName": "プロンプト名",
"promptText": "プロンプトテキスト",
"save": "保存",
"nameExists": "名前が既に存在します"
"nameExists": "名前が既に存在します",
"deleteConfirmation": "プロンプト「{{name}}」を削除してもよろしいですか?"
},
"chunk": {
"add": "チャンクを追加",
"edit": "チャンクを編集",
"title": "タイトル",
"enterTitle": "タイトルを入力",
"bodyText": "本文",
"promptText": "プロンプトテキスト",
"update": "更新",
"close": "閉じる",
"delete": "削除",
"deleteConfirmation": "このチャンクを削除してもよろしいですか?"
}
},
"sharedConv": {
@@ -238,7 +294,12 @@
"title": "ソース",
"text": "ソーステキスト",
"link": "ソースリンク",
"view_more": "さらに{{count}}個のソースを表示"
"view_more": "さらに{{count}}個のソース"
},
"attachments": {
"attach": "添付",
"remove": "添付ファイルを削除",
"uploadFailed": "アップロード失敗"
},
"retry": "再試行"
}

View File

@@ -11,6 +11,7 @@
"help": "Помощь",
"emailUs": "Напишите нам",
"documentation": "Документация",
"manageAgents": "Управление агентами",
"demo": [
{
"header": "Узнайте о DocsGPT",
@@ -72,7 +73,13 @@
},
"actions": "Действия",
"view": "Просмотр",
"deleteWarning": "Вы уверены, что хотите удалить \"{{name}}\"?"
"deleteWarning": "Вы уверены, что хотите удалить \"{{name}}\"?",
"backToAll": "Вернуться ко всем документам",
"chunks": "Фрагменты",
"noChunks": "Фрагменты не найдены",
"noChunksAlt": "Фрагменты не найдены",
"goToDocuments": "Перейти к документам",
"uploadNew": "Загрузить новый"
},
"apiKeys": {
"label": "API ключи",
@@ -116,9 +123,45 @@
"addTool": "Добавить инструмент",
"noToolsFound": "Инструменты не найдены",
"selectToolSetup": "Выберите инструмент для настройки",
"settingsIconAlt": "Иконка настроек",
"configureToolAria": "Настроить {toolName}",
"toggleToolAria": "Переключить {toolName}"
"settingsIconAlt": "Значок настроек",
"configureToolAria": "Настроить {{toolName}}",
"toggleToolAria": "Переключить {{toolName}}",
"manageTools": "Перейти к инструментам",
"edit": "Редактировать",
"delete": "Удалить",
"deleteWarning": "Вы уверены, что хотите удалить инструмент \"{{toolName}}\"?",
"unsavedChanges": "У вас есть несохраненные изменения, которые будут потеряны, если вы уйдете без сохранения.",
"leaveWithoutSaving": "Уйти без сохранения",
"saveAndLeave": "Сохранить и уйти",
"customName": "Пользовательское имя",
"customNamePlaceholder": "Введите пользовательское имя (необязательно)",
"authentication": "Аутентификация",
"actions": "Действия",
"addAction": "Добавить действие",
"noActionsFound": "Действия не найдены",
"url": "URL",
"urlPlaceholder": "Введите URL",
"method": "Метод",
"description": "Описание",
"descriptionPlaceholder": "Введите описание",
"headers": "Заголовки",
"queryParameters": "Параметры запроса",
"body": "Тело запроса",
"deleteActionWarning": "Вы уверены, что хотите удалить действие \"{{name}}\"?",
"backToAllTools": "Вернуться ко всем инструментам",
"save": "Сохранить",
"fieldName": "Имя поля",
"fieldType": "Тип поля",
"filledByLLM": "Заполняется LLM",
"fieldDescription": "Описание поля",
"value": "Значение",
"addProperty": "Добавить свойство",
"propertyName": "Новый ключ свойства",
"add": "Добавить",
"cancel": "Отмена",
"addNew": "Добавить новое",
"name": "Имя",
"type": "Тип"
}
},
"modals": {
@@ -202,7 +245,20 @@
"promptName": "Название подсказки",
"promptText": "Текст подсказки",
"save": "Сохранить",
"nameExists": "Название уже существует"
"nameExists": "Название уже существует",
"deleteConfirmation": "Вы уверены, что хотите удалить подсказку «{{name}}»?"
},
"chunk": {
"add": "Добавить фрагмент",
"edit": "Редактировать фрагмент",
"title": "Заголовок",
"enterTitle": "Введите заголовок",
"bodyText": "Текст",
"promptText": "Текст подсказки",
"update": "Обновить",
"close": "Закрыть",
"delete": "Удалить",
"deleteConfirmation": "Вы уверены, что хотите удалить этот фрагмент?"
}
},
"sharedConv": {
@@ -236,9 +292,14 @@
},
"sources": {
"title": "Источники",
"text": "Текст источника",
"text": "Выберите ваши источники",
"link": "Ссылка на источник",
"view_more": "Показать еще {{count}} источников"
"view_more": "ещё {{count}} источников"
},
"attachments": {
"attach": "Прикрепить",
"remove": "Удалить вложение",
"uploadFailed": "Ошибка загрузки"
},
"retry": "Повторить"
}

View File

@@ -11,6 +11,7 @@
"help": "幫助",
"emailUs": "給我們發電郵",
"documentation": "文件",
"manageAgents": "管理代理",
"demo": [
{
"header": "了解 DocsGPT",
@@ -72,7 +73,13 @@
},
"actions": "操作",
"view": "查看",
"deleteWarning": "您確定要刪除 \"{{name}}\" 嗎?"
"deleteWarning": "您確定要刪除 \"{{name}}\" 嗎?",
"backToAll": "返回所有文件",
"chunks": "文本塊",
"noChunks": "未找到文本塊",
"noChunksAlt": "未找到文本塊",
"goToDocuments": "前往文件",
"uploadNew": "上傳新文件"
},
"apiKeys": {
"label": "聊天機器人",
@@ -112,13 +119,49 @@
},
"tools": {
"label": "工具",
"searchPlaceholder": "搜尋...",
"searchPlaceholder": "搜尋工具...",
"addTool": "新增工具",
"noToolsFound": "找不到工具",
"selectToolSetup": "選擇要設定的工具",
"settingsIconAlt": "設定圖",
"configureToolAria": "配置 {toolName}",
"toggleToolAria": "切換 {toolName}"
"settingsIconAlt": "設定圖",
"configureToolAria": "設定 {{toolName}}",
"toggleToolAria": "切換 {{toolName}}",
"manageTools": "前往工具",
"edit": "編輯",
"delete": "刪除",
"deleteWarning": "您確定要刪除工具 \"{{toolName}}\" 嗎?",
"unsavedChanges": "您有未儲存的變更,如果不儲存就離開將會遺失。",
"leaveWithoutSaving": "不儲存離開",
"saveAndLeave": "儲存並離開",
"customName": "自訂名稱",
"customNamePlaceholder": "輸入自訂名稱(選填)",
"authentication": "認證",
"actions": "操作",
"addAction": "新增操作",
"noActionsFound": "找不到操作",
"url": "URL",
"urlPlaceholder": "輸入url",
"method": "方法",
"description": "描述",
"descriptionPlaceholder": "輸入描述",
"headers": "標頭",
"queryParameters": "查詢參數",
"body": "主體",
"deleteActionWarning": "您確定要刪除操作 \"{{name}}\" 嗎?",
"backToAllTools": "返回所有工具",
"save": "儲存",
"fieldName": "欄位名稱",
"fieldType": "欄位類型",
"filledByLLM": "由LLM填入",
"fieldDescription": "欄位描述",
"value": "值",
"addProperty": "新增屬性",
"propertyName": "新屬性鍵",
"add": "新增",
"cancel": "取消",
"addNew": "新增",
"name": "名稱",
"type": "類型"
}
},
"modals": {
@@ -202,7 +245,20 @@
"promptName": "提示名稱",
"promptText": "提示文字",
"save": "儲存",
"nameExists": "名稱已存在"
"nameExists": "名稱已存在",
"deleteConfirmation": "您確定要刪除提示「{{name}}」嗎?"
},
"chunk": {
"add": "新增區塊",
"edit": "編輯區塊",
"title": "標題",
"enterTitle": "輸入標題",
"bodyText": "內文",
"promptText": "提示文字",
"update": "更新",
"close": "關閉",
"delete": "刪除",
"deleteConfirmation": "您確定要刪除此區塊嗎?"
}
},
"sharedConv": {
@@ -240,6 +296,11 @@
"link": "來源連結",
"view_more": "查看更多 {{count}} 個來源"
},
"attachments": {
"attach": "附件",
"remove": "刪除附件",
"uploadFailed": "上傳失敗"
},
"retry": "重試"
}
}

View File

@@ -11,6 +11,7 @@
"help": "帮助",
"emailUs": "给我们发邮件",
"documentation": "文档",
"manageAgents": "管理代理",
"demo": [
{
"header": "了解 DocsGPT",
@@ -72,7 +73,13 @@
},
"actions": "操作",
"view": "查看",
"deleteWarning": "您确定要删除 \"{{name}}\" 吗?"
"deleteWarning": "您确定要删除 \"{{name}}\" 吗?",
"backToAll": "返回所有文档",
"chunks": "文本块",
"noChunks": "未找到文本块",
"noChunksAlt": "未找到文本块",
"goToDocuments": "前往文档",
"uploadNew": "上传新文档"
},
"apiKeys": {
"label": "聊天机器人",
@@ -112,13 +119,49 @@
},
"tools": {
"label": "工具",
"searchPlaceholder": "搜索...",
"searchPlaceholder": "搜索工具...",
"addTool": "添加工具",
"noToolsFound": "未找到工具",
"selectToolSetup": "选择要设置的工具",
"settingsIconAlt": "设置图标",
"configureToolAria": "配置 {toolName}",
"toggleToolAria": "切换 {toolName}"
"configureToolAria": "配置 {{toolName}}",
"toggleToolAria": "切换 {{toolName}}",
"manageTools": "前往工具",
"edit": "编辑",
"delete": "删除",
"deleteWarning": "您确定要删除工具 \"{{toolName}}\" 吗?",
"unsavedChanges": "您有未保存的更改,如果不保存就离开将会丢失。",
"leaveWithoutSaving": "不保存离开",
"saveAndLeave": "保存并离开",
"customName": "自定义名称",
"customNamePlaceholder": "输入自定义名称(可选)",
"authentication": "认证",
"actions": "操作",
"addAction": "添加操作",
"noActionsFound": "未找到操作",
"url": "URL",
"urlPlaceholder": "输入url",
"method": "方法",
"description": "描述",
"descriptionPlaceholder": "输入描述",
"headers": "请求头",
"queryParameters": "查询参数",
"body": "请求体",
"deleteActionWarning": "您确定要删除操作 \"{{name}}\" 吗?",
"backToAllTools": "返回所有工具",
"save": "保存",
"fieldName": "字段名称",
"fieldType": "字段类型",
"filledByLLM": "由LLM填充",
"fieldDescription": "字段描述",
"value": "值",
"addProperty": "添加属性",
"propertyName": "新属性键",
"add": "添加",
"cancel": "取消",
"addNew": "添加新的",
"name": "名称",
"type": "类型"
}
},
"modals": {
@@ -202,7 +245,20 @@
"promptName": "提示名称",
"promptText": "提示文本",
"save": "保存",
"nameExists": "名称已存在"
"nameExists": "名称已存在",
"deleteConfirmation": "您确定要删除提示'{{name}}'吗?"
},
"chunk": {
"add": "添加块",
"edit": "编辑块",
"title": "标题",
"enterTitle": "输入标题",
"bodyText": "正文",
"promptText": "提示文本",
"update": "更新",
"close": "关闭",
"delete": "删除",
"deleteConfirmation": "您确定要删除此块吗?"
}
},
"sharedConv": {
@@ -238,7 +294,12 @@
"title": "来源",
"text": "来源文本",
"link": "来源链接",
"view_more": "更多{{count}}个来源"
"view_more": "还有{{count}}个来源"
},
"attachments": {
"attach": "附件",
"remove": "删除附件",
"uploadFailed": "上传失败"
},
"retry": "重试"
}

View File

@@ -47,9 +47,6 @@ export default function AddActionModal({
New Action
</h2>
<div className="relative mt-6 px-3">
<span className="absolute -top-2 left-5 z-10 bg-white px-2 text-xs text-gray-4000 dark:bg-[#26272E] dark:text-silver">
Action Name
</span>
<Input
type="text"
value={actionName}
@@ -60,7 +57,8 @@ export default function AddActionModal({
}}
borderVariant="thin"
labelBgClassName="bg-white dark:bg-charleston-green-2"
placeholder={'Enter name'}
placeholder="Action Name"
required={true}
/>
<p
className={`ml-1 mt-2 text-xs italic ${

View File

@@ -97,7 +97,7 @@ export default function AgentDetailsModal({
{sharedToken && (
<div className="mb-1">
<CopyButton
textToCopy={`${baseURL}/agents/shared/${sharedToken}`}
textToCopy={`${baseURL}/shared/agent/${sharedToken}`}
padding="p-1"
/>
</div>
@@ -106,7 +106,7 @@ export default function AgentDetailsModal({
{sharedToken ? (
<div className="flex flex-col flex-wrap items-start gap-2">
<p className="f break-all font-mono text-sm text-gray-700 dark:text-[#ECECF1]">
{`${baseURL}/agents/shared/${sharedToken}`}
{`${baseURL}/shared/agent/${sharedToken}`}
</p>
</div>
) : (

View File

@@ -1,9 +1,10 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import Exit from '../assets/exit.svg';
import Input from '../components/Input';
import { ActiveState } from '../models/misc';
import ConfirmationModal from './ConfirmationModal';
import WrapperModal from './WrapperModal';
export default function ChunkModal({
type,
@@ -22,6 +23,7 @@ export default function ChunkModal({
originalText?: string;
handleDelete?: () => void;
}) {
const { t } = useTranslation();
const [title, setTitle] = React.useState('');
const [chunkText, setChunkText] = React.useState('');
const [deleteModal, setDeleteModal] = React.useState<ActiveState>('INACTIVE');
@@ -30,157 +32,105 @@ export default function ChunkModal({
setTitle(originalTitle || '');
setChunkText(originalText || '');
}, [originalTitle, originalText]);
if (type === 'ADD') {
return (
<div
className={`${
modalState === 'ACTIVE' ? 'visible' : 'hidden'
} fixed left-0 top-0 z-30 flex h-screen w-screen items-center justify-center bg-gray-alpha`}
>
<article className="flex w-11/12 flex-col gap-4 rounded-2xl bg-white shadow-lg dark:bg-[#26272E] sm:w-[620px]">
<div className="relative">
<button
className="absolute right-4 top-3 m-2 w-3"
onClick={() => {
setModalState('INACTIVE');
}}
>
<img className="filter dark:invert" src={Exit} />
</button>
<div className="p-6">
<h2 className="px-3 text-xl font-semibold text-jet dark:text-bright-gray">
Add Chunk
</h2>
<div className="relative mt-6 px-3">
<span className="absolute -top-2 left-5 z-10 bg-white px-2 text-xs text-gray-4000 dark:bg-[#26272E] dark:text-silver">
Title
</span>
<Input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
borderVariant="thin"
placeholder={'Enter title'}
labelBgClassName="bg-white dark:bg-charleston-green-2"
></Input>
</div>
<div className="relative mt-6 px-3">
<div className="rounded-lg border border-silver pb-1 pt-3 dark:border-silver/40">
<span className="absolute -top-2 left-5 rounded-lg bg-white px-2 text-xs text-gray-4000 dark:bg-[#26272E] dark:text-silver">
Body text
</span>
<textarea
id="chunk-body-text"
className="h-60 w-full px-3 outline-none dark:bg-transparent dark:text-white"
value={chunkText}
onChange={(e) => setChunkText(e.target.value)}
aria-label="Prompt Text"
></textarea>
</div>
</div>
<div className="mt-8 flex flex-row-reverse gap-1 px-3">
<button
onClick={() => {
handleSubmit(title, chunkText);
setModalState('INACTIVE');
}}
className="rounded-3xl bg-purple-30 px-5 py-2 text-sm text-white transition-all hover:bg-violets-are-blue"
>
Add
</button>
<button
onClick={() => {
setModalState('INACTIVE');
}}
className="cursor-pointer rounded-3xl px-5 py-2 text-sm font-medium hover:bg-gray-100 dark:bg-transparent dark:text-light-gray dark:hover:bg-[#767183]/50"
>
Close
</button>
</div>
</div>
</div>
</article>
if (modalState !== 'ACTIVE') return null;
const content = (
<div>
<h2 className="px-3 text-xl font-semibold text-jet dark:text-bright-gray">
{t(`modals.chunk.${type === 'ADD' ? 'add' : 'edit'}`)}
</h2>
<div className="relative mt-6 px-3">
<Input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
borderVariant="thin"
placeholder={t('modals.chunk.title')}
labelBgClassName="bg-white dark:bg-charleston-green-2"
/>
</div>
);
} else {
return (
<div
className={`${
modalState === 'ACTIVE' ? 'visible' : 'hidden'
} fixed left-0 top-0 z-30 flex h-screen w-screen items-center justify-center bg-gray-alpha`}
>
<article className="flex w-11/12 flex-col gap-4 rounded-2xl bg-white shadow-lg dark:bg-[#26272E] sm:w-[620px]">
<div className="relative">
<div className="relative mt-6 px-3">
<div className="rounded-lg border border-silver pb-1 pt-3 dark:border-silver/40">
<span className="absolute -top-2 left-5 rounded-lg bg-white px-2 text-xs text-gray-4000 dark:bg-[#26272E] dark:text-silver">
{t('modals.chunk.bodyText')}
</span>
<textarea
id="chunk-body-text"
className="h-60 max-h-60 w-full resize-none px-3 outline-none dark:bg-transparent dark:text-white"
value={chunkText}
onChange={(e) => setChunkText(e.target.value)}
aria-label={t('modals.chunk.promptText')}
></textarea>
</div>
</div>
{type === 'ADD' ? (
<div className="mt-8 flex flex-row-reverse gap-1 px-3">
<button
onClick={() => {
handleSubmit(title, chunkText);
setModalState('INACTIVE');
}}
className="rounded-3xl bg-purple-30 px-5 py-2 text-sm text-white transition-all hover:bg-violets-are-blue"
>
{t('modals.chunk.add')}
</button>
<button
onClick={() => {
setModalState('INACTIVE');
}}
className="cursor-pointer rounded-3xl px-5 py-2 text-sm font-medium hover:bg-gray-100 dark:bg-transparent dark:text-light-gray dark:hover:bg-[#767183]/50"
>
{t('modals.chunk.close')}
</button>
</div>
) : (
<div className="mt-8 flex w-full items-center justify-between px-3">
<button
className="text-nowrap rounded-full border border-solid border-red-500 px-5 py-2 text-sm text-red-500 hover:bg-red-500 hover:text-white"
onClick={() => {
setDeleteModal('ACTIVE');
}}
>
{t('modals.chunk.delete')}
</button>
<div className="flex flex-row-reverse gap-1">
<button
onClick={() => {
handleSubmit(title, chunkText);
setModalState('INACTIVE');
}}
className="rounded-3xl bg-purple-30 px-5 py-2 text-sm text-white transition-all hover:bg-violets-are-blue"
>
{t('modals.chunk.update')}
</button>
<button
className="absolute right-4 top-3 m-2 w-3"
onClick={() => {
setModalState('INACTIVE');
}}
className="cursor-pointer rounded-3xl px-5 py-2 text-sm font-medium hover:bg-gray-100 dark:bg-transparent dark:text-light-gray dark:hover:bg-[#767183]/50"
>
<img className="filter dark:invert" src={Exit} />
{t('modals.chunk.close')}
</button>
<div className="p-6">
<h2 className="px-3 text-xl font-semibold text-jet dark:text-bright-gray">
Edit Chunk
</h2>
<div className="relative mt-6 px-3">
<Input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
borderVariant="thin"
placeholder={'Enter title'}
labelBgClassName="bg-white dark:bg-charleston-green-2"
></Input>
</div>
<div className="relative mt-6 px-3">
<div className="rounded-lg border border-silver pb-1 pt-3 dark:border-silver/40">
<span className="absolute -top-2 left-5 rounded-lg bg-white px-2 text-xs text-gray-4000 dark:bg-[#26272E] dark:text-silver">
Body text
</span>
<textarea
id="chunk-body-text"
className="h-60 w-full px-3 outline-none dark:bg-transparent dark:text-white"
value={chunkText}
onChange={(e) => setChunkText(e.target.value)}
aria-label="Prompt Text"
></textarea>
</div>
</div>
<div className="mt-8 flex w-full items-center justify-between px-3">
<button
className="text-nowrap rounded-full border border-solid border-red-500 px-5 py-2 text-sm text-red-500 hover:bg-red-500 hover:text-white"
onClick={() => {
setDeleteModal('ACTIVE');
}}
>
Delete
</button>
<div className="flex flex-row-reverse gap-1">
<button
onClick={() => {
handleSubmit(title, chunkText);
setModalState('INACTIVE');
}}
className="rounded-3xl bg-purple-30 px-5 py-2 text-sm text-white transition-all hover:bg-violets-are-blue"
>
Update
</button>
<button
onClick={() => {
setModalState('INACTIVE');
}}
className="cursor-pointer rounded-3xl px-5 py-2 text-sm font-medium hover:bg-gray-100 dark:bg-transparent dark:text-light-gray dark:hover:bg-[#767183]/50"
>
Close
</button>
</div>
</div>
</div>
</div>
</article>
</div>
)}
</div>
);
return (
<>
<WrapperModal
close={() => setModalState('INACTIVE')}
className="sm:w-[620px]"
>
{content}
</WrapperModal>
{type === 'EDIT' && (
<ConfirmationModal
message="Are you sure you want to delete this chunk?"
message={t('modals.chunk.deleteConfirmation')}
modalState={deleteModal}
setModalState={setDeleteModal}
handleSubmit={
@@ -190,9 +140,9 @@ export default function ChunkModal({
/* no-op */
}
}
submitLabel="Delete"
submitLabel={t('modals.chunk.delete')}
/>
</div>
);
}
)}
</>
);
}

View File

@@ -25,6 +25,7 @@ export default function ConfigToolModal({
const { t } = useTranslation();
const token = useSelector(selectToken);
const [authKey, setAuthKey] = React.useState<string>('');
const [customName, setCustomName] = React.useState<string>('');
const handleAddTool = (tool: AvailableToolType) => {
userService
@@ -34,6 +35,7 @@ export default function ConfigToolModal({
displayName: tool.displayName,
description: tool.description,
config: { token: authKey },
customName: customName,
actions: tool.actions,
status: true,
},
@@ -58,6 +60,16 @@ export default function ConfigToolModal({
{t('modals.configTool.type')}:{' '}
<span className="font-semibold">{tool?.name}</span>
</p>
<div className="mt-6 px-3">
<Input
type="text"
value={customName}
onChange={(e) => setCustomName(e.target.value)}
borderVariant="thin"
placeholder="Enter custom name (optional)"
labelBgClassName="bg-white dark:bg-charleston-green-2"
/>
</div>
<div className="mt-6 px-3">
<Input
type="text"

View File

@@ -32,12 +32,7 @@ export default function ConfirmationModal({
return (
<>
{modalState === 'ACTIVE' && (
<WrapperModal
close={() => {
setModalState('INACTIVE');
handleCancel && handleCancel();
}}
>
<WrapperModal close={() => setModalState('INACTIVE')}>
<div className="relative">
<div>
<p className="font-base mb-1 w-[90%] break-words text-lg text-jet dark:text-bright-gray">

View File

@@ -1,4 +1,5 @@
import React, { useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import Exit from '../assets/exit.svg';
@@ -45,7 +46,7 @@ export default function WrapperModal({
};
}, [close]);
return (
const modalContent = (
<div className="fixed left-0 top-0 z-30 flex h-screen w-screen items-center justify-center bg-gray-alpha bg-opacity-50">
<div
ref={modalRef}
@@ -63,4 +64,6 @@ export default function WrapperModal({
</div>
</div>
);
return createPortal(modalContent, document.body);
}

Some files were not shown because too many files have changed in this diff Show More