Compare commits

...

968 Commits

Author SHA1 Message Date
dependabot[bot]
37c672b891 chore(deps): bump actions/setup-python from 5 to 6
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-04 20:22:48 +00:00
Alex
f7db22edff Merge pull request #1937 from ManishMadan2882/main
Connectors: Google Drive Ingestion
2025-09-04 12:05:15 +01:00
ManishMadan2882
5a9bc6d2bf (feat:connector) infinite scroll file pick 2025-09-04 08:35:41 +05:30
ManishMadan2882
f7f6042579 (feat:connector) paginate files 2025-09-04 07:58:12 +05:30
ManishMadan2882
c4a598f3d3 (lint-fix) ruff 2025-09-03 19:29:34 +05:30
ManishMadan2882
7e2cbdd88c (feat:connector) redirect url as backend overhead 2025-09-03 09:57:13 +05:30
ManishMadan2882
3b3a04a249 (feat:connector) sync fixes UI, minor refactor 2025-09-02 20:28:23 +05:30
ManishMadan2882
f9b2c95695 (feat:connector) sync, simply re-ingest 2025-09-02 18:06:04 +05:30
ManishMadan2882
c2c18e8319 (feat:connector,fe) sync api, notification 2025-09-02 13:36:41 +05:30
ManishMadan2882
384ad3e0ac (feat:connector) raw sync flow 2025-09-02 13:34:31 +05:30
ManishMadan2882
8c986aaa7f Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-09-01 12:05:17 +05:30
ManishMadan2882
bb4ea76d30 (fix:connectorTree) path navigation fn 2025-09-01 12:04:58 +05:30
ManishMadan2882
2868e47cf8 (feat:connector) provider metadata, separate fe nested display 2025-08-29 18:05:58 +05:30
GH Action - Upstream Sync
e0adc3e5d5 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-08-29 01:36:09 +00:00
ManishMadan2882
e55d1a5865 (feat:connector,auth) consider user_id 2025-08-29 02:13:51 +05:30
ManishMadan2882
018273c6b2 (feat:connector) refactor, updated routes FE 2025-08-29 01:06:40 +05:30
Alex
44b8a11c04 Merge pull request #1936 from Ankit-Matth/feature/load-containers-from-dockerhub
Speed up scripts by loading containers from docker hub
2025-08-28 14:48:08 +01:00
Siddhant Rai
56e5aba559 fix: correct frontend image name in docker-compose configuration 2025-08-28 18:21:54 +05:30
Alex
46904ccd54 feat: add theme color meta tags for light and dark modes 2025-08-28 11:36:42 +01:00
Siddhant Rai
5b7c7a4471 fix: update Docker images in docker-compose to use 'develop' tag 2025-08-28 12:11:06 +05:30
Siddhant Rai
9da4215d1f feat: implement Docker Hub integration for building and pushing images in CI/CD workflow 2025-08-28 12:01:04 +05:30
ManishMadan2882
f39ac9945f (feat:auth) follow connector-session 2025-08-28 00:53:19 +05:30
ManishMadan2882
a0cc2e4d46 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-08-28 00:51:29 +05:30
ManishMadan2882
4065041a9f (feat:connectors) separate routes, namespace 2025-08-28 00:51:09 +05:30
GH Action - Upstream Sync
f08067a161 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-08-27 01:36:38 +00:00
Alex
545caacfa3 feat: prevent NUL character ingestion failures 2025-08-26 23:30:57 +01:00
Alex
a06f646637 feat: enhance tool call error handling 2025-08-26 22:37:21 +01:00
ManishMadan2882
578c68205a (feat:connectors) abstracting auth, base class 2025-08-26 02:46:36 +05:30
ManishMadan2882
f09f1433a9 (feat:connectors) separate layer 2025-08-26 01:38:36 +05:30
ManishMadan2882
15a9e97a1e (feat:ingest_connectors) spread config params 2025-08-26 00:56:39 +05:30
Ankit Matth
b3af4ee50b speed up scripts by using docker hub 2025-08-24 08:59:19 +05:30
GH Action - Upstream Sync
e25b988dc8 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-08-23 01:35:35 +00:00
ManishMadan2882
2410bd8654 (fix:driveLoader) folder ingesting 2025-08-22 19:07:52 +05:30
Alex
44d21ab703 fix: passing sources and chunk if agent is shared 2025-08-22 13:36:31 +01:00
Alex
e283957c8f Fix source field retrieval in SharedAgent to handle DBRef correctly 2025-08-22 11:41:35 +01:00
ManishMadan2882
b1210c4902 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-08-22 13:36:52 +05:30
ManishMadan2882
e7430f0fbc (feat:googleDrive,fe) file tree 2025-08-22 13:36:32 +05:30
ManishMadan2882
92d6ae54c3 (fix:google-oauth) no explicit datetime compare 2025-08-22 13:35:03 +05:30
ManishMadan2882
f82be23ca9 (feat:ingestion) external drive connect 2025-08-22 13:33:21 +05:30
ManishMadan2882
8c3f75e3e2 (feat:ingestion) google drive loader 2025-08-22 13:32:40 +05:30
GH Action - Upstream Sync
193d59f193 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-08-22 01:38:59 +00:00
ManishMadan2882
c2bebbaefa (feat:oauth/drive) raw fe integrate 2025-08-22 03:29:57 +05:30
Alex
7ae5a9c5a5 Refactor diagramId initialization to use a combination of Date.now() and random string for uniqueness 2025-08-21 14:50:37 +01:00
ManishMadan2882
3b69bea23d (chore:settings)addefault oath creds 2025-08-21 17:02:23 +05:30
ManishMadan2882
ab05726b99 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-08-21 02:46:56 +05:30
ManishMadan2882
b2b04268e9 (feat:drive) oauth flow 2025-08-21 02:46:32 +05:30
Alex
927d10d66e Update README.md 2025-08-17 12:44:47 +03:00
Alex
b67329623c Update README.md 2025-08-16 13:51:40 +03:00
Alex
6a02bcf15b Merge pull request #1873 from ManishMadan2882/main
Sources are the new Docs
2025-08-13 18:24:35 +01:00
Alex
cd0fbf79a3 Merge pull request #1924 from siiddhantt/feat/agent-schema-response
feat: add support for structured output and JSON schema validation
2025-08-13 17:33:29 +01:00
Alex
15d2d0115b Merge branch 'main' into feat/agent-schema-response 2025-08-13 17:12:26 +01:00
ManishMadan2882
d1a0fe6e91 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-08-13 17:39:59 +05:30
ManishMadan2882
1db80d140f (fix) search dropdowns 2025-08-13 17:39:39 +05:30
Siddhant Rai
896dcf1f9e feat: add support for structured output and JSON schema validation 2025-08-13 13:29:51 +05:30
GH Action - Upstream Sync
819a12fb49 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-08-13 01:45:41 +00:00
Alex
c68273706c Merge pull request #1920 from hanzalahwaheed/fix/response-bubble-btns
fix: always show the response bubble buttons.
2025-08-13 00:14:01 +01:00
Hanzalah Waheed
6bb0cd535a fix: rm redundant states. track feedback state w prop var 2025-08-13 02:36:58 +04:00
Hanzalah Waheed
cb9ec69cf6 chore: refactor code to use ternary operator for error type check 2025-08-13 02:31:25 +04:00
Hanzalah Waheed
143854fa81 fix: show both like and dislike buttons 2025-08-13 02:11:47 +04:00
ManishMadan2882
2f48a3d7d5 (feat:chunks) consistent path header 2025-08-13 02:53:32 +05:30
ManishMadan2882
ec95dafe1e (feat:sources) matching the figma 2025-08-13 01:35:23 +05:30
ManishMadan2882
3d1fe724e5 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-08-12 18:05:11 +05:30
ManishMadan2882
5c615d6f2d (feat:sources) card ui 2025-08-12 18:04:40 +05:30
GH Action - Upstream Sync
d72558eb36 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-08-12 01:43:57 +00:00
ManishMadan2882
65c33ad915 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-08-12 03:09:13 +05:30
ManishMadan2882
9be128a963 (feat:sources) closer to figma,ux 2025-08-12 03:08:47 +05:30
Hanzalah Waheed
eb05132008 fix: always show the response bubble buttons. 2025-08-12 00:16:21 +04:00
Alex
f94a093e8c fix: truncate long text fields to prevent overflow in logs and sources 2025-08-11 14:56:31 +01:00
GH Action - Upstream Sync
0d0c2daf64 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-08-09 01:44:41 +00:00
ManishMadan2882
823d948b25 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-08-08 21:59:01 +05:30
Alex
56831fbcf2 Merge pull request #1917 from ManishMadan2882/fix/agent_prompts
Fixes missing attributes on shared agents
2025-08-08 16:30:09 +01:00
ManishMadan2882
bf49b9cb88 (fix/shared_agent)missing main attr 2025-08-08 20:44:09 +05:30
GH Action - Upstream Sync
e01adffbad Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-08-08 01:54:52 +00:00
Alex
08a5d52d82 Update README.md 2025-08-07 17:20:45 +03:00
ManishMadan2882
fdae235742 (feat:sources) i18n 2025-08-07 12:53:12 +05:30
GH Action - Upstream Sync
9903fad1e9 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-08-07 01:55:18 +00:00
Alex
14bbd5338d Merge pull request #1909 from siiddhantt/main
fix: remove unnecessary parameter from fetchPreviewAnswer
2025-08-06 19:21:59 +01:00
Siddhant Rai
4a236c2f6f fix: remove unnecessary parameter from fetchPreviewAnswer 2025-08-06 22:34:06 +05:30
Alex
0a8cdbd7f1 fix: update token selector in FileTreeComponent 2025-08-06 12:44:21 +01:00
Alex
94c49843be Merge pull request #1906 from arc53/feat/pg-vector
feat: implement PGVectorStore for PostgreSQL vector storage
2025-08-06 10:42:34 +01:00
Alex
9281fac898 fix: improve error logging for index creation and add PARSE_IMAGE_REMOTE setting 2025-08-06 10:40:20 +01:00
GH Action - Upstream Sync
0b2736f454 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-08-06 01:55:04 +00:00
ManishMadan2882
ae116b0d0d (fix:agentPreview) build err 2025-08-06 02:55:39 +05:30
ManishMadan2882
ba260e3382 (fix:faiss) not save tmp dir 2025-08-06 02:53:39 +05:30
Alex
1282e7687f fix: add error handling for index creation in user and agents collections 2025-08-05 17:19:06 +01:00
Alex
b1d8266eef feat: implement PGVectorStore for PostgreSQL vector storage 2025-08-05 13:54:39 +01:00
Alex
7acae6935b Merge pull request #1905 from arc53/fix-qdrant
fix: qdrant issues
2025-08-05 12:26:24 +01:00
Alex
092c01cae7 fix: ruff lint 2025-08-05 12:22:33 +01:00
Alex
56a1066c30 fix: qdrant issues 2025-08-05 12:19:18 +01:00
ManishMadan2882
1356d71839 (lint) ruff fix 2025-08-05 15:37:39 +05:30
ManishMadan2882
1eb011e8c3 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-08-05 15:28:51 +05:30
ManishMadan2882
e349eb28b0 (fix:update_chunk) data integrity, uplod back faiss 2025-08-05 06:31:00 +05:30
ManishMadan2882
b000b235a2 (feat:sources) renamed docs,fe 2025-08-05 05:23:29 +05:30
ManishMadan2882
16fe92282e (fix:chunks) responsive, editing controls 2025-08-05 03:04:37 +05:30
ManishMadan2882
e218e88cf4 (fix:chunks) alignment and ui 2025-08-04 19:28:15 +05:30
ManishMadan2882
888ea81a32 (feat:fil_management) serialising updates, queue 2025-08-04 17:27:12 +05:30
ManishMadan2882
735fab7640 (feat:storage) sync base 2025-08-04 16:36:38 +05:30
ManishMadan2882
45745c2a47 (feat:docs) skeleton loader 2025-08-04 16:35:24 +05:30
Alex
4caff0fcf6 fix: enhance error logging for malformed request in stream route 2025-08-04 11:41:41 +01:00
Alex
762ea6ce7f Merge pull request #1866 from arc53/dependabot/npm_and_yarn/frontend/tailwindcss-4.1.11
build(deps-dev): bump tailwindcss from 4.1.10 to 4.1.11 in /frontend
2025-08-02 23:49:54 +01:00
ManishMadan2882
8b4f6553f3 (fix:menu) left and right 2025-08-02 02:08:35 +05:30
ManishMadan2882
a61e44d175 (feat:dir_tree) improvement 2025-08-02 01:48:43 +05:30
ManishMadan2882
e1b1558fc9 (feat:storage) rm dir 2025-08-02 00:54:09 +05:30
ManishMadan2882
53225bda4e (feat:reingestion) spit directories 2025-08-02 00:49:15 +05:30
ManishMadan2882
5212769848 (feat:reingest) UI, polling 2025-08-01 01:25:37 +05:30
ManishMadan2882
d5ded3c9f4 (feat:reingest) eat and spit specific chunks 2025-08-01 01:14:48 +05:30
ManishMadan2882
c92d778894 (feat:chunker) do not combine text 2025-07-31 02:13:55 +05:30
ManishMadan2882
829abd1ad6 (fix:textarea) consistent line indxs 2025-07-30 21:00:00 +05:30
ManishMadan2882
266d256a07 (feat:sources) management, simple re-ingest 2025-07-30 01:57:40 +05:30
Alex
8380cac3e7 Merge pull request #1900 from naaa760/docs/auth-type-configuration
Docs: Expand and Clarify AUTH_TYPE Configuration and Authentication Methods (#1882)
2025-07-28 23:07:40 +01:00
ManishMadan2882
a24652f901 (feat:chunks) update iff changed 2025-07-28 19:35:41 +05:30
ManishMadan2882
2d203d3c70 (fix:chunks)responsive 2025-07-28 18:01:51 +05:30
Alex
48d21600da Merge pull request #1896 from siiddhantt/feat/rework-answer-routes
feat: answer routes re-structure for better maintainability and reuse
2025-07-26 14:18:10 +01:00
ManishMadan2882
2508d0fbb3 (fix:chunks) preserve paths 2025-07-26 00:27:39 +05:30
ManishMadan2882
e90e80c289 (fix:chunks) also count tokens 2025-07-26 00:16:45 +05:30
ManishMadan2882
5e4748f9d9 (fix:faiss) rely on storage abstrct 2025-07-26 00:14:17 +05:30
Siddhant Rai
212952f3e9 fix: allow api call in stream route + get_prompt error 2025-07-25 16:17:18 +05:30
naaa760
f99b6496c5 update 2025-07-25 14:48:49 +05:30
ManishMadan2882
67423d51b9 (feat:chunks) ask to edit, ui 2025-07-25 04:05:06 +05:30
ManishMadan2882
58465ece65 (feat:chunks) server-side filter on search 2025-07-25 01:43:50 +05:30
ManishMadan2882
8ede3a0173 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-07-23 20:48:00 +05:30
ManishMadan2882
ad2f0f8950 (chore:chunks) i18n 2025-07-23 20:47:36 +05:30
Siddhant Rai
76973a4b4c feat: answer routes re-structure for better maintainability and reuse 2025-07-23 20:07:42 +05:30
GH Action - Upstream Sync
b198e2e029 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-07-23 01:52:41 +00:00
ManishMadan2882
4d6ea401b5 (feat:chunks) line numbered editor 2025-07-23 03:36:18 +05:30
ManishMadan2882
b00c4cc3b6 (feat:chunk) editing mode 2025-07-23 02:22:56 +05:30
Alex
4185e64c65 Merge pull request #1893 from arc53/fix-attachment-bugs
fix: replace secure_filename with safe_filename for attachment handling
2025-07-22 16:24:55 +01:00
ManishMadan2882
6eb2c884a2 (refactor) separation in chunks/files view 2025-07-22 19:36:52 +05:30
Alex
6c0362a4cf fix: replace secure_filename with safe_filename for attachment handling 2025-07-22 12:56:17 +01:00
ManishMadan2882
50b1755a63 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-07-21 16:31:57 +05:30
ManishMadan2882
ff3c7eb5fb (fix:delete_old) comply with storage abtrctn 2025-07-21 16:31:42 +05:30
ManishMadan2882
3755316d49 (fix:chunks) responsive design 2025-07-21 16:30:30 +05:30
GH Action - Upstream Sync
f952046847 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-07-19 01:47:09 +00:00
Alex
969cdb4a63 Merge pull request #1890 from siiddhantt/feat/enhance-agents
feat: enhance prompt selection in new agents
2025-07-18 13:07:45 +01:00
ManishMadan2882
f336d44595 (feat:chunks) search in dir 2025-07-18 15:03:23 +05:30
Siddhant Rai
a53f93c195 feat: enhance dropdown component and prompts integration 2025-07-18 14:02:29 +05:30
GH Action - Upstream Sync
fcb334ce33 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-07-17 01:50:59 +00:00
ManishMadan2882
8ddf04a904 (feat:chunks) use common header, navigate 2025-07-17 03:08:01 +05:30
ManishMadan2882
29698ca169 (feat:chunks) redesigned 2025-07-17 02:16:40 +05:30
ManishMadan2882
a9baf7436a Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-07-17 01:05:59 +05:30
ManishMadan2882
99a8962183 (fix/docs) menu event capture 2025-07-17 01:05:24 +05:30
Alex
afc5b15a6b Merge pull request #1887 from Krrish0902/fix-issue-1854
fix: Removed incorrect www from URL in DocsGPT Docs frontend (#1854)
2025-07-16 13:20:05 +01:00
GH Action - Upstream Sync
b6ab508e27 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-07-16 01:50:25 +00:00
ananthakrishnan
789e65557a fix: Removed incorrect www from URL in DocsGPT Docs frontend (#1854) 2025-07-15 22:02:02 +05:30
ManishMadan2882
8a7806ab2d (feat:nested source view) file tree, chunks display 2025-07-15 19:15:40 +05:30
Alex
493303e103 Merge pull request #1886 from arc53/copilot/fix-1878
🐛 Fix conversation summary prompt to use user query language
2025-07-15 12:57:14 +01:00
ManishMadan2882
1d9af05e9e (feat:storage) is dir fnc 2025-07-15 15:34:16 +05:30
ManishMadan2882
5b07c5f2e8 (feat:ingestion) unzip, extract and store 2025-07-15 15:31:26 +05:30
copilot-swe-agent[bot]
2a4ec0cf5b Fix conversation summary prompt to use user query language
Co-authored-by: dartpain <15183589+dartpain@users.noreply.github.com>
2025-07-15 09:33:52 +00:00
copilot-swe-agent[bot]
a00c44386e Initial plan 2025-07-15 09:29:22 +00:00
ManishMadan2882
a38d71bbfb (feat:get_chunks) filtered by relative path 2025-07-15 13:38:19 +05:30
ManishMadan2882
a24a3f868c (feat:dir-structure) adding route 2025-07-15 13:38:19 +05:30
ManishMadan2882
f60c516185 (feat:dir_tree) table with folder contents 2025-07-15 13:38:19 +05:30
Manish Madan
26f4646304 Merge branch 'arc53:main' into main 2025-07-14 23:02:36 +05:30
ananthakrishnan
3a351f67e6 fix: correct agent tools name when creating new agent (#1877) 2025-07-14 20:20:55 +05:30
dependabot[bot]
e7c09cb91e build(deps-dev): bump tailwindcss from 4.1.10 to 4.1.11 in /frontend
Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) from 4.1.10 to 4.1.11.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.11/packages/tailwindcss)

---
updated-dependencies:
- dependency-name: tailwindcss
  dependency-version: 4.1.11
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-14 09:02:48 +00:00
Alex
ae1a6ef303 Merge pull request #1883 from siiddhantt/feat/agent-validation-enhance
feat: improve interactivity of publishing/drafting of agents
2025-07-14 09:58:12 +01:00
Siddhant Rai
2ff477a339 feat(agent): enhance validation for agent creation by checking required and invalid fields 2025-07-14 12:53:09 +05:30
Siddhant Rai
793f3fb683 refactor(NewAgent): remove debug logs 2025-07-12 12:36:18 +05:30
Siddhant Rai
a472ee7602 feat: add validation for required fields and improve agent creation logic 2025-07-12 12:30:00 +05:30
GH Action - Upstream Sync
c62040e232 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-07-11 01:49:09 +00:00
Alex
2e7cb510ae Update README.md 2025-07-10 17:27:08 +03:00
Alex
dbe45904d7 Merge pull request #1881 from siiddhantt/fix/glacier-images
fix: s3 storage class for image upload
2025-07-10 12:00:55 +01:00
Siddhant Rai
5623734276 feat(storage): enhance save_file method to accept storage class parameter 2025-07-10 15:34:52 +05:30
ManishMadan2882
d3b592bffc Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-07-09 01:29:59 +05:30
ManishMadan2882
4fcbdae5bf (feat:docs) cards UI 2025-07-08 02:46:34 +05:30
ManishMadan2882
ca95d7275a (feat:dateTimeUtils) localise weekday format 2025-07-08 02:32:18 +05:30
Manish Madan
61baf3701c Merge branch 'arc53:main' into main 2025-07-04 02:26:24 +05:30
ManishMadan2882
bbce872ac5 (fix:chunker) combine metadata as well 2025-07-04 02:19:58 +05:30
ManishMadan2882
0f7ebcd8e4 (feat:dir-reader) store mime types, file size in db 2025-07-03 18:09:19 +05:30
ManishMadan2882
82fc19e7b7 (fix:dir-reader) conflict of same filename in dir 2025-07-03 17:28:12 +05:30
Alex
839a12bed4 Merge pull request #1869 from siiddhantt/refactor/tools-dict
refactor: update user tools dict to use enumeration based key
2025-07-03 12:51:33 +09:00
ManishMadan2882
2ef23fe1b3 (feat:dir-reader) maintain dir structure in db 2025-07-03 01:24:22 +05:30
ManishMadan2882
fd905b1a06 (feat:dir-reader) save tokens with filenames 2025-07-02 16:30:29 +05:30
Siddhant Rai
1372210004 refactor: update user tools dict to use enumeration based key 2025-07-02 10:37:32 +05:30
ManishMadan2882
ade704d065 (refactor:ingestion) pass file path once 2025-07-01 04:00:57 +05:30
Alex
42f48649b9 Merge pull request #1843 from arc53/dependabot/npm_and_yarn/frontend/tailwindcss-4.1.10
build(deps-dev): bump tailwindcss from 3.4.17 to 4.1.10 in /frontend
2025-06-28 13:22:47 +09:00
ManishMadan2882
0b08e8b617 (fix:nav) settings gear dimesions 2025-06-27 22:16:01 +05:30
ManishMadan2882
926b2f1a1b clean 2025-06-25 19:03:24 +05:30
ManishMadan2882
1770a1a45f Merge branch 'main' of https://github.com/arc53/DocsGPT into dependabot/npm_and_yarn/frontend/tailwindcss-4.1.10 2025-06-25 18:59:51 +05:30
ManishMadan2882
50ed2a64c6 (fix/ddropdown) use complete classNames, interpolation 2025-06-25 18:26:33 +05:30
Alex
2332344988 Merge pull request #1861 from arc53/docs-agents-update
Agent docs upd
2025-06-25 09:23:09 +01:00
Alex
7ccc8cdc58 Merge pull request #1855 from siiddhantt/refactor/ddg-brave-tools
refactor: ddg and brave tools with sources fix
2025-06-25 09:21:38 +01:00
ManishMadan2882
ecec9f913e (fix:messageInput) modern tailwind syntx 2025-06-25 02:15:03 +05:30
ManishMadan2882
777f40fc5e (fix:conflicting global css) layered styles 2025-06-25 01:11:39 +05:30
Pavel
327ae35420 Agent docs upd
1. Added a page about interacting with agent API.
2. Added a page about interacting with agent webhooks.
3. Fixed small bug with /api/answer
2025-06-24 16:48:12 +02:00
Siddhant Rai
0d48159da8 feat: enhance modal functionality with reset and confirmation handlers 2025-06-24 02:14:15 +05:30
Siddhant Rai
d36f12a4ea feat: add DuckDuckGo icon and remove sources skeleton 2025-06-24 02:11:58 +05:30
Siddhant Rai
709488beb1 fix: sources not getting set on stream end 2025-06-23 09:23:18 +05:30
Siddhant Rai
a9e4583695 refactor: use DuckDuckGo and Brave as tools instead of retrievers 2025-06-23 09:22:17 +05:30
ManishMadan2882
4702dec933 (chore:tailwindcss) via upgrade tool 2025-06-22 18:31:26 +05:30
ManishMadan2882
e6352dd691 Revert "build(deps-dev): bump tailwindcss from 3.4.17 to 4.1.10 in /frontend"
This reverts commit 240ea3b857.
2025-06-22 18:16:41 +05:30
dependabot[bot]
240ea3b857 build(deps-dev): bump tailwindcss from 3.4.17 to 4.1.10 in /frontend
Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) from 3.4.17 to 4.1.10.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.10/packages/tailwindcss)

---
updated-dependencies:
- dependency-name: tailwindcss
  dependency-version: 4.1.10
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-22 12:08:36 +00:00
Alex
f0908af3c0 Merge pull request #1829 from arc53/dependabot/npm_and_yarn/frontend/multi-d4a34be08c
build(deps): bump react and @types/react in /frontend
2025-06-22 12:07:44 +01:00
ManishMadan2882
6834961dd1 (fix:types) stricter in v19 2025-06-20 23:11:53 +05:30
Alex
b404162364 fix: fallback to OpenAILLMHandler when no handler class is found 2025-06-20 16:08:40 +01:00
Alex
e879ef805f Merge pull request #1851 from ManishMadan2882/main
Fixed conflict while switching conversations, separated concerns
2025-06-20 13:07:24 +01:00
ManishMadan2882
7077ca5e98 (chore/upgrade) migrate to react v19 2025-06-20 17:36:28 +05:30
GH Action - Upstream Sync
a1e6978c8f Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-06-20 01:44:14 +00:00
Alex
584391dd59 Update README.md 2025-06-19 17:34:19 +03:00
dependabot[bot]
bab3ae809c build(deps): bump react and @types/react in /frontend
Bumps [react](https://github.com/facebook/react/tree/HEAD/packages/react) and [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react). These dependencies needed to be updated together.

Updates `react` from 18.3.1 to 19.1.0
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.1.0/packages/react)

Updates `@types/react` from 18.3.23 to 19.1.6
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: react
  dependency-version: 19.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
- dependency-name: "@types/react"
  dependency-version: 19.1.6
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-19 11:02:29 +00:00
Manish Madan
c78518baf0 Merge pull request #1827 from arc53/dependabot/npm_and_yarn/frontend/react-dropzone-14.3.8
build(deps): bump react-dropzone from 14.3.5 to 14.3.8 in /frontend
2025-06-19 16:31:06 +05:30
dependabot[bot]
556d7e0497 build(deps): bump react-dropzone from 14.3.5 to 14.3.8 in /frontend
Bumps [react-dropzone](https://github.com/react-dropzone/react-dropzone) from 14.3.5 to 14.3.8.
- [Release notes](https://github.com/react-dropzone/react-dropzone/releases)
- [Commits](https://github.com/react-dropzone/react-dropzone/compare/v14.3.5...v14.3.8)

---
updated-dependencies:
- dependency-name: react-dropzone
  dependency-version: 14.3.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-19 10:59:31 +00:00
Manish Madan
2d27936dab Merge pull request #1826 from arc53/dependabot/npm_and_yarn/frontend/reduxjs/toolkit-2.8.2
build(deps): bump @reduxjs/toolkit from 2.5.1 to 2.8.2 in /frontend
2025-06-19 16:27:30 +05:30
GH Action - Upstream Sync
0cc22de545 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-06-19 01:45:04 +00:00
Alex
63f6127049 Revert "Update README.md"
This reverts commit 55f60a9fe1.
2025-06-18 22:26:38 +01:00
Alex
f34e00c986 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-06-18 22:26:19 +01:00
Alex
55f60a9fe1 Update README.md 2025-06-18 22:26:11 +01:00
Alex
7da3618e0c Update README.md 2025-06-18 22:25:47 +01:00
Alex
56bfa98633 Merge remote-tracking branch 'upstream/main' 2025-06-18 22:18:06 +01:00
Alex
96f6188722 Initial commit 2025-06-18 22:17:23 +01:00
Manish Madan
aa9d359039 Merge branch 'arc53:main' into main 2025-06-19 02:20:08 +05:30
ManishMadan2882
cef5731028 (feat:nav) shut sidebar on click outside 2025-06-19 02:19:29 +05:30
ManishMadan2882
5bc28bd4fd (fix:prompts) show delete only when possible 2025-06-19 02:18:20 +05:30
ManishMadan2882
55a1d867c3 (refactor/converstationSlice) separate preview 2025-06-19 02:16:03 +05:30
Alex
6c3a79802e Merge pull request #1849 from siiddhantt/feat/upload-agent-logo
feat: enhance agent management with image upload and retrieval
2025-06-18 17:45:51 +01:00
Siddhant Rai
c35c5e0793 Refactor image upload handling and add URL strategy setting 2025-06-18 21:54:44 +05:30
ManishMadan2882
7bc83caa99 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-06-18 19:42:16 +05:30
ManishMadan2882
3aceca63c6 (fix/bubble) evenly padded 2025-06-18 19:41:52 +05:30
ManishMadan2882
9bc166ffd4 (fix:convSlice) append to right conv id 2025-06-18 19:41:20 +05:30
Siddhant Rai
fc01b90007 Add tailwind-merge dependency to package.json and package-lock.json 2025-06-18 19:00:59 +05:30
Siddhant Rai
e35f1d70e4 - Added image upload functionality for agents in the backend and frontend.
- Implemented image URL generation based on storage strategy (S3 or local).
- Updated agent creation and update endpoints to handle image files.
- Enhanced frontend components to display agent images with fallbacks.
- New API endpoint to serve images from storage.
- Refactored API client to support FormData for file uploads.
- Improved error handling and logging for image processing.
2025-06-18 18:17:20 +05:30
Siddhant Rai
cab1f3787a Refactor S3 storage implementation and enhance file handling
- Improved code readability by reorganizing imports and formatting.
- Updated S3Storage class to handle file uploads and downloads more efficiently.
- Added a new function to generate image URLs based on settings.
- Enhanced file listing and processing methods for better error handling.
- Introduced a FileUpload component for improved file upload experience in the frontend.
- Updated agent management components to support image uploads and previews.
- Added new SVG assets for UI enhancements.
- Modified API client to support FormData for file uploads.
2025-06-18 18:04:44 +05:30
Alex
bb42f4cbc1 Merge pull request #1846 from ManishMadan2882/main
Agents Details: UI update, external links
2025-06-17 14:31:16 +01:00
ManishMadan2882
98dc418a51 (feat:agent-details) redirect on url 2025-06-17 18:30:11 +05:30
ManishMadan2882
322b4eb18c Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-06-17 16:04:33 +05:30
ManishMadan2882
7f1cc30ed8 (feat:agent-details) test, learn more redirects 2025-06-17 16:04:08 +05:30
ManishMadan2882
7b45a6b956 (feat:agentDetails) copy button ui 2025-06-17 12:03:36 +05:30
Alex
e36769e70f Merge pull request #1844 from ManishMadan2882/main
Collapsible Question bubbles
2025-06-15 16:36:37 +01:00
ManishMadan2882
bd4a4cc4af (feat:question) match design 2025-06-14 02:32:59 +05:30
ManishMadan2882
8343fe63cb (feat:bubble) collapsable questions 2025-06-14 02:02:36 +05:30
Alex
7d89fb8461 fix: lint 2025-06-13 01:14:09 +01:00
Alex
098955d230 fix paths in docker compose 2025-06-13 01:11:22 +01:00
Alex
d254d14928 Merge pull request #1838 from ManishMadan2882/main
Fixes ingestion of file with non-ascii characters in name
2025-06-12 09:52:03 +01:00
GH Action - Upstream Sync
0a3e8ca535 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-06-12 01:43:21 +00:00
ManishMadan2882
b8a10e0962 (fix:ingestion) display names are separate 2025-06-12 00:57:46 +05:30
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
ManishMadan2882
44b6ec25a2 clean 2025-06-11 21:18:37 +05:30
ManishMadan2882
1b84d1fa9d Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-06-11 21:04:57 +05:30
ManishMadan2882
78d5ed2ed2 (fix:ingestion) uuid for non-ascii filename 2025-06-11 21:04:50 +05:30
ManishMadan2882
142477ab9b (feat:safe_filename) handles case of non-ascii char 2025-06-11 21:03:38 +05:30
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
ManishMadan2882
8b3e960be0 (feat:ingestion) store filepath from now 2025-06-11 16:00:09 +05:30
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
dependabot[bot]
3da7cba06c build(deps): bump @reduxjs/toolkit from 2.5.1 to 2.8.2 in /frontend
Bumps [@reduxjs/toolkit](https://github.com/reduxjs/redux-toolkit) from 2.5.1 to 2.8.2.
- [Release notes](https://github.com/reduxjs/redux-toolkit/releases)
- [Commits](https://github.com/reduxjs/redux-toolkit/compare/v2.5.1...v2.8.2)

---
updated-dependencies:
- dependency-name: "@reduxjs/toolkit"
  dependency-version: 2.8.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 20:27:20 +00:00
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
Alex
50fbe3d5af Merge pull request #1798 from arc53/dependabot/pip/application/markdownify-1.1.0
build(deps): bump markdownify from 0.14.1 to 1.1.0 in /application
2025-05-19 14:01:13 +03:00
GH Action - Upstream Sync
af40a77d24 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-05-19 01:45:51 +00:00
dependabot[bot]
8af9a5e921 build(deps): bump markdownify from 0.14.1 to 1.1.0 in /application
Bumps [markdownify](https://github.com/matthewwithanm/python-markdownify) from 0.14.1 to 1.1.0.
- [Release notes](https://github.com/matthewwithanm/python-markdownify/releases)
- [Commits](https://github.com/matthewwithanm/python-markdownify/compare/0.14.1...1.1.0)

---
updated-dependencies:
- dependency-name: markdownify
  dependency-version: 1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-18 12:07:13 +00:00
Alex
9807788ecb Merge pull request #1795 from arc53/dependabot/pip/application/pypdf-5.5.0
build(deps): bump pypdf from 5.2.0 to 5.5.0 in /application
2025-05-18 15:05:57 +03:00
dependabot[bot]
5e2f329f15 build(deps): bump pypdf from 5.2.0 to 5.5.0 in /application
Bumps [pypdf](https://github.com/py-pdf/pypdf) from 5.2.0 to 5.5.0.
- [Release notes](https://github.com/py-pdf/pypdf/releases)
- [Changelog](https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/py-pdf/pypdf/compare/5.2.0...5.5.0)

---
updated-dependencies:
- dependency-name: pypdf
  dependency-version: 5.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-18 12:02:27 +00:00
Alex
9572a7adaa Merge pull request #1800 from arc53/dependabot/pip/application/boto3-1.38.18
build(deps): bump boto3 from 1.35.97 to 1.38.18 in /application
2025-05-18 15:00:45 +03:00
dependabot[bot]
1ba94f4f5f build(deps): bump boto3 from 1.35.97 to 1.38.18 in /application
Bumps [boto3](https://github.com/boto/boto3) from 1.35.97 to 1.38.18.
- [Release notes](https://github.com/boto/boto3/releases)
- [Commits](https://github.com/boto/boto3/compare/1.35.97...1.38.18)

---
updated-dependencies:
- dependency-name: boto3
  dependency-version: 1.38.18
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-18 11:54:08 +00:00
Alex
237afa0a3a Merge pull request #1799 from arc53/dependabot/pip/application/beautifulsoup4-4.13.4
build(deps): bump beautifulsoup4 from 4.12.3 to 4.13.4 in /application
2025-05-18 14:53:01 +03: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
Alex
9d8073d468 Merge pull request #1794 from arc53/remove-duplicate-docs
remove duplicate chat widget page
2025-05-16 12:45:59 +03:00
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
dependabot[bot]
3bf7f67adf build(deps): bump beautifulsoup4 from 4.12.3 to 4.13.4 in /application
Bumps [beautifulsoup4](https://www.crummy.com/software/BeautifulSoup/bs4/) from 4.12.3 to 4.13.4.

---
updated-dependencies:
- dependency-name: beautifulsoup4
  dependency-version: 4.13.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-15 20:15:17 +00:00
Pavel
594ce05292 remove duplicate chat widget page 2025-05-15 21:32:59 +04:00
Alex
fe02ca68d5 fix: update API key for DocsGPTWidget 2025-05-15 18:18:12 +01:00
Alex
21ef27ee9b Merge pull request #1791 from arc53/fix-openai-conflict
fix for OPENAI_BASE_URL + ollama can't connect to container
2025-05-15 19:04:21 +03:00
Alex
09d37f669f Merge pull request #1793 from arc53/prompts-update
refactor: enhance AI assistant prompts for clarity and creativity
2025-05-15 16:19:32 +03:00
Pavel
416b776062 redundant f string 2025-05-15 16:57:41 +04:00
Alex
5ed05d4020 refactor: enhance AI assistant prompts for clarity and creativity 2025-05-15 13:30:18 +01:00
Alex
4004bfb5ef Merge pull request #1792 from arc53/read_webpage_tools
feat: add ReadWebpageTool for fetching and converting webpage content…
2025-05-15 15:14:02 +03:00
Alex
45aace8966 feat: add ReadWebpageTool for fetching and converting webpage content to Markdown 2025-05-15 12:56:06 +01:00
Alex
d9fc623dcb Merge pull request #1790 from ManishMadan2882/setup-fix
(fix:dockercompose) volumes, user
2025-05-15 13:37:11 +03:00
Pavel
dbb822f6b0 fix for OPENAI_BASE_URL + ollama can't connect to container
- fix for OpenAI trying to use base_url=""
- fix for ollama container error:
`Error code: 404 - {'error': {'message': 'model "MODEL_NAME" not found, try pulling it first', 'type': 'api_error', 'param': None, 'code': None}}`
2025-05-15 13:50:08 +04:00
ManishMadan2882
3d64dffc32 (fix:dockercompose) volumes, user 2025-05-15 15:19:34 +05:30
GH Action - Upstream Sync
130ece7bc0 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-05-15 01:38:39 +00:00
Alex
b2809b2e9a remove old deps 2025-05-14 22:03:27 +01:00
Alex
29e89d2965 Merge pull request #1785 from arc53/dependabot/pip/application/yarl-1.20.0
build(deps): bump yarl from 1.18.3 to 1.20.0 in /application
2025-05-14 23:55:09 +03:00
dependabot[bot]
e7d54a639e build(deps): bump yarl from 1.18.3 to 1.20.0 in /application
Bumps [yarl](https://github.com/aio-libs/yarl) from 1.18.3 to 1.20.0.
- [Release notes](https://github.com/aio-libs/yarl/releases)
- [Changelog](https://github.com/aio-libs/yarl/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/yarl/compare/v1.18.3...v1.20.0)

---
updated-dependencies:
- dependency-name: yarl
  dependency-version: 1.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-14 20:50:26 +00:00
Alex
22df98e9bb Merge pull request #1782 from arc53/dependabot/pip/application/prompt-toolkit-3.0.51
build(deps): bump prompt-toolkit from 3.0.50 to 3.0.51 in /application
2025-05-14 23:49:14 +03:00
dependabot[bot]
0d45c44c6f build(deps): bump prompt-toolkit from 3.0.50 to 3.0.51 in /application
Bumps [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) from 3.0.50 to 3.0.51.
- [Release notes](https://github.com/prompt-toolkit/python-prompt-toolkit/releases)
- [Changelog](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/main/CHANGELOG)
- [Commits](https://github.com/prompt-toolkit/python-prompt-toolkit/compare/3.0.50...3.0.51)

---
updated-dependencies:
- dependency-name: prompt-toolkit
  dependency-version: 3.0.51
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-14 20:47:44 +00:00
Alex
63c6912841 lazy load elasticsearch 2025-05-14 21:45:30 +01:00
Alex
73bce73034 Merge pull request #1786 from arc53/dependabot/pip/application/flask-3.1.1
build(deps): bump flask from 3.1.0 to 3.1.1 in /application
2025-05-14 23:40:58 +03:00
Manish Madan
b2582796a2 Merge branch 'arc53:main' into main 2025-05-15 01:20:23 +05:30
Alex
8babb6e68f Merge pull request #1787 from siiddhantt/refactor/agents
refactor: handle empty sources and chunks in agent
2025-05-14 14:39:01 +03:00
Siddhant Rai
d1d28df8a1 fix: handle empty chunks and retriever values in agent creation and update 2025-05-14 09:26:36 +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
dependabot[bot]
23bfd4683c build(deps): bump flask from 3.1.0 to 3.1.1 in /application
Bumps [flask](https://github.com/pallets/flask) from 3.1.0 to 3.1.1.
- [Release notes](https://github.com/pallets/flask/releases)
- [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/flask/compare/3.1.0...3.1.1)

---
updated-dependencies:
- dependency-name: flask
  dependency-version: 3.1.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-13 20:32:17 +00:00
Alex
a52a3e3158 Merge pull request #1750 from arc53/dependabot/pip/application/packaging-25.0
build(deps): bump packaging from 24.1 to 25.0 in /application
2025-05-13 18:34:56 +03:00
Alex
44e524e3c3 build(deps): update langchain and openai dependencies 2025-05-13 16:29:24 +01:00
dependabot[bot]
9a430f73e2 build(deps): bump packaging from 24.1 to 25.0 in /application
Bumps [packaging](https://github.com/pypa/packaging) from 24.1 to 25.0.
- [Release notes](https://github.com/pypa/packaging/releases)
- [Changelog](https://github.com/pypa/packaging/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pypa/packaging/compare/24.1...25.0)

---
updated-dependencies:
- dependency-name: packaging
  dependency-version: '25.0'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-13 15:10:29 +00:00
Alex
fdea40ec11 Merge pull request #1735 from arc53/dependabot/pip/application/referencing-0.36.2
build(deps): bump referencing from 0.30.2 to 0.36.2 in /application
2025-05-13 18:09:10 +03:00
Alex
526d340849 fix: stale deps 2025-05-13 16:03:48 +01:00
dependabot[bot]
fe95f6ad81 build(deps): bump referencing from 0.30.2 to 0.36.2 in /application
Bumps [referencing](https://github.com/python-jsonschema/referencing) from 0.30.2 to 0.36.2.
- [Release notes](https://github.com/python-jsonschema/referencing/releases)
- [Changelog](https://github.com/python-jsonschema/referencing/blob/main/docs/changes.rst)
- [Commits](https://github.com/python-jsonschema/referencing/compare/v0.30.2...v0.36.2)

---
updated-dependencies:
- dependency-name: referencing
  dependency-version: 0.36.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-13 13:20:27 +00:00
Alex
39e73c37ab Merge pull request #1576 from arc53/dependabot/pip/application/portalocker-3.1.1
build(deps): bump portalocker from 2.10.1 to 3.1.1 in /application
2025-05-13 16:06:44 +03:00
Alex
39b36b6857 Feat: Add MD gen script, enable Qdrant lazy loading 2025-05-13 14:03:05 +01:00
dependabot[bot]
44e98748c5 build(deps): bump portalocker from 2.10.1 to 3.1.1 in /application
Bumps [portalocker](https://github.com/wolph/portalocker) from 2.10.1 to 3.1.1.
- [Release notes](https://github.com/wolph/portalocker/releases)
- [Changelog](https://github.com/wolph/portalocker/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/wolph/portalocker/compare/v2.10.1...v3.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-13 12:44:30 +00:00
Alex
8a7aeee955 Merge pull request #1751 from arc53/dependabot/pip/application/torch-2.7.0
build(deps): bump torch from 2.5.1 to 2.7.0 in /application
2025-05-13 15:41:10 +03:00
dependabot[bot]
1c7befb8d3 build(deps): bump torch from 2.5.1 to 2.7.0 in /application
Bumps [torch](https://github.com/pytorch/pytorch) from 2.5.1 to 2.7.0.
- [Release notes](https://github.com/pytorch/pytorch/releases)
- [Changelog](https://github.com/pytorch/pytorch/blob/main/RELEASE.md)
- [Commits](https://github.com/pytorch/pytorch/compare/v2.5.1...v2.7.0)

---
updated-dependencies:
- dependency-name: torch
  dependency-version: 2.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-13 12:20:22 +00:00
Alex
d5d59ac62c Merge pull request #1759 from arc53/dependabot/pip/application/transformers-4.51.3
build(deps): bump transformers from 4.49.0 to 4.51.3 in /application
2025-05-13 15:19:18 +03:00
Alex
562f0762a0 Merge pull request #1775 from siiddhantt/feat/enhance-agents
feat: share and pin agents
2025-05-13 11:54:29 +03:00
Siddhant Rai
e46aedce21 Merge branch 'feat/enhance-agents' of https://github.com/siiddhantt/DocsGPT into feat/enhance-agents 2025-05-13 13:05:50 +05:30
Siddhant Rai
57cc09b1d7 feat: update tool display name and improve SharedAgent component layout 2025-05-13 13:05:20 +05:30
ManishMadan2882
e1e608b744 (fix:bubble) sleeker source cards 2025-05-13 06:16:16 +05:30
Alex
cbfa5a5118 Delete .jwt_secret_key 2025-05-12 15:11:42 +03:00
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
Siddhant Rai
ec7f14b82d Merge branch 'main' into feat/enhance-agents 2025-05-12 13:45:18 +05:30
Siddhant Rai
6520be5b85 feat: shared and pinning agents + fix for streaming tools 2025-05-12 06:06:11 +05:30
GH Action - Upstream Sync
17e4fad6fb Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-05-10 01:36:32 +00:00
Alex
d84c416421 Merge pull request #1774 from arc53/limit-yield-on-tools
fix: truncate tool call results to 50 characters for cleaner output
2025-05-10 00:53:28 +03:00
Alex
32803c89a3 fix: truncate tool call results to 50 characters for cleaner output 2025-05-09 22:52:17 +01:00
Alex
a86bcb5c29 Merge pull request #1773 from arc53/cols-agent-fix
fix:(style) update layout for agent list and card components
2025-05-10 00:21:05 +03:00
Alex
7d76a33790 fix:(style) update layout for agent list and card components 2025-05-09 22:15:55 +01: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
Alex
24bb2e469d Merge pull request #1772 from ManishMadan2882/main
Bug Fixes
2025-05-09 11:01:01 +03:00
ManishMadan2882
e1aa2cc0b8 (fix:ingestion) store file name as metadata, not path 2025-05-09 02:26:35 +05:30
ManishMadan2882
d073947f3b Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-05-08 16:11:14 +05:30
ManishMadan2882
3243740dd1 (fix-bubble) inconsistent width with snippets 2025-05-08 16:10:31 +05:30
Alex
f9bd566a3b Merge pull request #1768 from ManishMadan2882/main
Attachments : File storage changes
2025-05-08 01:41:08 +03:00
Alex
183251487c lint: remove unused import of 'io' in worker.py 2025-05-07 23:37:35 +01:00
ManishMadan2882
ff532210f7 lint 2025-05-08 00:15:28 +05:30
ManishMadan2882
d0a04d9801 (fix/lint) empty methods 2025-05-08 00:14:42 +05:30
ManishMadan2882
ea6533db4e (fix:attachment) strictly use filename 2025-05-08 00:11:02 +05:30
ManishMadan2882
89d5e7bee5 (feat:attachment) store file in endpoint layer 2025-05-07 19:15:36 +05:30
Alex
7e6cdee592 Merge pull request #1732 from asminkarki012/feature/mermaid-integration
feat[mermaid]:integration of mermaid
2025-05-07 11:52:50 +03:00
ManishMadan2882
990c2fb416 Merge branch 'main' into 'feature/mermaid-integration' 2025-05-07 03:44:47 +05:30
ManishMadan2882
09e054c6aa (fix/scroll) bring back arrowDown button, smoother scroll 2025-05-07 02:48:49 +05:30
ManishMadan2882
23f648f53a (feat/mermaid) zoom as per the requirement 2025-05-07 02:07:57 +05:30
Siddhant Rai
07fa656e7c feat: implement pinning functionality for agents with UI updates 2025-05-06 16:12:55 +05:30
Alex
7858c48f11 fix: zip file uploads 2025-05-06 11:12:26 +01:00
Alex
e56d54c3f0 fix: improve source and description handling in GetAgent and GetAgents responses 2025-05-06 10:59:25 +01:00
ManishMadan2882
f37ca95c10 (fix/mermaid loading): load only the diagrams which stream 2025-05-06 15:16:14 +05:30
ManishMadan2882
72e51bb072 (feat:mermaid) dont pass isDarkTheme 2025-05-06 04:38:38 +05:30
Alex
dcfcbf54be Merge pull request #1767 from arc53/sources-icon-fix-agent-menu
fix: sources icon mini fix
2025-05-06 01:36:29 +03:00
Alex
204936b2d0 fix: sources icon mini fix 2025-05-05 23:34:13 +01:00
ManishMadan2882
98856b39ac (feat:mermaid) zoom onhover, throw syntax errors 2025-05-06 00:53:33 +05:30
Alex
ad5f707486 lint: ruff fix 2025-05-05 18:03:45 +01:00
Alex
5ecfb0ce6d fix: enhance error logging 2025-05-05 17:59:37 +01:00
Alex
2147b3f06f lint: mini fix 2025-05-05 13:14:56 +01:00
Alex
7daed3daaf Merge pull request #1764 from arc53/feat/better-logs
fix: enhance error logging with exception info across multiple modules
2025-05-05 15:13:21 +03:00
Alex
481df4d604 fix: enhance error logging with exception info across multiple modules 2025-05-05 13:12:39 +01:00
Alex
cf333873fd fix: json body 2025-05-05 00:08:56 +01:00
Alex
ae700e8f3a fix: display only 2 demos buttons on mobile 2025-05-04 18:56:33 +01:00
ManishMadan2882
16386a9524 (feat:mermaid) zoom on hover 2025-05-04 19:39:24 +05:30
ManishMadan2882
7e7ce276b2 (fix:mermaid/flicker) separated from markdown 2025-05-02 14:12:24 +05:30
Alex
71c6b41b83 Merge pull request #1762 from arc53/dartpain-patch-2
Update README.md
2025-05-01 17:19:10 +03:00
Alex
4b2faae29a Update README.md 2025-05-01 17:15:08 +03:00
ManishMadan2882
7e28e562d0 (fix:mermaid) download svg/png 2025-05-01 19:37:03 +05:30
dependabot[bot]
93c2e2a597 build(deps): bump transformers from 4.49.0 to 4.51.3 in /application
Bumps [transformers](https://github.com/huggingface/transformers) from 4.49.0 to 4.51.3.
- [Release notes](https://github.com/huggingface/transformers/releases)
- [Commits](https://github.com/huggingface/transformers/compare/v4.49.0...v4.51.3)

---
updated-dependencies:
- dependency-name: transformers
  dependency-version: 4.51.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-29 20:50:32 +00:00
Alex
c45d13d834 Merge pull request #1755 from siiddhantt/feat/agent-menu
feat: agent webhook and minor fixes
2025-04-29 00:41:36 +03:00
Alex
330276cdf7 fix: lint for ruff 2025-04-28 22:32:13 +01:00
Siddhant Rai
22c7015c69 refactor: webhook listener handle both POST and GET requests 2025-04-29 00:29:16 +05:30
Alex
cc67d4a1e2 process all request data implicitly 2025-04-28 17:49:29 +01:00
Siddhant Rai
eeb9da696f Merge remote-tracking branch 'upstream/main' into feat/agent-menu 2025-04-28 17:01:46 +05:30
Siddhant Rai
4979e1ac9a feat: add clsx dependency, enhance logging in agent logic, and improve agent logs component 2025-04-28 14:18:28 +05:30
ManishMadan2882
545353dabf (feat:mermaid) use contentLoaded to render 2025-04-27 03:42:28 +05:30
ManishMadan2882
545376740c (fix:re-render) useRef to check for bottom 2025-04-26 19:33:24 +05:30
Siddhant Rai
8289b02ab0 feat: add agent webhook endpoint and implement related functionality 2025-04-26 12:00:29 +05:30
ManishMadan2882
fc0060662b (fix:mermaid) suppress mermaid injected errors in DOM 2025-04-25 21:18:49 +05:30
Alex
df9d432d29 fix: mongo db database name in settings 2025-04-24 17:29:41 +01:00
Alex
76fd6e15cc Update Dockerfile 2025-04-24 18:54:58 +03:00
Alex
06982efda5 Merge pull request #1742 from ManishMadan2882/main
File System Abstraction
2025-04-24 01:32:27 +03:00
Alex
3cd9a72495 add storage type to the settings cofig 2025-04-23 23:13:39 +01:00
ManishMadan2882
0ce27f274a (feat:storage) file indexes/faiss 2025-04-23 04:28:45 +05:30
ManishMadan2882
e60f78ac4a (feat:storage) file uploads 2025-04-23 03:39:35 +05:30
ManishMadan2882
637d3a24a1 Revert "(feat:storage) file, indexes uploads"
This reverts commit 64c42f0ddf.
2025-04-23 00:52:55 +05:30
ManishMadan2882
24c8b24b1f Revert "(fix:indexes) look for the right path"
This reverts commit 5ad34e2216.
2025-04-23 00:52:22 +05:30
ManishMadan2882
5ad34e2216 (fix:indexes) look for the right path 2025-04-22 17:34:25 +05:30
ManishMadan2882
64c42f0ddf (feat:storage) file, indexes uploads 2025-04-22 05:18:07 +05:30
ManishMadan2882
0a31ddaae6 (feat:storage) use get storage 2025-04-22 01:41:53 +05:30
ManishMadan2882
38476cfeb8 (gfeat:storage) get storage instance based on settings 2025-04-22 00:57:57 +05:30
asminkarki012
decc31f1f0 update latest changes 2025-04-21 21:05:12 +05:45
asminkarki012
ea0aa64330 fix: added renderer in answer bubble 2025-04-21 20:50:04 +05:45
Manish Madan
e9a6044645 Merge branch 'main' into main 2025-04-20 16:01:13 +05:30
Alex
474d700df2 Merge pull request #1745 from arc53/dependabot/pip/application/google-generativeai-0.8.5
build(deps): bump google-generativeai from 0.8.3 to 0.8.5 in /application
2025-04-20 01:39:11 +03:00
ManishMadan2882
c50ff6faa3 (feat:fs abstract) googleLLM class 2025-04-18 21:03:28 +05:30
ManishMadan2882
c8efef8f04 (fix:openai) image uplads, use lambda in process_files 2025-04-18 18:27:02 +05:30
dependabot[bot]
1d22f77568 build(deps): bump google-generativeai in /application
Bumps [google-generativeai](https://github.com/google/generative-ai-python) from 0.8.3 to 0.8.5.
- [Release notes](https://github.com/google/generative-ai-python/releases)
- [Changelog](https://github.com/google-gemini/deprecated-generative-ai-python/blob/main/RELEASE.md)
- [Commits](https://github.com/google/generative-ai-python/compare/v0.8.3...v0.8.5)

---
updated-dependencies:
- dependency-name: google-generativeai
  dependency-version: 0.8.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-17 20:06:10 +00:00
ManishMadan2882
5aa51f5f36 (feat:file_abstract) openai attachments comply 2025-04-18 01:27:21 +05:30
ManishMadan2882
335c21c48a (fix:attachment) dont calculate MIME again 2025-04-17 16:36:40 +05:30
ManishMadan2882
c35d1cecfe (feat:file_abstract) return storage metadata after upload 2025-04-17 16:29:34 +05:30
ManishMadan2882
0d3e6157cd (feat:attachmentUpload) parse content before upload 2025-04-17 16:23:01 +05:30
ManishMadan2882
68e4cf4d14 (feat:fsabstract) add factory class 2025-04-17 02:40:53 +05:30
ManishMadan2882
9454150f7d (fix:s3) processor func 2025-04-17 02:36:55 +05:30
ManishMadan2882
0a0e16547e (feat:fs_abstract) attachment uploads 2025-04-17 02:35:45 +05:30
Alex
0aec1b9969 Merge pull request #1739 from siiddhantt/feat/agent-menu
feat: new agents section
2025-04-16 17:50:11 +03:00
Alex
3e1ec23409 fix: lint 2025-04-16 15:47:39 +01:00
Siddhant Rai
2f9f428a2f feat: add prompt management functionality with modal integration 2025-04-16 19:41:16 +05:30
Siddhant Rai
da15cde49c Merge branch 'main' into feat/agent-menu 2025-04-16 18:35:38 +05:30
Siddhant Rai
e6ed37139a feat: implement agent preview functionality and enhance conversation handling 2025-04-16 16:26:47 +05:30
ManishMadan2882
377e33c148 (feat:file_abstract) process files method 2025-04-16 03:36:45 +05:30
ManishMadan2882
e567d88951 ((feat:fs_abstact) s3 2025-04-16 03:31:42 +05:30
ManishMadan2882
89b2937b11 ((feat:fs_abstact) local 2025-04-16 03:31:28 +05:30
ManishMadan2882
142ed75468 ((feat:fs_abstact) base 2025-04-16 03:31:06 +05:30
Siddhant Rai
d80eeb044c feat: add agent timestamps and improve agent retrieval logic 2025-04-15 14:06:20 +05:30
Siddhant Rai
7c69e99914 feat: Enhance agent selection and conversation handling
- Added functionality to select agents in the Navigation component, allowing users to reset conversations and set the selected agent.
- Updated the MessageInput component to conditionally show source and tool buttons based on the selected agent.
- Modified the Conversation component to handle agent-specific queries and manage file uploads.
- Improved conversation fetching logic to include agent IDs and handle attachments.
- Introduced new types for conversation summaries and results to streamline API responses.
- Refactored Redux slices to manage selected agent state and improve overall state management.
- Enhanced error handling and loading states across components for better user experience.
2025-04-15 11:53:53 +05:30
Alex
5e1aaf5a44 Merge pull request #1733 from ManishMadan2882/main
Attachments: Enhancements , strategy specific to certain LLMs
2025-04-15 01:55:46 +03:00
Alex
ad610d2f90 fix: lint ruff 2025-04-14 23:49:40 +01:00
Alex
02934452d6 fix: remove comment 2025-04-14 23:48:17 +01:00
ManishMadan2882
8b054010e1 (Feat:input) sleek attachment pills 2025-04-15 03:30:11 +05:30
Alex
5b77f3839b fix: maybe 2025-04-14 20:24:05 +01:00
Alex
231b792452 fix: streaming with tools google and openai halfway 2025-04-14 18:54:40 +01:00
ManishMadan2882
b468e0c164 (fix:generation) attach + tools 2025-04-13 18:33:26 +05:30
Siddhant Rai
fa1f9d7009 feat: agents route replacing chatbots
- Removed API Keys tab from SettingsBar and adjusted tab layout.
- Improved styling for tab scrolling buttons and gradient indicators.
- Introduced AgentDetailsModal for displaying agent access details.
- Updated Analytics component to fetch agent data and handle analytics for selected agent.
- Refactored Logs component to accept agentId as a prop for filtering logs.
- Enhanced type definitions for InputProps to include textSize.
- Cleaned up unused imports and optimized component structure across various files.
2025-04-11 17:24:22 +05:30
asminkarki012
c5a8f3abcd refact:generate unique meraid id using crypto.randomUUID 2025-04-11 11:48:07 +05:45
ManishMadan2882
dfe6a8d3e3 (feat:attach) simplify the format for files 2025-04-11 01:53:17 +05:30
ManishMadan2882
292257770c (refactor:attach) centralize attachment state 2025-04-10 03:02:39 +05:30
ManishMadan2882
b4c6b2b08b (feat:utils) getOS, isTouchDevice 2025-04-10 01:44:49 +05:30
ManishMadan2882
6cb4577e1b (feat:input) hotkey for sources open 2025-04-10 01:43:46 +05:30
ManishMadan2882
456784db48 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-04-10 01:29:52 +05:30
ManishMadan2882
dd9ea46e58 (feat:attach) strategy specific to google genai 2025-04-10 01:29:01 +05:30
GH Action - Upstream Sync
ed3af2fac0 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-04-09 01:27:51 +00:00
Alex
02f8132f3a Update README.md 2025-04-08 15:56:34 +03:00
ManishMadan2882
55bd90fad9 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-04-08 17:51:27 +05:30
ManishMadan2882
cd7bbb45c3 (fix:popups) minor hover 2025-04-08 17:51:18 +05:30
Alex
6c7fc0ed22 Merge pull request #1729 from arc53/setup-windows
win-setup
2025-04-08 13:58:28 +03:00
ManishMadan2882
5421bc1386 (feat:attach) extend support for imgs 2025-04-08 15:51:37 +05:30
GH Action - Upstream Sync
051841e566 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-04-08 01:27:09 +00:00
ManishMadan2882
0c68815cf2 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-04-07 20:16:12 +05:30
ManishMadan2882
0c1138179b (feat:attch) store file mime type 2025-04-07 20:16:03 +05:30
ManishMadan2882
1f3d1cc73e (feat:attach) handle unsupported attachments 2025-04-07 20:15:11 +05:30
Alex
707d1332de Merge pull request #1734 from arc53/fix-thought-param
fix: thought param in /api/answer
2025-04-07 13:08:23 +03:00
Alex
f6c88da81b fix: thought param in /api/answer 2025-04-07 11:03:49 +01:00
GH Action - Upstream Sync
a651e6e518 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-04-07 01:37:46 +00:00
Alex
bea89b93eb Merge pull request #1730 from arc53/dependabot/pip/application/multidict-6.3.2
build(deps): bump multidict from 6.1.0 to 6.3.2 in /application
2025-04-06 22:00:31 +03:00
ManishMadan2882
244c9b96a2 (fix:attach) pass attachment docs as it is 2025-04-06 16:02:30 +05:30
ManishMadan2882
a37bd76950 (feat:storeAttach) store in inputs, raise errors from worker 2025-04-06 16:01:57 +05:30
ManishMadan2882
9d70032de8 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-04-06 15:57:26 +05:30
ManishMadan2882
e4945b41e9 (feat:files) link attachment to openai_api 2025-04-06 15:57:18 +05:30
Pavel
493dc8689c guide-updates 2025-04-05 17:49:56 +04:00
GH Action - Upstream Sync
bdac2ffa27 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-04-05 01:25:36 +00:00
asminkarki012
b1235f3ce0 feat[mermaid]:clean comment 2025-04-04 18:40:57 +05:45
asminkarki012
ba4bb63a1f feat[mermaid]:integration of mermaid 2025-04-04 18:36:45 +05:45
Alex
3227b0e69c Merge pull request #1722 from asminkarki012/fix/csv-parser-include-headers
fix[csv_parser]:missing header
2025-04-04 14:53:45 +03:00
ManishMadan2882
29c899627e Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-04-04 03:31:45 +05:30
ManishMadan2882
5923781484 (feat:attach) warning for error, progress, removal 2025-04-04 03:29:01 +05:30
dependabot[bot]
8bb263a2ec build(deps): bump multidict from 6.1.0 to 6.3.2 in /application
Bumps [multidict](https://github.com/aio-libs/multidict) from 6.1.0 to 6.3.2.
- [Release notes](https://github.com/aio-libs/multidict/releases)
- [Changelog](https://github.com/aio-libs/multidict/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/multidict/compare/v6.1.0...v6.3.2)

---
updated-dependencies:
- dependency-name: multidict
  dependency-version: 6.3.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-03 20:24:09 +00:00
Alex
94c7bba168 Merge pull request #1716 from ManishMadan2882/main
Sources + Tools
2025-04-03 02:03:47 +03:00
ManishMadan2882
f9ad4c068a (feat:attach) fallback strategy to process docs 2025-04-03 03:26:37 +05:30
ManishMadan2882
19d68252cd (fix/attach): inputs are created in application 2025-04-02 16:36:58 +05:30
ManishMadan2882
72bbe3b1ce Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-04-02 15:57:45 +05:30
ManishMadan2882
856824316b (feat: attach) handle them locally from message input 2025-04-02 15:33:35 +05:30
ManishMadan2882
95e189d1d8 (feat:base/agents) default attachment 2025-04-02 15:29:04 +05:30
ManishMadan2882
c629460acb (feat:attach) extract contents in endpoint layer 2025-04-02 15:21:33 +05:30
ManishMadan2882
f235a94986 (feat:attach) pass attachments for generation 2025-04-02 15:14:56 +05:30
GH Action - Upstream Sync
632cba86e9 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-04-02 01:36:06 +00:00
Alex
6b92c7eccc Update README.md 2025-04-01 12:27:55 +03:00
Alex
ab0da1abac Merge pull request #1721 from siiddhantt/feat/react-agent
feat: ReActAgent and agent refactor
2025-04-01 12:08:09 +03:00
Siddhant Rai
7f31ac7bcb refactor: minor changes 2025-04-01 12:33:43 +05:30
Pavel
57a6fb31b2 periodic header injection 2025-03-31 22:28:04 +04:00
Siddhant Rai
fd2b6c111c feat: enhance ClassicAgent and ReActAgent with tool preparation steps 2025-03-31 17:02:36 +05:30
GH Action - Upstream Sync
302458b505 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-03-30 01:40:32 +00:00
Alex
0e31329785 Merge pull request #1723 from arc53/dependabot/pip/application/langsmith-0.3.19 2025-03-29 21:14:46 +02:00
GH Action - Upstream Sync
8978a4cf2d Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-03-29 01:25:46 +00:00
dependabot[bot]
57d103116f build(deps): bump langsmith from 0.3.15 to 0.3.19 in /application
Bumps [langsmith](https://github.com/langchain-ai/langsmith-sdk) from 0.3.15 to 0.3.19.
- [Release notes](https://github.com/langchain-ai/langsmith-sdk/releases)
- [Commits](https://github.com/langchain-ai/langsmith-sdk/compare/v0.3.15...v0.3.19)

---
updated-dependencies:
- dependency-name: langsmith
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-28 20:55:06 +00:00
Alex
a4e9ee72d4 Merge pull request #1713 from arc53/dependabot/pip/application/pymongo-4.11.3 2025-03-28 22:45:04 +02:00
asminkarki012
c70be12bfd fix[csv_parser]:missing header 2025-03-28 22:46:11 +05:45
ManishMadan2882
4241307990 (fix:responsive/messageInput) sleek badge buttons, aligned send btn 2025-03-28 20:42:12 +05:30
ManishMadan2882
727a8ef13d (fix:responsive) tools and source pop 2025-03-28 20:40:46 +05:30
ManishMadan2882
7c92558ad1 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-03-28 18:13:42 +05:30
ManishMadan2882
45083d29a6 (feat:attach) show files on conversations 2025-03-28 18:13:24 +05:30
ManishMadan2882
5089d86095 (feat:attach) send attachment ids 2025-03-28 18:12:38 +05:30
ManishMadan2882
80e55ef385 (feat:attach) functionality to upload files 2025-03-28 18:10:18 +05:30
GH Action - Upstream Sync
b5ed98445f Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-03-28 01:26:07 +00:00
Siddhant Rai
82d377abf5 feat: add support for thought processing in conversation flow and introduce ReActAgent 2025-03-27 23:19:08 +05:30
ManishMadan2882
2dbea5d1b2 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-03-27 16:55:49 +05:30
ManishMadan2882
4ba35d6189 (feat: attachment) integrate upload on fe 2025-03-27 16:54:21 +05:30
Alex
1620b4f214 Merge pull request #1709 from Charlesnorris509/main
Update Navigation.tsx
2025-03-27 11:28:45 +02:00
GH Action - Upstream Sync
cec3f987f2 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-03-27 01:26:14 +00:00
GH Action - Upstream Sync
ec27445728 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-03-27 01:21:05 +00:00
ManishMadan2882
55050a9f58 (feat:attachment) upload single file 2025-03-27 03:28:03 +05:30
Alex
4b1f572b04 Merge pull request #1720 from arc53/dependabot/npm_and_yarn/docs/next-14.2.26
build(deps): bump next from 14.2.22 to 14.2.26 in /docs
2025-03-26 16:53:50 +02:00
ManishMadan2882
502dc9ec52 (feat:attachments) store and ingest files shared 2025-03-26 18:01:31 +05:30
dependabot[bot]
28f925ef75 build(deps): bump next from 14.2.22 to 14.2.26 in /docs
Bumps [next](https://github.com/vercel/next.js) from 14.2.22 to 14.2.26.
- [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.22...v14.2.26)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-26 06:23:32 +00:00
ManishMadan2882
9c8999a3ae (fix:sources) doc selection for preLoaded 2025-03-25 19:04:31 +05:30
ManishMadan2882
90db42ce3a Revert "(fix:sources) preloaded docs have ids too"
This reverts commit 02c8bd06f5.
2025-03-25 16:08:59 +05:30
ManishMadan2882
551130f0e1 (feat:sources/tools) ui perfection for pop-ups 2025-03-25 03:22:21 +05:30
Charles Norris
98abeabc0d Update ConversationTile.tsx 2025-03-24 16:10:41 -04:00
ManishMadan2882
2940a60b3c (faet:input) tools pop-up 2025-03-24 17:16:24 +05:30
ManishMadan2882
76b9bc0d56 (feat:input) sources pop-up 2025-03-24 17:15:57 +05:30
ManishMadan2882
42422ccdcd (feat:settings) routes for tab 2025-03-24 13:56:12 +05:30
ManishMadan2882
e9702ae2de Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-03-21 23:57:09 +05:30
ManishMadan2882
5c54852ebe (fix:tools) no tools placeholder 2025-03-21 23:56:47 +05:30
GH Action - Upstream Sync
718a86ecda Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-03-21 01:26:21 +00:00
GH Action - Upstream Sync
e02f19058e Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-03-21 01:21:33 +00:00
Pavel
1223fd2149 win-setup 2025-03-20 21:48:59 +03:00
Alex
4095b2b674 Merge pull request #1714 from siiddhantt/fix/retrievers-broken
fix: brave and duckduckgo retrievers
2025-03-20 13:00:34 +00:00
Siddhant Rai
3be6e2132b refactor: remove outdated vector store tests 2025-03-20 17:27:18 +05:30
ManishMadan2882
b09386d102 (clean:nav) rm source dropdown 2025-03-20 11:26:45 +05:30
ManishMadan2882
6464698b6d Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-03-20 09:47:38 +05:30
ManishMadan2882
9230fd3bd6 Revert "(feat:nav) clean-up source dropdown"
This reverts commit 561a125c92.
2025-03-20 09:47:29 +05:30
ManishMadan2882
7771609ea0 (locales) udpate placeholder 2025-03-20 09:43:36 +05:30
ManishMadan2882
561a125c92 (feat:nav) clean-up source dropdown 2025-03-20 09:42:57 +05:30
ManishMadan2882
7149461d8e (feat:sources) add pop-up to switch sources 2025-03-20 09:42:08 +05:30
ManishMadan2882
02c8bd06f5 (fix:sources) preloaded docs have ids too 2025-03-20 09:39:30 +05:30
Siddhant Rai
0732d9b6c8 fix: brave and duckduckgo retrievers 2025-03-20 08:27:00 +05:30
GH Action - Upstream Sync
2952c1be08 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-03-20 01:19:59 +00:00
dependabot[bot]
96c4a13c93 build(deps): bump pymongo from 4.10.1 to 4.11.3 in /application
Bumps [pymongo](https://github.com/mongodb/mongo-python-driver) from 4.10.1 to 4.11.3.
- [Release notes](https://github.com/mongodb/mongo-python-driver/releases)
- [Changelog](https://github.com/mongodb/mongo-python-driver/blob/4.11.3/doc/changelog.rst)
- [Commits](https://github.com/mongodb/mongo-python-driver/compare/4.10.1...4.11.3)

---
updated-dependencies:
- dependency-name: pymongo
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-19 08:40:51 +00:00
Alex
53abf1a79e Merge pull request #1701 from siiddhantt/feat/jwt-auth
feat: implement JWT authentication and token management
2025-03-19 08:39:29 +00:00
Siddhant Rai
f00802dd6b fix: llm tests failing 2025-03-19 10:25:46 +05:30
Siddhant Rai
ab95d90284 feat: pass decoded_token to llm and retrievers 2025-03-18 23:46:02 +05:30
ManishMadan2882
9f17eb1d28 feat(textInput) new design 2025-03-18 19:33:26 +05:30
Siddhant Rai
f4ab85a2bb feat: minor fixes after merge 2025-03-18 18:56:02 +05:30
Siddhant Rai
5b40c5a9d7 Merge branch 'main' into feat/jwt-auth 2025-03-18 18:26:29 +05:30
Siddhant Rai
6583aeff08 feat: update authentication handling and integrate token usage across frontend and backend 2025-03-18 08:29:57 +05:30
Charles Norris
b1c531fbcc Update Navigation.tsx
Typo on onCoversationClick() function it should be onConversationClick()
2025-03-17 16:51:33 -04:00
Siddhant Rai
4406426515 feat: implement session_jwt and enhance auth 2025-03-17 11:51:30 +05:30
Alex
af48782464 Merge pull request #1708 from ManishMadan2882/main
Refactor(fe): Conversation
2025-03-16 20:05:46 +00:00
Manish Madan
726d4ddd9f Merge branch 'arc53:main' into main 2025-03-16 04:08:26 +05:30
ManishMadan2882
adc637b689 (clean:unused) 2025-03-16 04:07:33 +05:30
ManishMadan2882
d6c9b4fbc9 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-03-16 04:02:12 +05:30
ManishMadan2882
e17cc8ea34 (fix:date) handle iso 8601 date 2025-03-16 04:01:19 +05:30
ManishMadan2882
574a0e2dba (fix:conv) perfect aligment 2025-03-16 03:57:31 +05:30
ManishMadan2882
fd0bd13b08 (refactor:conv) separate textarea 2025-03-16 02:01:32 +05:30
Alex
f8c92147cd deps: bump langchian things 2025-03-15 19:51:30 +00:00
Alex
8136cd78d3 Merge pull request #1698 from arc53/dependabot/pip/application/duckduckgo-search-7.5.2
build(deps): bump duckduckgo-search from 7.4.2 to 7.5.2 in /application
2025-03-15 19:16:39 +00:00
ManishMadan2882
d9c4331480 (refactor:conv) wrap msgs separately 2025-03-15 16:41:56 +05:30
Alex
7af726f4b2 Merge pull request #1702 from nickaggarwal/main
fix signature for AzureOpenAILLM
2025-03-14 22:08:36 +00:00
ManishMadan2882
a50f3bc55b (fix:sourceDropdown) ask before delete 2025-03-15 00:15:23 +05:30
Nilesh Agarwal
5438bf9754 fix signature for AzureOpenAILLM 2025-03-14 09:52:09 -07:00
Siddhant Rai
7fd377bdbe feat: implement JWT authentication and token management in frontend and backend 2025-03-14 17:07:15 +05:30
Alex
84620a7375 Merge pull request #1700 from ScriptScientist/main
fix: docker compose up doesn't use the env
2025-03-14 10:39:28 +00:00
rock.lee
6968317db2 fix: docker compose up doesn't use the env and setup script will not exit when service start success 2025-03-14 17:09:10 +08:00
dependabot[bot]
67a92428b5 build(deps): bump duckduckgo-search from 7.4.2 to 7.5.2 in /application
Bumps [duckduckgo-search](https://github.com/deedy5/duckduckgo_search) from 7.4.2 to 7.5.2.
- [Release notes](https://github.com/deedy5/duckduckgo_search/releases)
- [Commits](https://github.com/deedy5/duckduckgo_search/compare/v7.4.2...v7.5.2)

---
updated-dependencies:
- dependency-name: duckduckgo-search
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-13 20:19:41 +00:00
Alex
5bb639f0ad Merge pull request #1670 from ManishMadan2882/main
Figma consolidation
2025-03-13 15:21:03 +00:00
ManishMadan2882
5bc758aa2d (fix:ui) minor adjustments 2025-03-13 20:39:17 +05:30
Pavel
27b24f19de user-avatar-svg 2025-03-13 14:25:41 +03:00
GH Action - Upstream Sync
3dfde84827 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-03-13 01:25:22 +00:00
Alex
5e39be6a2c fix: tests anthropic 2025-03-13 00:32:24 +00:00
Alex
35248991e7 fix: cache 2025-03-13 00:25:52 +00:00
Alex
b76e820122 fix: ruff fix 2025-03-13 00:11:50 +00:00
Alex
51eced00aa fix: openai compatable with llama and gemini 2025-03-13 00:10:13 +00:00
ManishMadan2882
079a216f5b (fix:chunkDocs) alike purple action btn 2025-03-13 03:08:02 +05:30
ManishMadan2882
8b5df98f57 (fix:ui) minor adjust 2025-03-13 03:06:49 +05:30
ManishMadan2882
fb6fd5b5b2 (fix:input) unwanted autocomplete style 2025-03-13 03:06:22 +05:30
Alex
5d5ea3eb8f Merge pull request #1689 from ScriptScientist/main
feat: novita llms support
2025-03-12 21:03:09 +00:00
ManishMadan2882
21360981ee (fix:tool-cards) ui adjust 2025-03-13 01:39:51 +05:30
ManishMadan2882
0b3cad152f (fix:prompts) design specs 2025-03-13 00:43:56 +05:30
Alex
2c2dbe45a6 Merge pull request #1696 from arc53/fixes-cache
Fixes cache
2025-03-12 17:37:38 +00:00
ManishMadan2882
5c7a3a515c (fix:general) perfect labels, delete btn 2025-03-12 22:52:08 +05:30
Alex
f2b05ad56d fix: handle cache issues with more grace 2025-03-12 15:13:14 +00:00
Alex
5f9702b91c fix: /search 2025-03-12 13:47:16 +00:00
ManishMadan2882
93de4065c7 (fix:tables) table header, name table data 2025-03-12 17:32:05 +05:30
ManishMadan2882
8e0e55fe5e (fix:analytics) feedback title 2025-03-12 17:31:30 +05:30
ManishMadan2882
a8a8585570 (fix:purple-btn) hover to 976af3 2025-03-12 08:17:13 +05:30
ManishMadan2882
1f3c07979a (fix:bubble) color adjustments 2025-03-12 06:41:37 +05:30
ManishMadan2882
fa07b3349d (fix:sidebar) upload icon, bg perfect 2025-03-12 05:57:59 +05:30
GH Action - Upstream Sync
519ffe617b Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-03-11 01:24:52 +00:00
Alex
fe02bf9347 Merge pull request #1693 from siiddhantt/fix/response-and-sources
feat: agent use in answer and enhance search
2025-03-10 12:53:46 +00:00
Siddhant Rai
faa583864d feat: enhance conversation saving and response streaming with source handling 2025-03-10 14:19:43 +05:30
GH Action - Upstream Sync
1a7504eba0 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-03-10 01:11:30 +00:00
Alex
46d32b4072 Merge pull request #1682 from arc53/dependabot/pip/application/jinja2-3.1.6
build(deps): bump jinja2 from 3.1.5 to 3.1.6 in /application
2025-03-10 00:04:03 +00:00
Alex
18d8b9c395 Merge pull request #1692 from arc53/tool-ntfy
feat: ntfy tool
2025-03-09 01:28:00 +00:00
Alex
8b9b74464e feat: ntfy tool 2025-03-09 01:22:00 +00:00
rock.lee
867c375843 add novita provider 2025-03-08 15:45:49 +08:00
GH Action - Upstream Sync
54ca6acf5a Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-03-08 01:07:50 +00:00
ManishMadan2882
6ac2d6d228 (fix:tile) hover behaviour on rename 2025-03-08 00:48:53 +05:30
ManishMadan2882
10c7a5f36b (clean) comments 2025-03-07 20:24:38 +05:30
Alex
4fd6c52951 fix: api tool avoid sending body if empty 2025-03-07 14:34:23 +00:00
ManishMadan2882
93fea17918 (feat:config) color name update 2025-03-07 19:55:47 +05:30
ManishMadan2882
b3f6a3aae6 (fix:ui) mninor adjustments 2025-03-07 17:20:14 +05:30
ManishMadan2882
773147701d (fix:ui) tool cards 2025-03-07 17:19:14 +05:30
ManishMadan2882
d891c8dae2 (fix:ui) minor perfections 2025-03-07 17:18:28 +05:30
ManishMadan2882
101852c7d1 (feat:toggle) add id, aria props 2025-03-07 17:16:45 +05:30
ManishMadan2882
c1f13ba8b1 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-03-07 17:11:00 +05:30
ManishMadan2882
71e45860f3 (fix:input) remove ambiguous label prop 2025-03-07 17:10:48 +05:30
ManishMadan2882
25dfd63c4f (fix:ui) color adjust 2025-03-07 17:09:00 +05:30
ManishMadan2882
fc12d7b4c8 (fix:tool) rely on reusable components 2025-03-07 17:07:01 +05:30
GH Action - Upstream Sync
a6eedc6d84 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-03-07 01:24:48 +00:00
Alex
b523a98289 Merge pull request #1671 from aidanbennettjones/AidanComponentEditChat
Enables Enter Key Functionality to Submit Edited Chats
2025-03-06 16:44:06 -05:00
Alex
a0929c96ba fix: postgres tool migration 2025-03-06 16:20:19 +00:00
Alex
ae1f25379f Merge pull request #1684 from arc53/brave-tool
brave-tool
2025-03-06 11:17:47 -05:00
Pavel
1e3c8cb7b1 fix imports 2025-03-06 19:13:19 +03:00
Pavel
b9f28705c8 brave-tool 2025-03-06 18:46:50 +03:00
Alex
ad4f3ce379 Merge pull request #1648 from siiddhantt/feat/agent-refactor-and-logging
feat: agent-retriever workflow + logging stack
2025-03-06 09:32:18 -05:00
Alex
d4f53bf6bb fix: ruff check 2025-03-06 14:31:46 +00:00
dependabot[bot]
2ea2819477 build(deps): bump jinja2 from 3.1.5 to 3.1.6 in /application
Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.5 to 3.1.6.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.1.5...3.1.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-06 04:47:24 +00:00
Alex
49a2b2ce6d fix: agent not forgotten 2025-03-05 16:11:06 -05:00
Alex
06edc261c0 fix: duplicates... 2025-03-05 16:09:13 -05:00
Alex
af69bc9d3c Merge branch 'main' into feat/agent-refactor-and-logging 2025-03-05 16:04:09 -05:00
ManishMadan2882
6eb8256220 (feat:docs,chatbots) danger delete btn 2025-03-06 02:00:05 +05:30
ManishMadan2882
ecf3067d67 (fix:analytics) rename feedback title 2025-03-06 01:58:37 +05:30
ManishMadan2882
3a7f23f75e (feat:logs) ui 2025-03-06 01:56:38 +05:30
Siddhant Rai
f88c34a0be feat: streaming responses with function call 2025-03-05 09:02:55 +05:30
ManishMadan2882
572c57e023 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-03-05 03:53:34 +05:30
ManishMadan2882
79cf2150d5 (fix:ui) minor adjustments 2025-03-05 03:15:50 +05:30
ManishMadan2882
68b868047e (feat:copy) prop to showText 2025-03-05 03:13:34 +05:30
ManishMadan2882
377670b34a (feat:docs) adding view option 2025-03-05 03:12:48 +05:30
ManishMadan2882
2b7f4de832 (feat:docs):add contextMenu to actions 2025-03-04 18:53:23 +05:30
GH Action - Upstream Sync
4a88a63fa0 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-03-04 01:24:01 +00:00
Alex
bf195051e2 Merge pull request #1663 from arc53/dependabot/pip/application/primp-0.14.0
build(deps): bump primp from 0.10.0 to 0.14.0 in /application
2025-03-03 14:50:26 +00:00
GH Action - Upstream Sync
c3ccd9feff Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-03-03 01:25:14 +00:00
Alex
2d0f0948fb Merge pull request #1673 from arc53/dependabot/pip/application/anthropic-0.49.0
build(deps): bump anthropic from 0.45.2 to 0.49.0 in /application
2025-03-02 22:44:03 +00:00
aidanbennettjones
fc7a5d098d Merge branch 'arc53:main' into AidanComponentEditChat 2025-03-02 13:35:20 -05:00
aidanbennettjones
b7f766ab82 Fix Number of Rows and Remove Comments 2025-03-02 13:34:55 -05:00
ManishMadan2882
bfffd5e4b3 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-03-01 20:15:24 +05:30
ManishMadan2882
63ba005f4d (fix:ui) minor color perfections 2025-03-01 20:15:10 +05:30
ManishMadan2882
f66ef05f2a (feat:barGraph) on hover opacity 2025-03-01 20:14:27 +05:30
ManishMadan2882
a3b28843b6 (feat:confirm-modal) danger variant 2025-03-01 20:12:40 +05:30
ManishMadan2882
b07ec8accb (feat:general) ui 2025-03-01 20:11:46 +05:30
GH Action - Upstream Sync
06f4b5823a Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-03-01 01:35:16 +00:00
Alex
99fe57f99a Merge pull request #1664 from arc53/dependabot/pip/application/langchain-core-0.3.40
build(deps): bump langchain-core from 0.3.29 to 0.3.40 in /application
2025-03-01 00:58:31 +00:00
dependabot[bot]
d1226031e1 build(deps): bump anthropic from 0.45.2 to 0.49.0 in /application
Bumps [anthropic](https://github.com/anthropics/anthropic-sdk-python) from 0.45.2 to 0.49.0.
- [Release notes](https://github.com/anthropics/anthropic-sdk-python/releases)
- [Changelog](https://github.com/anthropics/anthropic-sdk-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/anthropics/anthropic-sdk-python/compare/v0.45.2...v0.49.0)

---
updated-dependencies:
- dependency-name: anthropic
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-28 20:35:48 +00:00
aidanbennettjones
78f3e64d5a Merge branch 'arc53:main' into AidanComponentEditChat 2025-02-28 10:35:17 -05:00
ManishMadan2882
1d98e75b92 (fix:share) modal covers screen 2025-02-28 16:41:34 +05:30
ManishMadan2882
66d8d95763 (feat:input) floating input labels 2025-02-28 03:46:26 +05:30
ManishMadan2882
e2bf468195 (refactor) conv tile 2025-02-28 03:45:54 +05:30
ManishMadan2882
b7efc16257 (feat:menu) add reusable menu, ui 2025-02-28 03:44:14 +05:30
ManishMadan2882
ec6bcdff7e (feat:convBubble) minor adjustment 2025-02-28 03:42:34 +05:30
ManishMadan2882
3e65885e1f (feat:toggle) greener colors, flexible 2025-02-28 03:40:42 +05:30
Siddhant Rai
c6ce4d9374 feat: logging stacks 2025-02-27 19:14:10 +05:30
ManishMadan2882
0b437d0e8d (feat:ui) updating hero, code snippets 2025-02-27 03:04:55 +05:30
dependabot[bot]
e1df3be4b9 build(deps): bump langchain-core from 0.3.29 to 0.3.40 in /application
Bumps [langchain-core](https://github.com/langchain-ai/langchain) from 0.3.29 to 0.3.40.
- [Release notes](https://github.com/langchain-ai/langchain/releases)
- [Commits](https://github.com/langchain-ai/langchain/compare/langchain-core==0.3.29...langchain-core==0.3.40)

---
updated-dependencies:
- dependency-name: langchain-core
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-26 20:16:15 +00:00
dependabot[bot]
b944769f8c build(deps): bump primp from 0.10.0 to 0.14.0 in /application
Bumps [primp](https://github.com/deedy5/primp) from 0.10.0 to 0.14.0.
- [Release notes](https://github.com/deedy5/primp/releases)
- [Commits](https://github.com/deedy5/primp/compare/v0.10.0...v0.14.0)

---
updated-dependencies:
- dependency-name: primp
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-26 20:16:08 +00:00
Alex
56b8074c22 Merge pull request #1658 from ManishMadan2882/main
Widget upgrade to 0.5.0
2025-02-26 11:05:32 +00:00
ManishMadan2882
b577f322c9 Merge branch 'main' of https://github.com/arc53/docsgpt 2025-02-26 16:17:19 +05:30
ManishMadan2882
b007e2af8f (update:docs) docsgpt dep 2025-02-26 16:12:38 +05:30
ManishMadan2882
df89990aa5 (upgrade:widget) v0.5.0 2025-02-26 16:09:12 +05:30
Alex
c108a53b11 fix: default keys 2025-02-26 10:35:26 +00:00
ManishMadan2882
4831f5bb5d Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-02-26 14:52:59 +05:30
ManishMadan2882
987ef63e64 (feat:widget) simplify scrolling 2025-02-26 14:52:48 +05:30
Alex
e997e12bb9 Merge pull request #1656 from arc53/dependabot/pip/application/lxml-5.3.1
build(deps): bump lxml from 5.3.0 to 5.3.1 in /application
2025-02-25 22:29:56 +00:00
Alex
6ba0add265 Merge pull request #1655 from arc53/dependabot/pip/application/qdrant-client-1.13.2
build(deps): bump qdrant-client from 1.12.2 to 1.13.2 in /application
2025-02-25 22:29:40 +00:00
Alex
9160c13039 Merge pull request #1651 from arc53/dependabot/pip/application/elasticsearch-8.17.1
build(deps): bump elasticsearch from 8.17.0 to 8.17.1 in /application
2025-02-25 22:28:49 +00:00
Alex
40be9f65e4 Merge pull request #1647 from ManishMadan2882/main
Analytics and feedback
2025-02-25 22:28:16 +00:00
dependabot[bot]
0aae53524c build(deps): bump lxml from 5.3.0 to 5.3.1 in /application
Bumps [lxml](https://github.com/lxml/lxml) from 5.3.0 to 5.3.1.
- [Release notes](https://github.com/lxml/lxml/releases)
- [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt)
- [Commits](https://github.com/lxml/lxml/compare/lxml-5.3.0...lxml-5.3.1)

---
updated-dependencies:
- dependency-name: lxml
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-25 20:38:23 +00:00
dependabot[bot]
1d1efc00b5 build(deps): bump qdrant-client from 1.12.2 to 1.13.2 in /application
Bumps [qdrant-client](https://github.com/qdrant/qdrant-client) from 1.12.2 to 1.13.2.
- [Release notes](https://github.com/qdrant/qdrant-client/releases)
- [Commits](https://github.com/qdrant/qdrant-client/compare/v1.12.2...v1.13.2)

---
updated-dependencies:
- dependency-name: qdrant-client
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-25 20:38:17 +00:00
ManishMadan2882
7584305159 (feat:feedback) unset feedback when null 2025-02-26 01:01:30 +05:30
ManishMadan2882
554601d674 (fix:feedback) widget can handle feedback 2025-02-26 01:00:38 +05:30
Alex
6caf14f4b2 Merge pull request #1653 from asminkarki012/main
docs: Ensure --env-file .env is included for environment variable loa…
2025-02-25 17:16:58 +00:00
asminkarki012
edbd08be8a docs: Ensure --env-file .env is included for environment variable loading 2025-02-25 21:52:39 +05:45
ManishMadan2882
caed6df53b (feat:stream) save conversations optionally 2025-02-25 17:32:35 +05:30
ManishMadan2882
d823fba60b Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-02-25 16:23:55 +05:30
ManishMadan2882
92c8abe65d (fix:sharedConv) makes sure that response state updates 2025-02-25 16:23:35 +05:30
Alex
91e966b480 Merge pull request #1650 from arc53/dependabot/pip/application/transformers-4.49.0
build(deps): bump transformers from 4.48.0 to 4.49.0 in /application
2025-02-25 08:51:41 +00:00
Siddhant Rai
1f0b779c64 refactor: folder restructure for agent based workflow 2025-02-25 09:03:45 +05:30
GH Action - Upstream Sync
0ccd76074a Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-02-25 01:22:53 +00:00
Alex
07c6dcab4a Merge pull request #1652 from arc53/dependabot/pip/application/google-genai-1.3.0
build(deps): bump google-genai from 0.5.0 to 1.3.0 in /application
2025-02-24 22:34:16 +00:00
Alex
84cbc1201c fix: googles update 2025-02-24 22:30:09 +00:00
dependabot[bot]
495bbc2aba build(deps): bump google-genai from 0.5.0 to 1.3.0 in /application
Bumps [google-genai](https://github.com/googleapis/python-genai) from 0.5.0 to 1.3.0.
- [Release notes](https://github.com/googleapis/python-genai/releases)
- [Changelog](https://github.com/googleapis/python-genai/blob/main/CHANGELOG.md)
- [Commits](https://github.com/googleapis/python-genai/compare/v0.5.0...v1.3.0)

---
updated-dependencies:
- dependency-name: google-genai
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 22:24:08 +00:00
dependabot[bot]
cb0bceacfa build(deps): bump elasticsearch from 8.17.0 to 8.17.1 in /application
Bumps [elasticsearch](https://github.com/elastic/elasticsearch-py) from 8.17.0 to 8.17.1.
- [Release notes](https://github.com/elastic/elasticsearch-py/releases)
- [Commits](https://github.com/elastic/elasticsearch-py/compare/v8.17.0...v8.17.1)

---
updated-dependencies:
- dependency-name: elasticsearch
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 22:23:48 +00:00
Alex
6799050718 Merge pull request #1601 from arc53/dependabot/pip/application/pydantic-2.10.6
build(deps): bump pydantic from 2.10.4 to 2.10.6 in /application
2025-02-24 22:23:46 +00:00
dependabot[bot]
4b892e8939 build(deps): bump transformers from 4.48.0 to 4.49.0 in /application
Bumps [transformers](https://github.com/huggingface/transformers) from 4.48.0 to 4.49.0.
- [Release notes](https://github.com/huggingface/transformers/releases)
- [Commits](https://github.com/huggingface/transformers/compare/v4.48.0...v4.49.0)

---
updated-dependencies:
- dependency-name: transformers
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 22:23:30 +00:00
Alex
674001b499 Merge pull request #1583 from arc53/dependabot/pip/application/openapi-schema-validator-0.6.3
build(deps): bump openapi-schema-validator from 0.6.2 to 0.6.3 in /application
2025-02-24 22:23:27 +00:00
ManishMadan2882
c730777134 (fix:bubble) keeping feedback visible once submitted 2025-02-25 00:51:33 +05:30
ManishMadan2882
8148876249 (feat:message analytics) count individual queries 2025-02-25 00:45:19 +05:30
ManishMadan2882
4cf946f856 (feat:feedback) timestamp feedback 2025-02-24 18:53:29 +05:30
ManishMadan2882
05706f1641 (feat:feedback and tokens) count apiKey docs separately 2025-02-24 17:24:53 +05:30
Siddhant Rai
6fed84958e feat: agent-retriever workflow + query rephrase 2025-02-24 16:41:57 +05:30
ManishMadan2882
64011c5988 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-02-24 16:28:44 +05:30
ManishMadan2882
3e02d5a56f (feat:conv) save the conv with key 2025-02-24 16:28:24 +05:30
Alex
14f57bc3a4 Update README.md 2025-02-23 15:30:59 +00:00
aidanbennettjones
ac8f1b9aa3 Merge branch 'arc53:main' into AidanComponentEditChat 2025-02-21 15:02:39 -05:00
Alex
104c6ef457 Merge pull request #1645 from ManishMadan2882/main
Settings: Improving table layout
2025-02-20 22:47:25 +00:00
ManishMadan2882
84661cea36 lint 2025-02-21 00:54:06 +05:30
ManishMadan2882
c2b0ed85d2 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-02-21 00:42:56 +05:30
ManishMadan2882
5a081f2419 (feat/docs) prevent event bubble 2025-02-21 00:42:27 +05:30
ManishMadan2882
88016f9c35 (fix/chatbots) prevent overflow 2025-02-21 00:41:30 +05:30
ManishMadan2882
0d56e62bb8 (fix/tables) responsiveness issues 2025-02-20 14:48:16 +05:30
GH Action - Upstream Sync
567756edd3 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-02-20 01:21:11 +00:00
ManishMadan2882
7cc0a3620e (fix:docs) consistency 2025-02-20 03:33:35 +05:30
ManishMadan2882
b5587e458f Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-02-20 01:39:37 +05:30
ManishMadan2882
b22d965b7b (fix:chatbots) ui 2025-02-20 01:39:25 +05:30
Alex
cc0b41ddfb Merge pull request #1642 from siiddhantt/fix/minor-bugs
fix: minor bugs and enhancement
2025-02-19 16:16:05 +00:00
ManishMadan2882
006aeeebb0 (fix:typo) copy 2025-02-19 16:56:15 +05:30
ManishMadan2882
3cfb1abf62 (fix/merge) revert dropdown skeletons 2025-02-19 16:55:36 +05:30
ManishMadan2882
e1da69040d (fix/merge error) tool config modal 2025-02-19 16:44:35 +05:30
Siddhant Rai
5924693e90 fix: merge errors 2025-02-19 14:37:47 +05:30
Siddhant Rai
9ee7d659df Merge branch 'main' into fix/minor-bugs 2025-02-19 14:16:56 +05:30
Siddhant Rai
ac1b1c3cdd feat: add loading spinner to AddToolModal and improve label spacing in General settings 2025-02-19 13:58:40 +05:30
Alex
8440138ba0 Merge pull request #1641 from ManishMadan2882/main
Refactor: Apply base modal for UI consitency
2025-02-18 23:42:06 +00:00
ManishMadan2882
877b44ec0a (lint) 2025-02-19 04:49:22 +05:30
ManishMadan2882
cc4acb8766 (feat:createAPIModal): UI inconsistency 2025-02-19 04:38:49 +05:30
ManishMadan2882
3aa85bb51c (refactor:modals) reuse wrapper modal 2025-02-19 04:35:33 +05:30
ManishMadan2882
4e948d8bff (refactor:tools modal) reuse wrapper modal 2025-02-19 04:15:31 +05:30
ManishMadan2882
28489d244c (feat:input) consistent colors 2025-02-19 04:10:14 +05:30
ManishMadan2882
acf3dd2762 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-02-19 04:01:23 +05:30
ManishMadan2882
8589303753 (feat:consitency)modals, dropdowns 2025-02-19 04:01:14 +05:30
ManishMadan2882
0d9fc26119 (refactor): purge unused files 2025-02-19 03:39:15 +05:30
Alex
9dd63c1da4 Merge pull request #1636 from arc53/dependabot/pip/application/duckduckgo-search-7.4.2
build(deps): bump duckduckgo-search from 6.3.0 to 7.4.2 in /application
2025-02-18 21:55:44 +00:00
aidanbennettjones
7ff03ab098 Enables "Enter" Key Functionality for Edit Chat Submission 2025-02-18 15:58:37 -05:00
Alex
750345d209 feat: architecrure guide 2025-02-18 14:51:17 +00:00
Alex
03ee16f5ca Update _app.mdx 2025-02-18 09:09:37 +00:00
Alex
586fc80c19 Merge pull request #1640 from ManishMadan2882/docs 2025-02-18 08:54:28 +00:00
ManishMadan2882
13cd221fe5 (feat:widget) udpate docs 2025-02-18 14:19:53 +05:30
Siddhant Rai
f35af54e9f refactor: clean up code and improve UI elements in various components 2025-02-18 13:10:35 +05:30
Alex
67e37f1ce1 bump docs widget docs 2025-02-17 23:46:23 +00:00
ManishMadan2882
49ff27a5fe Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-02-18 04:22:32 +05:30
ManishMadan2882
04730ba8c7 (feat:theme)exacting the designs 2025-02-18 04:20:20 +05:30
Alex
b2fcf91958 Merge pull request #1637 from arc53/feat/sources-in-widget
Feat/sources in widget
2025-02-17 21:55:02 +00:00
ManishMadan2882
b78d2bd4b1 (feat:widget) add optional sources 2025-02-18 02:09:13 +05:30
dependabot[bot]
2612ce5ad9 build(deps): bump duckduckgo-search from 6.3.0 to 7.4.2 in /application
Bumps [duckduckgo-search](https://github.com/deedy5/duckduckgo_search) from 6.3.0 to 7.4.2.
- [Release notes](https://github.com/deedy5/duckduckgo_search/releases)
- [Commits](https://github.com/deedy5/duckduckgo_search/compare/v6.3.0...v7.4.2)

---
updated-dependencies:
- dependency-name: duckduckgo-search
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-17 20:34:44 +00:00
Manish Madan
798913740e Merge pull request #1317 from utin-francis-peter/feat/sources-in-react-widget
Feat/sources in react widget
2025-02-17 16:26:43 +05:30
Manish Madan
7d0445cc20 Merge branch 'main' into feat/sources-in-react-widget 2025-02-17 15:41:34 +05:30
Alex
361f6895ee Merge pull request #1514 from ayaan-qadri/Fixing-1513 2025-02-16 20:10:33 +00:00
Alex
47442f4f58 fix: bandit workflow only on main repo 2025-02-15 15:06:18 +00:00
dependabot[bot]
307c2e1682 build(deps): bump openapi-schema-validator in /application
Bumps [openapi-schema-validator](https://github.com/python-openapi/openapi-schema-validator) from 0.6.2 to 0.6.3.
- [Release notes](https://github.com/python-openapi/openapi-schema-validator/releases)
- [Commits](https://github.com/python-openapi/openapi-schema-validator/compare/0.6.2...0.6.3)

---
updated-dependencies:
- dependency-name: openapi-schema-validator
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-14 14:49:26 +00:00
Alex
2190359e4d Merge pull request #1631 from arc53/dependabot/pip/application/anthropic-0.45.2
build(deps): bump anthropic from 0.40.0 to 0.45.2 in /application
2025-02-14 14:47:32 +00:00
Alex
27a933c7b7 Merge pull request #1628 from ManishMadan2882/main
Smoother transition in settings
2025-02-14 14:47:16 +00:00
ManishMadan2882
71970a0d1d (feat:transit): reduce delay to 250 2025-02-14 20:06:50 +05:30
ManishMadan2882
7661273cfd (fix/logs) append loader 2025-02-14 19:50:19 +05:30
ManishMadan2882
cd06334049 (feat:transitions) uniform color and animation 2025-02-14 17:38:58 +05:30
Alex
05319e36a7 Merge pull request #1630 from siiddhantt/feat/show-tool-execution
feat: tool calls tracking
2025-02-14 10:27:15 +00:00
ManishMadan2882
200a3b81e5 (feat:loaders) loader for logs, dropdown 2025-02-14 02:20:29 +05:30
dependabot[bot]
5647755762 build(deps): bump anthropic from 0.40.0 to 0.45.2 in /application
Bumps [anthropic](https://github.com/anthropics/anthropic-sdk-python) from 0.40.0 to 0.45.2.
- [Release notes](https://github.com/anthropics/anthropic-sdk-python/releases)
- [Changelog](https://github.com/anthropics/anthropic-sdk-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/anthropics/anthropic-sdk-python/compare/v0.40.0...v0.45.2)

---
updated-dependencies:
- dependency-name: anthropic
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-13 20:24:49 +00:00
ManishMadan2882
adb2947b52 (feat:transitions): reshaped the tablular loaders 2025-02-14 01:01:07 +05:30
Siddhant Rai
7b05afab74 refactor: formatting + token limit for gemini-2.0-flash-exp 2025-02-14 00:27:27 +05:30
Siddhant Rai
5cf5bed6a8 feat: enhance tool call handling with structured message cleaning and improved UI display 2025-02-14 00:15:01 +05:30
Alex
095cb58df3 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-02-13 11:48:59 +00:00
ManishMadan2882
181bf69994 (feat:transitions) custom hook to loading state 2025-02-13 17:16:17 +05:30
Alex
927b513bf8 fix: error warning 2025-02-13 11:44:05 +00:00
Alex
05801cd90c Update README.md 2025-02-13 11:37:47 +00:00
Alex
a8ac00469d fix: bandit 2025-02-13 11:30:02 +00:00
Alex
1e3ae948a2 feat: add static code analysis 2025-02-13 11:25:03 +00:00
Alex
2d8aa229c6 fix: used for security comment 2025-02-13 11:10:44 +00:00
Alex
84f4812189 Update CONTRIBUTING.md 2025-02-13 10:48:49 +00:00
Siddhant Rai
8a3612e56c fix: improve tool call handling and UI adjustments 2025-02-13 05:02:10 +05:30
ManishMadan2882
d08861fb30 (fix/docs) revert effected portions 2025-02-13 01:16:11 +05:30
ManishMadan2882
ecc0f9d9f5 Merge branch 'main' of https://github.com/arc53/docsgpt 2025-02-13 00:17:58 +05:30
Siddhant Rai
e209699b19 feat: add tool calls tracking and show in frontend 2025-02-12 21:47:47 +05:30
Alex
c8d8690cfd Update README.md 2025-02-12 16:09:53 +00:00
Alex
59d05b698a Update README.md 2025-02-12 16:09:22 +00:00
Alex
1bcbfc8d18 Merge pull request #1629 from arc53/easy-deploy 2025-02-12 15:13:36 +00:00
Pavel
bafed63d40 super-final 2025-02-12 17:41:59 +03:00
ManishMadan2882
828a056e21 Merge branch 'main' of https://github.com/ManishMadan2882/docsgpt 2025-02-12 20:10:36 +05:30
ManishMadan2882
9424f6303a (feat:upload) smooth transitions on advanced fields 2025-02-12 20:10:21 +05:30
Pavel
c0dc5c3a4d finished 2025-02-12 17:33:11 +03:00
Alex
d0fb3da285 Merge pull request #1626 from arc53/dependabot/pip/application/prompt-toolkit-3.0.50
build(deps): bump prompt-toolkit from 3.0.48 to 3.0.50 in /application
2025-02-12 13:49:50 +00:00
Pavel
ccce01800d index-desc 2025-02-12 14:43:24 +03:00
Pavel
b44b9d8016 models+guide-upd+extentions 2025-02-12 14:38:21 +03:00
dependabot[bot]
7592c45bd9 build(deps): bump prompt-toolkit from 3.0.48 to 3.0.50 in /application
Bumps [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) from 3.0.48 to 3.0.50.
- [Release notes](https://github.com/prompt-toolkit/python-prompt-toolkit/releases)
- [Changelog](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/CHANGELOG)
- [Commits](https://github.com/prompt-toolkit/python-prompt-toolkit/compare/3.0.48...3.0.50)

---
updated-dependencies:
- dependency-name: prompt-toolkit
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-12 10:32:09 +00:00
Alex
b024936ad7 Update devc-welcome.md 2025-02-12 09:48:21 +00:00
Manish Madan
be2246283f Merge branch 'arc53:main' into main 2025-02-12 00:00:23 +05:30
ManishMadan2882
a7969f6ec8 Merge branch 'main' of https://github.com/ManishMadan2882/docsgpt 2025-02-11 23:58:14 +05:30
ManishMadan2882
ac447dd055 (feat:settings) smoother transitions 2025-02-11 23:55:30 +05:30
Pavel
28cdbe407c tools-remove 2025-02-11 21:19:09 +03:00
Alex
bf486082c9 fix: centering 2025-02-11 17:27:45 +00:00
Alex
41290b463c feat: cards 2025-02-11 17:17:12 +00:00
Pavel
385ebe234e setup+development-docs 2025-02-11 19:17:59 +03:00
Alex
72e9fcc895 Update README.md 2025-02-11 16:08:48 +00:00
Alex
5f42e4ac3f fix: default file codespace 2025-02-11 09:53:26 +00:00
Alex
926ec89f48 Create devc-welcome.md 2025-02-11 09:48:45 +00:00
GH Action - Upstream Sync
440e1b9156 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-02-11 01:19:54 +00:00
ManishMadan2882
ea0a6e413d (feat:bubble) formattedtable/inline code 2025-02-11 02:07:54 +05:30
Alex
0de4241b56 Merge pull request #1625 from arc53/handle-bad-tool-names
Handle bad tool names
2025-02-10 17:25:45 +00:00
Alex
6e8a53a204 fix: open new tool after its added 2025-02-10 16:52:59 +00:00
Alex
60772889d5 fix: handle bad tool name input 2025-02-10 16:20:37 +00:00
Alex
7db7c9e978 Merge pull request #1612 from arc53/dependabot/pip/application/marshmallow-3.26.1
build(deps): bump marshmallow from 3.24.1 to 3.26.1 in /application
2025-02-10 13:19:04 +00:00
dependabot[bot]
d85bf67103 build(deps): bump marshmallow from 3.24.1 to 3.26.1 in /application
Bumps [marshmallow](https://github.com/marshmallow-code/marshmallow) from 3.24.1 to 3.26.1.
- [Changelog](https://github.com/marshmallow-code/marshmallow/blob/dev/CHANGELOG.rst)
- [Commits](https://github.com/marshmallow-code/marshmallow/compare/3.24.1...3.26.1)

---
updated-dependencies:
- dependency-name: marshmallow
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-10 13:06:17 +00:00
Alex
926f2e9f48 Merge pull request #1621 from ManishMadan2882/main
Refactor Upload
2025-02-10 13:04:53 +00:00
ManishMadan2882
2019f29e8c (clean) mock changes 2025-02-10 16:02:37 +05:30
ManishMadan2882
3b45b63d2a (fix:upload) ui adjust 2025-02-10 16:02:02 +05:30
Alex
1c08c53121 Merge pull request #1624 from siiddhantt/feat/edit-chunks
feat: view chunks for docs and add/delete them
2025-02-10 09:48:11 +00:00
Siddhant Rai
7623bde159 feat: add update chunk API endpoint and service method 2025-02-10 09:36:18 +05:30
GH Action - Upstream Sync
1ed0f5e78d Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-02-10 01:21:14 +00:00
Piotr Idzik
568ab33a37 style: use underscore for an unused loop variable (#1593)
This addresses the SC2034 warning.
2025-02-09 22:56:52 +00:00
Alex
f639b052e3 fix: remove debugging code 2025-02-09 12:08:19 +00:00
Alex
56f91948f8 feat: improve logging 2025-02-09 12:05:37 +00:00
GH Action - Upstream Sync
6c5e481318 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-02-09 01:24:07 +00:00
ManishMadan2882
f487f1e8c1 Merge branch 'main' of https://github.com/ManishMadan2882/docsgpt 2025-02-09 01:31:15 +05:30
ManishMadan2882
68ee9743fe (feat:upload) advanced fields 2025-02-09 01:31:01 +05:30
Alex
f4cb48ed0d fix: logging 2025-02-08 19:29:54 +00:00
Alex
ad77fe1116 fix: logging in submodules 2025-02-08 18:07:03 +00:00
Alex
28a0667da6 fix: minor logging issue 2025-02-08 12:49:42 +00:00
Siddhant Rai
1f0366c989 Merge branch 'feat/edit-chunks' of https://github.com/siiddhantt/DocsGPT into feat/edit-chunks 2025-02-08 15:00:20 +05:30
Siddhant Rai
3a51922650 fix: linting error 2025-02-08 15:00:02 +05:30
Siddhant Rai
82b2be5046 Merge branch 'main' into feat/edit-chunks 2025-02-08 14:56:34 +05:30
Siddhant Rai
0fc9718c35 feat: loading state and spinner + delete chunk option 2025-02-08 14:52:32 +05:30
GH Action - Upstream Sync
976733a3c3 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-02-08 01:17:57 +00:00
Alex
5d17072709 fix: readme space 2025-02-07 19:22:01 +00:00
Alex
fbad183d39 fix: post create devcontainers 2025-02-07 18:44:24 +00:00
Alex
7356a2ff07 fix: minor docker fixes 2025-02-07 18:39:07 +00:00
Alex
6ff948c107 fix: dockerfile in devcontainer build dir fix 2025-02-07 14:35:44 +00:00
Alex
e3ebce117b fix: devcontainer paths 2 2025-02-07 14:34:19 +00:00
Alex
ce69b09730 fix: devcontainer paths 2025-02-07 14:30:15 +00:00
Alex
c823cef405 fix: devcontainer codespaces correct api address 2025-02-07 14:25:09 +00:00
Siddhant Rai
0379b81d43 feat: view and add document chunks for mongodb and faiss 2025-02-07 19:39:07 +05:30
ManishMadan2882
6a997163fd Merge branch 'main' of https://github.com/ManishMadan2882/docsgpt 2025-02-07 19:37:39 +05:30
ManishMadan2882
93f8466230 (feat:Upload): required form fields 2025-02-07 19:37:24 +05:30
ManishMadan2882
114c8d3c22 (feat:Input) required prop 2025-02-07 17:59:28 +05:30
GH Action - Upstream Sync
3e77e79194 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-02-07 01:20:29 +00:00
dependabot[bot]
ca91d36979 build(deps): bump pydantic from 2.10.4 to 2.10.6 in /application
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.10.4 to 2.10.6.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.10.4...v2.10.6)

---
updated-dependencies:
- dependency-name: pydantic
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-06 20:01:07 +00:00
Alex
d47232246a fix: remove old pypdf 2025-02-06 19:59:42 +00:00
Alex
d819222cf7 fix: remove unused import 2025-02-06 19:46:11 +00:00
Alex
0c4c4d5622 fix: improve error logging 2025-02-06 19:44:09 +00:00
Alex
ad051ed083 fix: docker compose files 2025-02-06 18:51:17 +00:00
ManishMadan2882
1aa0af3e58 Merge branch 'main' of https://github.com/ManishMadan2882/docsgpt 2025-02-06 20:16:31 +05:30
ManishMadan2882
72556b37f5 (refactor:upload) simplify call to /api/remote 2025-02-06 20:16:07 +05:30
Manish Madan
0bddae5775 Merge branch 'arc53:main' into main 2025-02-06 04:09:08 +05:30
ManishMadan2882
1f1e710a6d Merge branch 'main' of https://github.com/ManishMadan2882/docsgpt 2025-02-06 04:07:51 +05:30
ManishMadan2882
b57d418b98 (refactor:upload) remove redundant types 2025-02-06 04:04:18 +05:30
Alex
0913c43219 feat: edit deploymen files locations 2025-02-05 18:04:41 +00:00
Alex
d754a43fba feat: devcontainer 2025-02-05 11:54:06 +00:00
Manish Madan
f97b56a87b (fix): date formatting (#1617) 2025-02-05 10:20:14 +00:00
Michele Grimaldi
2f78398914 Fixing issues #1445 (#1603)
* Fixing issues #1445

* Fixed issue #1445
2025-02-05 08:52:18 +00:00
Manish Madan
81b9a34e5e Merge branch 'arc53:main' into main 2025-02-05 03:33:20 +05:30
Alex
73ba078efc fix: init push to ghcr 2025-02-04 22:02:56 +00:00
ManishMadan2882
1ffe0ad85c (fix): date formatting 2025-02-05 03:31:17 +05:30
Alex
797b36a81e fix: container name 2025-02-04 21:56:26 +00:00
Alex
b82c14892e Arm builds (#1615)
* fix: matrix build

* fix: trigger build

* fix: trigger wrong name

* fix: runner name

* fix: manifest fix

* fix: yaml error

* fix: manifest build

* fix: build error

* feat: multi arch containers
2025-02-04 21:02:44 +00:00
Alex
a8891dabec feat: docker arm64 2025-02-04 17:46:44 +00:00
Alex
86ba797665 Merge pull request #1614 from arc53/documentation-footer
footer text with links
2025-02-04 17:35:22 +00:00
Pavel
3830dcb3f3 footer text with links 2025-02-04 19:42:24 +03:00
Alex
c20fe7a773 Update docker-compose.yaml 2025-02-03 13:21:28 +00:00
Ayaan
220a801138 Requested changes 2025-01-29 20:29:39 +05:30
Ayaan
c6821d9cc3 Improve edit message interface 2025-01-29 20:11:55 +05:30
Ayaan
8b59245e6a Decreased margin right for message hover button 2025-01-29 20:06:10 +05:30
utin-francis-peter
2c8a2945f0 feat: better sources scroll management 2024-11-17 10:34:22 +01:00
utin-francis-peter
ba59042e5c Merge branch 'main' into feat/sources-in-react-widget 2024-11-17 09:19:20 +01:00
utin-francis-peter
6f83bd8961 Merge branch 'main' into feat/sources-in-react-widget 2024-11-11 14:36:34 +01:00
utin-francis-peter
a7aae3ff7e style: minor adjustments in border-radius and spacings 2024-11-10 03:29:56 +01:00
utin-francis-peter
25feab9a29 chore: removed unused import 2024-11-10 03:11:41 +01:00
utin-francis-peter
97916bf925 chore: returned themes cofig into DocsGPTWidget component 2024-11-10 03:08:35 +01:00
utin-francis-peter
42e2c784c4 Merge branch 'main' of https://github.com/utin-francis-peter/DocsGPT into feat/sources-in-react-widget 2024-11-10 03:06:23 +01:00
utin-francis-peter
1a8f89573d feat: query sources in widget 2024-11-09 01:09:22 +01:00
utin-francis-peter
3e87d83ae8 chore: adjusted spacing in source bubble 2024-11-05 21:50:42 +01:00
utin-francis-peter
0784823e21 Merge branch 'main' of https://github.com/utin-francis-peter/DocsGPT into feat/sources-in-react-widget 2024-11-04 16:36:13 +01:00
utin-francis-peter
1a9f47b1bc chore: modified query sources and removed tooltip 2024-11-04 16:33:00 +01:00
utin-francis-peter
991a38df28 Merge branch 'main' of https://github.com/utin-francis-peter/DocsGPT into feat/sources-in-react-widget 2024-10-28 17:44:50 +01:00
utin-francis-peter
656f4da8f9 feat: rendering of response source 2024-10-28 17:34:35 +01:00
utin-francis-peter
f8d65b84db chore: wrapped the base component with ThemeProvider at the root level to make theme props available globally 2024-10-28 17:33:40 +01:00
utin-francis-peter
bd66d0a987 Merge branch 'main' of https://github.com/utin-francis-peter/DocsGPT into feat/sources-in-react-widget 2024-10-14 13:02:38 +01:00
utin-francis-peter
62802eb138 chore: styled component styles for sources, added showSources prop to widget, handled sources data.type, and rendering sources when available 2024-10-14 12:37:02 +01:00
utin-francis-peter
848beb11df chore: corrected typo in var declaration 2024-10-14 12:33:56 +01:00
utin-francis-peter
0481e766ae chore: updated Query and WidgetProps interface with source property 2024-10-14 12:30:57 +01:00
utin-francis-peter
aa57984bde build: added missing dependency 2024-10-11 03:55:35 +01:00
381 changed files with 35401 additions and 11766 deletions

15
.devcontainer/Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
FROM python:3.12-bookworm
# Install Node.js 20.x
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y nodejs \
&& rm -rf /var/lib/apt/lists/*
# Install global npm packages
RUN npm install -g husky vite
# Create and activate Python virtual environment
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
WORKDIR /workspace

View File

@@ -0,0 +1,49 @@
# Welcome to DocsGPT Devcontainer
Welcome to the DocsGPT development environment! This guide will help you get started quickly.
## Starting Services
To run DocsGPT, you need to start three main services: Flask (backend), Celery (task queue), and Vite (frontend). Here are the commands to start each service within the devcontainer:
### Vite (Frontend)
```bash
cd frontend
npm run dev -- --host
```
### Flask (Backend)
```bash
flask --app application/app.py run --host=0.0.0.0 --port=7091
```
### Celery (Task Queue)
```bash
celery -A application.app.celery worker -l INFO
```
## Github Codespaces Instructions
### 1. Make Ports Public:
Go to the "Ports" panel in Codespaces (usually located at the bottom of the VS Code window).
For both port 5173 and 7091, right-click on the port and select "Make Public".
![CleanShot 2025-02-12 at 09 46 14@2x](https://github.com/user-attachments/assets/00a34b16-a7ef-47af-9648-87a7e3008475)
### 2. Update VITE_API_HOST:
After making port 7091 public, copy the public URL provided by Codespaces for port 7091.
Open the file frontend/.env.development.
Find the line VITE_API_HOST=http://localhost:7091.
Replace http://localhost:7091 with the public URL you copied from Codespaces.
![CleanShot 2025-02-12 at 09 46 56@2x](https://github.com/user-attachments/assets/c472242f-1079-4cd8-bc0b-2d78db22b94c)

View File

@@ -0,0 +1,24 @@
{
"name": "DocsGPT Dev Container",
"dockerComposeFile": ["docker-compose-dev.yaml", "docker-compose.override.yaml"],
"service": "dev",
"workspaceFolder": "/workspace",
"postCreateCommand": ".devcontainer/post-create-command.sh",
"forwardPorts": [7091, 5173, 6379, 27017],
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-toolsai.jupyter",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint"
]
},
"codespaces": {
"openFiles": [
".devcontainer/devc-welcome.md",
"CONTRIBUTING.md"
]
}
}
}

View File

@@ -0,0 +1,40 @@
version: '3.8'
services:
dev:
build:
context: .
dockerfile: Dockerfile
volumes:
- ../:/workspace:cached
command: sleep infinity
depends_on:
redis:
condition: service_healthy
mongo:
condition: service_healthy
environment:
- 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
networks:
- default
redis:
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 30s
retries: 5
mongo:
healthcheck:
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
interval: 5s
timeout: 30s
retries: 5
networks:
default:
name: docsgpt-dev-network

View File

@@ -0,0 +1,32 @@
#!/bin/bash
set -e # Exit immediately if a command exits with a non-zero status
if [ ! -f frontend/.env.development ]; then
cp -n .env-template frontend/.env.development || true # Assuming .env-template is in the root
fi
# Determine VITE_API_HOST based on environment
if [ -n "$CODESPACES" ]; then
# Running in Codespaces
CODESPACE_NAME=$(echo "$CODESPACES" | cut -d'-' -f1) # Extract codespace name
PUBLIC_API_HOST="https://${CODESPACE_NAME}-7091.${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}"
echo "Setting VITE_API_HOST for Codespaces: $PUBLIC_API_HOST in frontend/.env.development"
sed -i "s|VITE_API_HOST=.*|VITE_API_HOST=$PUBLIC_API_HOST|" frontend/.env.development
else
# Not running in Codespaces (local devcontainer)
DEFAULT_API_HOST="http://localhost:7091"
echo "Setting VITE_API_HOST for local dev: $DEFAULT_API_HOST in frontend/.env.development"
sed -i "s|VITE_API_HOST=.*|VITE_API_HOST=$DEFAULT_API_HOST|" frontend/.env.development
fi
mkdir -p model
if [ ! -d model/all-mpnet-base-v2 ]; then
wget -q https://d3dg1063dc54p9.cloudfront.net/models/embeddings/mpnet-base-v2.zip -O model/mpnet-base-v2.zip
unzip -q model/mpnet-base-v2.zip -d model
rm model/mpnet-base-v2.zip
fi
pip install -r application/requirements.txt
cd frontend
npm install --include=dev

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

40
.github/workflows/bandit.yaml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: Bandit Security Scan
on:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened]
jobs:
bandit_scan:
if: ${{ github.repository == 'arc53/DocsGPT' }}
runs-on: ubuntu-latest
permissions:
security-events: write
actions: read
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install bandit # Bandit is needed for this action
if [ -f application/requirements.txt ]; then pip install -r application/requirements.txt; fi
- name: Run Bandit scan
uses: PyCQA/bandit-action@v1
with:
severity: medium
confidence: medium
targets: application/
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -5,20 +5,33 @@ on:
types: [published]
jobs:
deploy:
build:
if: github.repository == 'arc53/DocsGPT'
runs-on: ubuntu-latest
strategy:
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
suffix: amd64
- platform: linux/arm64
runner: ubuntu-24.04-arm
suffix: arm64
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
- name: Set up QEMU # Only needed for emulation, not for native arm64 builds
if: matrix.platform == 'linux/arm64'
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver: docker-container
install: true
- name: Login to DockerHub
uses: docker/login-action@v3
@@ -33,15 +46,67 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker images to docker.io and ghcr.io
- name: Build and push platform-specific images
uses: docker/build-push-action@v6
with:
file: './application/Dockerfile'
platforms: linux/amd64
platforms: ${{ matrix.platform }}
context: ./application
push: true
tags: |
${{ secrets.DOCKER_USERNAME }}/docsgpt:${{ github.event.release.tag_name }},${{ secrets.DOCKER_USERNAME }}/docsgpt:latest
ghcr.io/${{ github.repository_owner }}/docsgpt:${{ github.event.release.tag_name }},ghcr.io/${{ github.repository_owner }}/docsgpt:latest
${{ secrets.DOCKER_USERNAME }}/docsgpt:${{ github.event.release.tag_name }}-${{ matrix.suffix }}
ghcr.io/${{ github.repository_owner }}/docsgpt:${{ github.event.release.tag_name }}-${{ matrix.suffix }}
provenance: false
sbom: false
cache-from: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/docsgpt:latest
cache-to: type=inline
manifest:
if: github.repository == 'arc53/DocsGPT'
needs: build
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver: docker-container
install: true
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to ghcr.io
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create and push manifest for DockerHub
run: |
set -e
docker manifest create ${{ secrets.DOCKER_USERNAME }}/docsgpt:${{ github.event.release.tag_name }} \
--amend ${{ secrets.DOCKER_USERNAME }}/docsgpt:${{ github.event.release.tag_name }}-amd64 \
--amend ${{ secrets.DOCKER_USERNAME }}/docsgpt:${{ github.event.release.tag_name }}-arm64
docker manifest push ${{ secrets.DOCKER_USERNAME }}/docsgpt:${{ github.event.release.tag_name }}
docker manifest create ${{ secrets.DOCKER_USERNAME }}/docsgpt:latest \
--amend ${{ secrets.DOCKER_USERNAME }}/docsgpt:${{ github.event.release.tag_name }}-amd64 \
--amend ${{ secrets.DOCKER_USERNAME }}/docsgpt:${{ github.event.release.tag_name }}-arm64
docker manifest push ${{ secrets.DOCKER_USERNAME }}/docsgpt:latest
- name: Create and push manifest for ghcr.io
run: |
set -e
docker manifest create ghcr.io/${{ github.repository_owner }}/docsgpt:${{ github.event.release.tag_name }} \
--amend ghcr.io/${{ github.repository_owner }}/docsgpt:${{ github.event.release.tag_name }}-amd64 \
--amend ghcr.io/${{ github.repository_owner }}/docsgpt:${{ github.event.release.tag_name }}-arm64
docker manifest push ghcr.io/${{ github.repository_owner }}/docsgpt:${{ github.event.release.tag_name }}
docker manifest create ghcr.io/${{ github.repository_owner }}/docsgpt:latest \
--amend ghcr.io/${{ github.repository_owner }}/docsgpt:${{ github.event.release.tag_name }}-amd64 \
--amend ghcr.io/${{ github.repository_owner }}/docsgpt:${{ github.event.release.tag_name }}-arm64
docker manifest push ghcr.io/${{ github.repository_owner }}/docsgpt:latest

View File

@@ -5,20 +5,33 @@ on:
types: [published]
jobs:
deploy:
build:
if: github.repository == 'arc53/DocsGPT'
runs-on: ubuntu-latest
strategy:
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
suffix: amd64
- platform: linux/arm64
runner: ubuntu-24.04-arm
suffix: arm64
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
- name: Set up QEMU # Only needed for emulation, not for native arm64 builds
if: matrix.platform == 'linux/arm64'
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver: docker-container
install: true
- name: Login to DockerHub
uses: docker/login-action@v3
@@ -33,16 +46,67 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
# Runs a single command using the runners shell
- name: Build and push Docker images to docker.io and ghcr.io
- name: Build and push platform-specific images
uses: docker/build-push-action@v6
with:
file: './frontend/Dockerfile'
platforms: linux/amd64, linux/arm64
platforms: ${{ matrix.platform }}
context: ./frontend
push: true
tags: |
${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:${{ github.event.release.tag_name }},${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:latest
ghcr.io/${{ github.repository_owner }}/docsgpt-fe:${{ github.event.release.tag_name }},ghcr.io/${{ github.repository_owner }}/docsgpt-fe:latest
${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:${{ github.event.release.tag_name }}-${{ matrix.suffix }}
ghcr.io/${{ github.repository_owner }}/docsgpt-fe:${{ github.event.release.tag_name }}-${{ matrix.suffix }}
provenance: false
sbom: false
cache-from: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:latest
cache-to: type=inline
manifest:
if: github.repository == 'arc53/DocsGPT'
needs: build
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver: docker-container
install: true
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to ghcr.io
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create and push manifest for DockerHub
run: |
set -e
docker manifest create ${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:${{ github.event.release.tag_name }} \
--amend ${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:${{ github.event.release.tag_name }}-amd64 \
--amend ${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:${{ github.event.release.tag_name }}-arm64
docker manifest push ${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:${{ github.event.release.tag_name }}
docker manifest create ${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:latest \
--amend ${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:${{ github.event.release.tag_name }}-amd64 \
--amend ${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:${{ github.event.release.tag_name }}-arm64
docker manifest push ${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:latest
- name: Create and push manifest for ghcr.io
run: |
set -e
docker manifest create ghcr.io/${{ github.repository_owner }}/docsgpt-fe:${{ github.event.release.tag_name }} \
--amend ghcr.io/${{ github.repository_owner }}/docsgpt-fe:${{ github.event.release.tag_name }}-amd64 \
--amend ghcr.io/${{ github.repository_owner }}/docsgpt-fe:${{ github.event.release.tag_name }}-arm64
docker manifest push ghcr.io/${{ github.repository_owner }}/docsgpt-fe:${{ github.event.release.tag_name }}
docker manifest create ghcr.io/${{ github.repository_owner }}/docsgpt-fe:latest \
--amend ghcr.io/${{ github.repository_owner }}/docsgpt-fe:${{ github.event.release.tag_name }}-amd64 \
--amend ghcr.io/${{ github.repository_owner }}/docsgpt-fe:${{ github.event.release.tag_name }}-arm64
docker manifest push ghcr.io/${{ github.repository_owner }}/docsgpt-fe:latest

View File

@@ -1,4 +1,4 @@
name: Build and push DocsGPT Docker image for development
name: Build and push multi-arch DocsGPT Docker image
on:
workflow_dispatch:
@@ -7,27 +7,36 @@ on:
- main
jobs:
deploy:
build:
if: github.repository == 'arc53/DocsGPT'
runs-on: ubuntu-latest
strategy:
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
suffix: amd64
- platform: linux/arm64
runner: ubuntu-24.04-arm
suffix: arm64
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver: docker-container
install: true
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to ghcr.io
uses: docker/login-action@v3
with:
@@ -35,15 +44,57 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker images to docker.io and ghcr.io
- name: Build and push platform-specific images
uses: docker/build-push-action@v6
with:
file: './application/Dockerfile'
platforms: linux/amd64
platforms: ${{ matrix.platform }}
context: ./application
push: true
tags: |
${{ secrets.DOCKER_USERNAME }}/docsgpt:develop
ghcr.io/${{ github.repository_owner }}/docsgpt:develop
${{ secrets.DOCKER_USERNAME }}/docsgpt:develop-${{ matrix.suffix }}
ghcr.io/${{ github.repository_owner }}/docsgpt:develop-${{ matrix.suffix }}
provenance: false
sbom: false
cache-from: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/docsgpt:develop
cache-to: type=inline
manifest:
if: github.repository == 'arc53/DocsGPT'
needs: build
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver: docker-container
install: true
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to ghcr.io
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create and push manifest for DockerHub
run: |
docker manifest create ${{ secrets.DOCKER_USERNAME }}/docsgpt:develop \
--amend ${{ secrets.DOCKER_USERNAME }}/docsgpt:develop-amd64 \
--amend ${{ secrets.DOCKER_USERNAME }}/docsgpt:develop-arm64
docker manifest push ${{ secrets.DOCKER_USERNAME }}/docsgpt:develop
- name: Create and push manifest for ghcr.io
run: |
docker manifest create ghcr.io/${{ github.repository_owner }}/docsgpt:develop \
--amend ghcr.io/${{ github.repository_owner }}/docsgpt:develop-amd64 \
--amend ghcr.io/${{ github.repository_owner }}/docsgpt:develop-arm64
docker manifest push ghcr.io/${{ github.repository_owner }}/docsgpt:develop

View File

@@ -7,20 +7,33 @@ on:
- main
jobs:
deploy:
build:
if: github.repository == 'arc53/DocsGPT'
runs-on: ubuntu-latest
strategy:
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
suffix: amd64
- platform: linux/arm64
runner: ubuntu-24.04-arm
suffix: arm64
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
- name: Set up QEMU # Only needed for emulation, not for native arm64 builds
if: matrix.platform == 'linux/arm64'
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver: docker-container
install: true
- name: Login to DockerHub
uses: docker/login-action@v3
@@ -35,15 +48,57 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker images to docker.io and ghcr.io
- name: Build and push platform-specific images
uses: docker/build-push-action@v6
with:
file: './frontend/Dockerfile'
platforms: linux/amd64
platforms: ${{ matrix.platform }}
context: ./frontend
push: true
tags: |
${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:develop
ghcr.io/${{ github.repository_owner }}/docsgpt-fe:develop
${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:develop-${{ matrix.suffix }}
ghcr.io/${{ github.repository_owner }}/docsgpt-fe:develop-${{ matrix.suffix }}
provenance: false
sbom: false
cache-from: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:develop
cache-to: type=inline
manifest:
if: github.repository == 'arc53/DocsGPT'
needs: build
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver: docker-container
install: true
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to ghcr.io
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create and push manifest for DockerHub
run: |
docker manifest create ${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:develop \
--amend ${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:develop-amd64 \
--amend ${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:develop-arm64
docker manifest push ${{ secrets.DOCKER_USERNAME }}/docsgpt-fe:develop
- name: Create and push manifest for ghcr.io
run: |
docker manifest create ghcr.io/${{ github.repository_owner }}/docsgpt-fe:develop \
--amend ghcr.io/${{ github.repository_owner }}/docsgpt-fe:develop-amd64 \
--amend ghcr.io/${{ github.repository_owner }}/docsgpt-fe:develop-arm64
docker manifest push ghcr.io/${{ github.repository_owner }}/docsgpt-fe:develop

View File

@@ -10,7 +10,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies

1
.gitignore vendored
View File

@@ -113,6 +113,7 @@ venv.bak/
# Spyder project settings
.spyderproject
.spyproject
.jwt_secret_key
# Rope project settings
.ropeproject

View File

@@ -35,18 +35,40 @@ Tech Stack Overview:
- 🖥 Backend: Developed in Python 🐍
### 🌐 If you are looking to contribute to frontend (⚛React, Vite):
### 🌐 Frontend Contributions (⚛️ React, Vite)
* The updated Figma design can be found [here](https://www.figma.com/file/OXLtrl1EAy885to6S69554/DocsGPT?node-id=0%3A1&t=hjWVuxRg9yi5YkJ9-1). Please try to follow the guidelines.
* **Coding Style:** We follow a strict coding style enforced by ESLint and Prettier. Please ensure your code adheres to the configuration provided in our repository's `fronetend/.eslintrc.js` file. We recommend configuring your editor with ESLint and Prettier to help with this.
* **Component Structure:** Strive for small, reusable components. Favor functional components and hooks over class components where possible.
* **State Management** If you need to add stores, please use Redux.
- The updated Figma design can be found [here](https://www.figma.com/file/OXLtrl1EAy885to6S69554/DocsGPT?node-id=0%3A1&t=hjWVuxRg9yi5YkJ9-1).
Please try to follow the guidelines.
### 🖥 If you are looking to contribute to Backend (🐍 Python):
### 🖥 Backend Contributions (🐍 Python)
- Review our issues and contribute to [`/application`](https://github.com/arc53/DocsGPT/tree/main/application)
- All new code should be covered with unit tests ([pytest](https://github.com/pytest-dev/pytest)). Please find tests under [`/tests`](https://github.com/arc53/DocsGPT/tree/main/tests) folder.
- Before submitting your Pull Request, ensure it can be queried after ingesting some test data.
- **Coding Style:** We adhere to the [PEP 8](https://www.python.org/dev/peps/pep-0008/) style guide for Python code. We use `ruff` as our linter and code formatter. Please ensure your code is formatted correctly and passes `ruff` checks before submitting.
- **Type Hinting:** Please use type hints for all function arguments and return values. This improves code readability and helps catch errors early. Example:
```python
def my_function(name: str, count: int) -> list[str]:
...
```
- **Docstrings:** All functions and classes should have docstrings explaining their purpose, parameters, and return values. We prefer the [Google style docstrings](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html). Example:
```python
def my_function(name: str, count: int) -> list[str]:
"""Does something with a name and a count.
Args:
name: The name to use.
count: The number of times to do it.
Returns:
A list of strings.
"""
...
```
### Testing

105
README.md
View File

@@ -3,11 +3,11 @@
</h1>
<p align="center">
<strong>Open-Source RAG Assistant</strong>
<strong>Private AI for agents, assistants and enterprise search</strong>
</p>
<p align="left">
<strong><a href="https://www.docsgpt.cloud/">DocsGPT</a></strong> is an open-source genAI tool that helps users get reliable answers from any knowledge source, while avoiding hallucinations. It enables quick and reliable information retrieval, with tooling and agentic system capability built in.
<strong><a href="https://www.docsgpt.cloud/">DocsGPT</a></strong> is an open-source AI platform for building intelligent agents and assistants. Features Agent Builder, deep research tools, document analysis (PDF, Office, web content), Multi-model support (choose your provider or run locally), and rich API connectivity for agents with actionable tools and integrations. Deploy anywhere with complete privacy control.
</p>
<div align="center">
@@ -15,14 +15,14 @@
<a href="https://github.com/arc53/DocsGPT">![link to main GitHub showing Stars number](https://img.shields.io/github/stars/arc53/docsgpt?style=social)</a>
<a href="https://github.com/arc53/DocsGPT">![link to main GitHub showing Forks number](https://img.shields.io/github/forks/arc53/docsgpt?style=social)</a>
<a href="https://github.com/arc53/DocsGPT/blob/main/LICENSE">![link to license file](https://img.shields.io/github/license/arc53/docsgpt)</a>
<a href="https://www.bestpractices.dev/projects/9907"><img src="https://www.bestpractices.dev/projects/9907/badge"></a>
<a href="https://discord.gg/n5BX8dh8rU">![link to discord](https://img.shields.io/discord/1070046503302877216)</a>
<a href="https://twitter.com/docsgptai">![X (formerly Twitter) URL](https://img.shields.io/twitter/follow/docsgptai)</a>
<br>
[☁️ Cloud Version](https://app.docsgpt.cloud/) • [💬 Discord](https://discord.gg/n5BX8dh8rU) • [📖 Guides](https://docs.docsgpt.cloud/)
<br>
[👫 Contribute](https://github.com/arc53/DocsGPT/blob/main/CONTRIBUTING.md) • [🏠 Self-host](https://docs.docsgpt.cloud/Guides/How-to-use-different-LLM) • [⚡️ Quickstart](https://github.com/arc53/DocsGPT#quickstart)
<a href="https://docs.docsgpt.cloud/quickstart">⚡️ Quickstart</a><a href="https://app.docsgpt.cloud/">☁️ Cloud Version</a><a href="https://discord.gg/n5BX8dh8rU">💬 Discord</a>
<br>
<a href="https://docs.docsgpt.cloud/">📖 Documentation</a><a href="https://github.com/arc53/DocsGPT/blob/main/CONTRIBUTING.md">👫 Contribute</a><a href="https://blog.docsgpt.cloud/">🗞 Blog</a>
<br>
</div>
<div align="center">
@@ -35,6 +35,7 @@
<li><strong>🗂️ Wide Format Support:</strong> Reads PDF, DOCX, CSV, XLSX, EPUB, MD, RST, HTML, MDX, JSON, PPTX, and images.</li>
<li><strong>🌐 Web & Data Integration:</strong> Ingests from URLs, sitemaps, Reddit, GitHub and web crawlers.</li>
<li><strong>✅ Reliable Answers:</strong> Get accurate, hallucination-free responses with source citations viewable in a clean UI.</li>
<li><strong>🔑 Streamlined API Keys:</strong> Generate keys linked to your settings, documents, and models, simplifying chatbot and integration setup.</li>
<li><strong>🔗 Actionable Tooling:</strong> Connect to APIs, tools, and other services to enable LLM actions.</li>
<li><strong>🧩 Pre-built Integrations:</strong> Use readily available HTML/React chat widgets, search tools, Discord/Telegram bots, and more.</li>
<li><strong>🔌 Flexible Deployment:</strong> Works with major LLMs (OpenAI, Google, Anthropic) and local models (Ollama, llama_cpp).</li>
@@ -45,12 +46,19 @@
- [x] Full GoogleAI compatibility (Jan 2025)
- [x] Add tools (Jan 2025)
- [ ] Anthropic Tool compatibility
- [ ] Add triggerable actions / tools (webhook)
- [ ] Add OAuth 2.0 authentication for tools and sources
- [ ] Manually updating chunks in the app UI
- [ ] Devcontainer for easy development
- [ ] Chatbots menu re-design to handle tools, scheduling, and more
- [x] Manually updating chunks in the app UI (Feb 2025)
- [x] Devcontainer for easy development (Feb 2025)
- [x] ReACT agent (March 2025)
- [x] Chatbots menu re-design to handle tools, agent types, and more (April 2025)
- [x] New input box in the conversation menu (April 2025)
- [x] Add triggerable actions / tools (webhook) (April 2025)
- [x] Agent optimisations (May 2025)
- [x] Filesystem sources update (July 2025)
- [x] Json Responses (August 2025)
- [ ] Sharepoint integration (August 2025)
- [ ] MCP support (August 2025)
- [ ] Add OAuth 2.0 authentication for tools and sources (August 2025)
- [ ] Agent scheduling
You can find our full roadmap [here](https://github.com/orgs/arc53/projects/2). Please don't hesitate to contribute or create issues, it helps us improve DocsGPT!
@@ -62,51 +70,53 @@ We're eager to provide personalized assistance when deploying your DocsGPT to a
[Send Email :email:](mailto:support@docsgpt.cloud?subject=DocsGPT%20support%2Fsolutions)
## Join the Lighthouse Program 🌟
Calling all developers and GenAI innovators! The **DocsGPT Lighthouse Program** connects technical leaders actively deploying or extending DocsGPT in real-world scenarios. Collaborate directly with our team to shape the roadmap, access priority support, and build enterprise-ready solutions with exclusive community insights.
[Learn More & Apply →](https://docs.google.com/forms/d/1KAADiJinUJ8EMQyfTXUIGyFbqINNClNR3jBNWq7DgTE)
## QuickStart
> [!Note]
> Make sure you have [Docker](https://docs.docker.com/engine/install/) installed
A more detailed [Quickstart](https://docs.docsgpt.cloud/quickstart) is available in our documentation
1. Clone the repository and run the following command:
```bash
git clone https://github.com/arc53/DocsGPT.git
cd DocsGPT
```
On Mac OS or Linux, write:
2. Run the following command:
```bash
./setup.sh
```
It will install all the dependencies and allow you to download the local model, use OpenAI or use our LLM API.
Otherwise, refer to this Guide for Windows:
On windows:
2. Create a `.env` file in your root directory and set the env variables.
It should look like this inside:
```
LLM_NAME=[docsgpt or openai or others]
API_KEY=[if LLM_NAME is openai]
```
See optional environment variables in the [/application/.env_sample](https://github.com/arc53/DocsGPT/blob/main/application/.env_sample) file.
3. Run the following command:
1. **Clone the repository:**
```bash
docker compose up --build
git clone https://github.com/arc53/DocsGPT.git
cd DocsGPT
```
4. Navigate to http://localhost:5173/.
To stop, just run `Ctrl + C`.
**For macOS and Linux:**
2. **Run the setup script:**
```bash
./setup.sh
```
**For Windows:**
2. **Run the PowerShell setup script:**
```powershell
PowerShell -ExecutionPolicy Bypass -File .\setup.ps1
```
Either script will guide you through setting up DocsGPT. Four options available: using the public API, running locally, connecting to a local inference engine, or using a cloud API provider. Scripts will automatically configure your `.env` file and handle necessary downloads and installations based on your chosen option.
**Navigate to http://localhost:5173/**
To stop DocsGPT, open a terminal in the `DocsGPT` directory and run:
```bash
docker compose -f deployment/docker-compose.yaml down
```
(or use the specific `docker compose down` command shown after running the setup script).
> [!Note]
> For development environment setup instructions, please refer to the [Development Environment Guide](https://docs.docsgpt.cloud/Deploying/Development-Environment).
@@ -133,7 +143,6 @@ Please refer to the [CONTRIBUTING.md](CONTRIBUTING.md) file for information abou
We as members, contributors, and leaders, pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. Please refer to the [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) file for more information about contributing.
## Many Thanks To Our Contributors⚡
<a href="https://github.com/arc53/DocsGPT/graphs/contributors" alt="View Contributors">

View File

@@ -6,7 +6,6 @@ ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && \
apt-get install -y software-properties-common && \
add-apt-repository ppa:deadsnakes/ppa && \
# Install necessary packages and Python
apt-get update && \
apt-get install -y --no-install-recommends gcc wget unzip libc6-dev python3.12 python3.12-venv && \
rm -rf /var/lib/apt/lists/*
@@ -20,7 +19,7 @@ RUN if [ -f /usr/bin/python3.12 ]; then \
# Download and unzip the model
RUN wget https://d3dg1063dc54p9.cloudfront.net/models/embeddings/mpnet-base-v2.zip && \
unzip mpnet-base-v2.zip -d model && \
unzip mpnet-base-v2.zip -d models && \
rm mpnet-base-v2.zip
# Install Rust
@@ -49,7 +48,6 @@ FROM ubuntu:24.04 as final
RUN apt-get update && \
apt-get install -y software-properties-common && \
add-apt-repository ppa:deadsnakes/ppa && \
# Install Python
apt-get update && apt-get install -y --no-install-recommends python3.12 && \
ln -s /usr/bin/python3.12 /usr/bin/python && \
rm -rf /var/lib/apt/lists/*
@@ -63,7 +61,8 @@ RUN groupadd -r appuser && \
# Copy the virtual environment and model from the builder stage
COPY --from=builder /venv /venv
COPY --from=builder /model /app/model
COPY --from=builder /models /app/models
# Copy your application code
COPY . /app/application
@@ -85,4 +84,4 @@ EXPOSE 7091
USER appuser
# Start Gunicorn
CMD ["gunicorn", "-w", "2", "--timeout", "120", "--bind", "0.0.0.0:7091", "application.wsgi:app"]
CMD ["gunicorn", "-w", "1", "--timeout", "120", "--bind", "0.0.0.0:7091", "--preload", "application.wsgi:app"]

View File

@@ -0,0 +1,16 @@
from application.agents.classic_agent import ClassicAgent
from application.agents.react_agent import ReActAgent
class AgentCreator:
agents = {
"classic": ClassicAgent,
"react": ReActAgent,
}
@classmethod
def create_agent(cls, type, *args, **kwargs):
agent_class = cls.agents.get(type.lower())
if not agent_class:
raise ValueError(f"No agent class found for type {type}")
return agent_class(*args, **kwargs)

400
application/agents/base.py Normal file
View File

@@ -0,0 +1,400 @@
import logging
import uuid
from abc import ABC, abstractmethod
from typing import Dict, Generator, List, Optional
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
logger = logging.getLogger(__name__)
class BaseAgent(ABC):
def __init__(
self,
endpoint: str,
llm_name: str,
gpt_model: str,
api_key: str,
user_api_key: Optional[str] = None,
prompt: str = "",
chat_history: Optional[List[Dict]] = None,
decoded_token: Optional[Dict] = None,
attachments: Optional[List[Dict]] = None,
json_schema: Optional[Dict] = None,
):
self.endpoint = endpoint
self.llm_name = llm_name
self.gpt_model = gpt_model
self.api_key = api_key
self.user_api_key = user_api_key
self.prompt = prompt
self.decoded_token = decoded_token or {}
self.user: str = decoded_token.get("sub")
self.tool_config: Dict = {}
self.tools: List[Dict] = []
self.tool_calls: List[Dict] = []
self.chat_history: List[Dict] = chat_history if chat_history is not None else []
self.llm = LLMCreator.create_llm(
llm_name,
api_key=api_key,
user_api_key=user_api_key,
decoded_token=decoded_token,
)
self.llm_handler = LLMHandlerCreator.create_handler(
llm_name if llm_name else "default"
)
self.attachments = attachments or []
self.json_schema = json_schema
@log_activity()
def gen(
self, query: str, retriever: BaseRetriever, log_context: LogContext = None
) -> Generator[Dict, None, None]:
yield from self._gen_inner(query, retriever, log_context)
@abstractmethod
def _gen_inner(
self, query: str, retriever: BaseRetriever, log_context: LogContext
) -> Generator[Dict, None, None]:
pass
def _get_tools(self, api_key: str = None) -> Dict[str, Dict]:
mongo = MongoDB.get_client()
db = mongo[settings.MONGO_DB_NAME]
agents_collection = db["agents"]
tools_collection = db["user_tools"]
agent_data = agents_collection.find_one({"key": api_key or self.user_api_key})
tool_ids = agent_data.get("tools", []) if agent_data else []
tools = (
tools_collection.find(
{"_id": {"$in": [ObjectId(tool_id) for tool_id in tool_ids]}}
)
if tool_ids
else []
)
tools = list(tools)
tools_by_id = {str(tool["_id"]): tool for tool in tools} if tools else {}
return tools_by_id
def _get_user_tools(self, user="local"):
mongo = MongoDB.get_client()
db = mongo[settings.MONGO_DB_NAME]
user_tools_collection = db["user_tools"]
user_tools = user_tools_collection.find({"user": user, "status": True})
user_tools = list(user_tools)
return {str(i): tool for i, tool in enumerate(user_tools)}
def _build_tool_parameters(self, action):
params = {"type": "object", "properties": {}, "required": []}
for param_type in ["query_params", "headers", "body", "parameters"]:
if param_type in action and action[param_type].get("properties"):
for k, v in action[param_type]["properties"].items():
if v.get("filled_by_llm", True):
params["properties"][k] = {
key: value
for key, value in v.items()
if key != "filled_by_llm" and key != "value"
}
params["required"].append(k)
return params
def _prepare_tools(self, tools_dict):
self.tools = [
{
"type": "function",
"function": {
"name": f"{action['name']}_{tool_id}",
"description": action["description"],
"parameters": self._build_tool_parameters(action),
},
}
for tool_id, tool in tools_dict.items()
if (
(tool["name"] == "api_tool" and "actions" in tool.get("config", {}))
or (tool["name"] != "api_tool" and "actions" in tool)
)
for action in (
tool["config"]["actions"].values()
if tool["name"] == "api_tool"
else tool["actions"]
)
if action.get("active", True)
]
def _execute_tool_action(self, tools_dict, call):
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())
# Check if parsing failed
if tool_id is None or action_name is None:
error_message = f"Error: Failed to parse LLM tool call. Tool name: {getattr(call, 'name', 'unknown')}"
logger.error(error_message)
tool_call_data = {
"tool_name": "unknown",
"call_id": call_id,
"action_name": getattr(call, 'name', 'unknown'),
"arguments": call_args or {},
"result": f"Failed to parse tool call. Invalid tool name format: {getattr(call, 'name', 'unknown')}",
}
yield {"type": "tool_call", "data": {**tool_call_data, "status": "error"}}
self.tool_calls.append(tool_call_data)
return "Failed to parse tool call.", call_id
# Check if tool_id exists in available tools
if tool_id not in tools_dict:
error_message = f"Error: Tool ID '{tool_id}' extracted from LLM call not found in available tools_dict. Available IDs: {list(tools_dict.keys())}"
logger.error(error_message)
# Return error result
tool_call_data = {
"tool_name": "unknown",
"call_id": call_id,
"action_name": f"{action_name}_{tool_id}",
"arguments": call_args,
"result": f"Tool with ID {tool_id} not found. Available tools: {list(tools_dict.keys())}",
}
yield {"type": "tool_call", "data": {**tool_call_data, "status": "error"}}
self.tool_calls.append(tool_call_data)
return f"Tool with ID {tool_id} not found.", call_id
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]
if tool_data["name"] == "api_tool"
else next(
action
for action in tool_data["actions"]
if action["name"] == action_name
)
)
query_params, headers, body, parameters = {}, {}, {}, {}
param_types = {
"query_params": query_params,
"headers": headers,
"body": body,
"parameters": parameters,
}
for param_type, target_dict in param_types.items():
if param_type in action_data and action_data[param_type].get("properties"):
for param, details in action_data[param_type]["properties"].items():
if param not in call_args and "value" in details:
target_dict[param] = details["value"]
for param, value in call_args.items():
for param_type, target_dict in param_types.items():
if param_type in action_data and param in action_data[param_type].get(
"properties", {}
):
target_dict[param] = value
tm = ToolManager(config={})
tool = tm.load_tool(
tool_data["name"],
tool_config=(
{
"url": tool_data["config"]["actions"][action_name]["url"],
"method": tool_data["config"]["actions"][action_name]["method"],
"headers": headers,
"query_params": query_params,
}
if tool_data["name"] == "api_tool"
else tool_data["config"]
),
)
if tool_data["name"] == "api_tool":
print(
f"Executing api: {action_name} with query_params: {query_params}, headers: {headers}, body: {body}"
)
result = tool.execute_action(action_name, **body)
else:
print(f"Executing tool: {action_name} with args: {call_args}")
result = tool.execute_action(action_name, **parameters)
tool_call_data["result"] = (
f"{str(result)[:50]}..." if len(str(result)) > 50 else 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,
query: str,
retrieved_data: List[Dict],
) -> List[Dict]:
docs_together = "\n".join([doc["text"] for doc in retrieved_data])
p_chat_combine = system_prompt.replace("{summaries}", docs_together)
messages_combine = [{"role": "system", "content": p_chat_combine}]
for i in self.chat_history:
if "prompt" in i and "response" in i:
messages_combine.append({"role": "user", "content": i["prompt"]})
messages_combine.append({"role": "assistant", "content": i["response"]})
if "tool_calls" in i:
for tool_call in i["tool_calls"]:
call_id = tool_call.get("call_id") or str(uuid.uuid4())
function_call_dict = {
"function_call": {
"name": tool_call.get("action_name"),
"args": tool_call.get("arguments"),
"call_id": call_id,
}
}
function_response_dict = {
"function_response": {
"name": tool_call.get("action_name"),
"response": {"result": tool_call.get("result")},
"call_id": call_id,
}
}
messages_combine.append(
{"role": "assistant", "content": [function_call_dict]}
)
messages_combine.append(
{"role": "tool", "content": [function_response_dict]}
)
messages_combine.append({"role": "user", "content": query})
return messages_combine
def _retriever_search(
self,
retriever: BaseRetriever,
query: str,
log_context: Optional[LogContext] = None,
) -> List[Dict]:
retrieved_data = retriever.search(query)
if log_context:
data = build_stack_data(retriever, exclude_attributes=["llm"])
log_context.stacks.append({"component": "retriever", "data": data})
return retrieved_data
def _llm_gen(self, messages: List[Dict], log_context: Optional[LogContext] = None):
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
if (
self.json_schema
and hasattr(self.llm, "_supports_structured_output")
and self.llm._supports_structured_output()
):
structured_format = self.llm.prepare_structured_output_format(
self.json_schema
)
if structured_format:
if self.llm_name == "openai":
gen_kwargs["response_format"] = structured_format
elif self.llm_name == "google":
gen_kwargs["response_schema"] = structured_format
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})
return resp
def _llm_handler(
self,
resp,
tools_dict: Dict,
messages: List[Dict],
log_context: Optional[LogContext] = None,
attachments: Optional[List[Dict]] = None,
):
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):
is_structured_output = (
self.json_schema is not None
and hasattr(self.llm, "_supports_structured_output")
and self.llm._supports_structured_output()
)
if isinstance(response, str):
answer_data = {"answer": response}
if is_structured_output:
answer_data["structured"] = True
answer_data["schema"] = self.json_schema
yield answer_data
return
if hasattr(response, "message") and getattr(response.message, "content", None):
answer_data = {"answer": response.message.content}
if is_structured_output:
answer_data["structured"] = True
answer_data["schema"] = self.json_schema
yield answer_data
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):
answer_data = {"answer": event}
if is_structured_output:
answer_data["structured"] = True
answer_data["schema"] = self.json_schema
yield answer_data
elif hasattr(event, "message") and getattr(event.message, "content", None):
answer_data = {"answer": event.message.content}
if is_structured_output:
answer_data["structured"] = True
answer_data["schema"] = self.json_schema
yield answer_data
elif isinstance(event, dict) and "type" in event:
yield event

View File

@@ -0,0 +1,53 @@
from typing import Dict, Generator
from application.agents.base import BaseAgent
from application.logging import LogContext
from application.retriever.base import BaseRetriever
import logging
logger = logging.getLogger(__name__)
class ClassicAgent(BaseAgent):
"""A simplified 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)
# 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)
# Step 4: Handle the response
yield from self._handle_response(
llm_response, tools_dict, messages, log_context
)
# 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()}}
)

View File

@@ -0,0 +1,229 @@
import os
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_template = f.read()
with open(
os.path.join(current_dir, "application/prompts", "react_final_prompt.txt"),
"r",
) as f:
final_prompt_template = f.read()
MAX_ITERATIONS_REASONING = 10
class ReActAgent(BaseAgent):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
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:
tools_dict = self._get_tools(self.user_api_key)
else:
tools_dict = self._get_user_tools(self.user)
self._prepare_tools(tools_dict)
docs_together = "\n".join([doc["text"] for doc in retrieved_data])
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}")
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. "
)
messages = self._build_messages(execution_prompt_str, query, retrieved_data)
resp_from_llm_gen = self._llm_gen(messages, log_context)
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)
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.")
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_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_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})
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
) -> Generator[str, None, None]:
observation_string = "\n".join(observations)
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_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})
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

@@ -1,7 +1,7 @@
import json
import requests
from application.tools.base import Tool
from application.agents.tools.base import Tool
class APITool(Tool):
@@ -25,16 +25,34 @@ class APITool(Tool):
def _make_api_call(self, url, method, headers, query_params, body):
if query_params:
url = f"{url}?{requests.compat.urlencode(query_params)}"
if isinstance(body, dict):
body = json.dumps(body)
# if isinstance(body, dict):
# body = json.dumps(body)
try:
print(f"Making API call: {method} {url} with body: {body}")
if body == "{}":
body = None
response = requests.request(method, url, headers=headers, data=body)
response.raise_for_status()
try:
data = response.json()
except ValueError:
content_type = response.headers.get(
"Content-Type", "application/json"
).lower()
if "application/json" in content_type:
try:
data = response.json()
except json.JSONDecodeError as e:
print(f"Error decoding JSON: {e}. Raw response: {response.text}")
return {
"status_code": response.status_code,
"message": f"API call returned invalid JSON. Error: {e}",
"data": response.text,
}
elif "text/" in content_type or "application/xml" in content_type:
data = response.text
elif not response.content:
data = None
else:
print(f"Unsupported content type: {content_type}")
data = response.content
return {
"status_code": response.status_code,

View File

@@ -0,0 +1,182 @@
import requests
from application.agents.tools.base import Tool
class BraveSearchTool(Tool):
"""
Brave Search
A tool for performing web and image searches using the Brave Search API.
Requires an API key for authentication.
"""
def __init__(self, config):
self.config = config
self.token = config.get("token", "")
self.base_url = "https://api.search.brave.com/res/v1"
def execute_action(self, action_name, **kwargs):
actions = {
"brave_web_search": self._web_search,
"brave_image_search": self._image_search,
}
if action_name in actions:
return actions[action_name](**kwargs)
else:
raise ValueError(f"Unknown action: {action_name}")
def _web_search(
self,
query,
country="ALL",
search_lang="en",
count=10,
offset=0,
safesearch="off",
freshness=None,
result_filter=None,
extra_snippets=False,
summary=False,
):
"""
Performs a web search using the Brave Search API.
"""
print(f"Performing Brave web search for: {query}")
url = f"{self.base_url}/web/search"
params = {
"q": query,
"country": country,
"search_lang": search_lang,
"count": min(count, 20),
"offset": min(offset, 9),
"safesearch": safesearch,
}
if freshness:
params["freshness"] = freshness
if result_filter:
params["result_filter"] = result_filter
if extra_snippets:
params["extra_snippets"] = 1
if summary:
params["summary"] = 1
headers = {
"Accept": "application/json",
"Accept-Encoding": "gzip",
"X-Subscription-Token": self.token,
}
response = requests.get(url, params=params, headers=headers)
if response.status_code == 200:
return {
"status_code": response.status_code,
"results": response.json(),
"message": "Search completed successfully.",
}
else:
return {
"status_code": response.status_code,
"message": f"Search failed with status code: {response.status_code}.",
}
def _image_search(
self,
query,
country="ALL",
search_lang="en",
count=5,
safesearch="off",
spellcheck=False,
):
"""
Performs an image search using the Brave Search API.
"""
print(f"Performing Brave image search for: {query}")
url = f"{self.base_url}/images/search"
params = {
"q": query,
"country": country,
"search_lang": search_lang,
"count": min(count, 100), # API max is 100
"safesearch": safesearch,
"spellcheck": 1 if spellcheck else 0,
}
headers = {
"Accept": "application/json",
"Accept-Encoding": "gzip",
"X-Subscription-Token": self.token,
}
response = requests.get(url, params=params, headers=headers)
if response.status_code == 200:
return {
"status_code": response.status_code,
"results": response.json(),
"message": "Image search completed successfully.",
}
else:
return {
"status_code": response.status_code,
"message": f"Image search failed with status code: {response.status_code}.",
}
def get_actions_metadata(self):
return [
{
"name": "brave_web_search",
"description": "Perform a web search using Brave Search",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query (max 400 characters, 50 words)",
},
"search_lang": {
"type": "string",
"description": "The search language preference (default: en)",
},
"freshness": {
"type": "string",
"description": "Time filter for results (pd: last 24h, pw: last week, pm: last month, py: last year)",
},
},
"required": ["query"],
"additionalProperties": False,
},
},
{
"name": "brave_image_search",
"description": "Perform an image search using Brave Search",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query (max 400 characters, 50 words)",
},
"count": {
"type": "integer",
"description": "Number of results to return (max 100, default: 5)",
},
},
"required": ["query"],
"additionalProperties": False,
},
},
]
def get_config_requirements(self):
return {
"token": {
"type": "string",
"description": "Brave Search API key for authentication",
},
}

View File

@@ -1,5 +1,5 @@
import requests
from application.tools.base import Tool
from application.agents.tools.base import Tool
class CryptoPriceTool(Tool):
@@ -31,7 +31,6 @@ class CryptoPriceTool(Tool):
response = requests.get(url)
if response.status_code == 200:
data = response.json()
# data will be like {"USD": <price>} if the call is successful
if currency.upper() in data:
return {
"status_code": response.status_code,

View File

@@ -0,0 +1,114 @@
from application.agents.tools.base import Tool
from duckduckgo_search import DDGS
class DuckDuckGoSearchTool(Tool):
"""
DuckDuckGo Search
A tool for performing web and image searches using DuckDuckGo.
"""
def __init__(self, config):
self.config = config
def execute_action(self, action_name, **kwargs):
actions = {
"ddg_web_search": self._web_search,
"ddg_image_search": self._image_search,
}
if action_name in actions:
return actions[action_name](**kwargs)
else:
raise ValueError(f"Unknown action: {action_name}")
def _web_search(
self,
query,
max_results=5,
):
print(f"Performing DuckDuckGo web search for: {query}")
try:
results = DDGS().text(
query,
max_results=max_results,
)
return {
"status_code": 200,
"results": results,
"message": "Web search completed successfully.",
}
except Exception as e:
return {
"status_code": 500,
"message": f"Web search failed: {str(e)}",
}
def _image_search(
self,
query,
max_results=5,
):
print(f"Performing DuckDuckGo image search for: {query}")
try:
results = DDGS().images(
keywords=query,
max_results=max_results,
)
return {
"status_code": 200,
"results": results,
"message": "Image search completed successfully.",
}
except Exception as e:
return {
"status_code": 500,
"message": f"Image search failed: {str(e)}",
}
def get_actions_metadata(self):
return [
{
"name": "ddg_web_search",
"description": "Perform a web search using DuckDuckGo.",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query",
},
"max_results": {
"type": "integer",
"description": "Number of results to return (default: 5)",
},
},
"required": ["query"],
},
},
{
"name": "ddg_image_search",
"description": "Perform an image search using DuckDuckGo.",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query",
},
"max_results": {
"type": "integer",
"description": "Number of results to return (default: 5, max: 50)",
},
},
"required": ["query"],
},
},
]
def get_config_requirements(self):
return {}

View File

@@ -0,0 +1,127 @@
import requests
from application.agents.tools.base import Tool
class NtfyTool(Tool):
"""
Ntfy Tool
A tool for sending notifications to ntfy topics on a specified server.
"""
def __init__(self, config):
"""
Initialize the NtfyTool with configuration.
Args:
config (dict): Configuration dictionary containing the access token.
"""
self.config = config
self.token = config.get("token", "")
def execute_action(self, action_name, **kwargs):
"""
Execute the specified action with given parameters.
Args:
action_name (str): Name of the action to execute.
**kwargs: Parameters for the action, including server_url.
Returns:
dict: Result of the action with status code and message.
Raises:
ValueError: If the action name is unknown.
"""
actions = {
"ntfy_send_message": self._send_message,
}
if action_name in actions:
return actions[action_name](**kwargs)
else:
raise ValueError(f"Unknown action: {action_name}")
def _send_message(self, server_url, message, topic, title=None, priority=None):
"""
Send a message to an ntfy topic on the specified server.
Args:
server_url (str): Base URL of the ntfy server (e.g., https://ntfy.sh).
message (str): The message text to send.
topic (str): The topic to send the message to.
title (str, optional): Title of the notification.
priority (int, optional): Priority of the notification (1-5).
Returns:
dict: Response with status code and a confirmation message.
Raises:
ValueError: If priority is not an integer between 1 and 5.
"""
url = f"{server_url.rstrip('/')}/{topic}"
headers = {}
if title:
headers["X-Title"] = title
if priority:
try:
priority = int(priority)
except (ValueError, TypeError):
raise ValueError("Priority must be convertible to an integer")
if priority < 1 or priority > 5:
raise ValueError("Priority must be an integer between 1 and 5")
headers["X-Priority"] = str(priority)
if self.token:
headers["Authorization"] = f"Basic {self.token}"
data = message.encode("utf-8")
response = requests.post(url, headers=headers, data=data)
return {"status_code": response.status_code, "message": "Message sent"}
def get_actions_metadata(self):
"""
Provide metadata about available actions.
Returns:
list: List of dictionaries describing each action.
"""
return [
{
"name": "ntfy_send_message",
"description": "Send a notification to an ntfy topic",
"parameters": {
"type": "object",
"properties": {
"server_url": {
"type": "string",
"description": "Base URL of the ntfy server",
},
"message": {
"type": "string",
"description": "Text to send in the notification",
},
"topic": {
"type": "string",
"description": "Topic to send the notification to",
},
"title": {
"type": "string",
"description": "Title of the notification (optional)",
},
"priority": {
"type": "integer",
"description": "Priority of the notification (1-5, optional)",
},
},
"required": ["server_url", "message", "topic"],
"additionalProperties": False,
},
},
]
def get_config_requirements(self):
"""
Specify the configuration requirements.
Returns:
dict: Dictionary describing required config parameters.
"""
return {
"token": {"type": "string", "description": "Access token for authentication"},
}

View File

@@ -1,5 +1,5 @@
import psycopg2
from application.tools.base import Tool
from application.agents.tools.base import Tool
class PostgresTool(Tool):
"""

View File

@@ -0,0 +1,83 @@
import requests
from markdownify import markdownify
from application.agents.tools.base import Tool
from urllib.parse import urlparse
class ReadWebpageTool(Tool):
"""
Read Webpage (browser)
A tool to fetch the HTML content of a URL and convert it to Markdown.
"""
def __init__(self, config=None):
"""
Initializes the tool.
:param config: Optional configuration dictionary. Not used by this tool.
"""
self.config = config
def execute_action(self, action_name: str, **kwargs) -> str:
"""
Executes the specified action. For this tool, the only action is 'read_webpage'.
:param action_name: The name of the action to execute. Should be 'read_webpage'.
:param kwargs: Keyword arguments, must include 'url'.
:return: The Markdown content of the webpage or an error message.
"""
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."
# Ensure the URL has a scheme (if not, default to http)
parsed_url = urlparse(url)
if not parsed_url.scheme:
url = "http://" + url
try:
response = requests.get(url, timeout=10, headers={'User-Agent': 'DocsGPT-Agent/1.0'})
response.raise_for_status() # Raise an exception for HTTP errors (4xx or 5xx)
html_content = response.text
#soup = BeautifulSoup(html_content, 'html.parser')
markdown_content = markdownify(html_content, heading_style="ATX", newline_style="BACKSLASH")
return markdown_content
except requests.exceptions.RequestException as e:
return f"Error fetching URL {url}: {e}"
except Exception as e:
return f"Error processing URL {url}: {e}"
def get_actions_metadata(self):
"""
Returns metadata for the actions supported by this tool.
"""
return [
{
"name": "read_webpage",
"description": "Fetches the HTML content of a given URL and returns it as clean Markdown text. Input must be a valid URL.",
"parameters": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "The fully qualified URL of the webpage to read (e.g., 'https://www.example.com').",
}
},
"required": ["url"],
"additionalProperties": False,
},
}
]
def get_config_requirements(self):
"""
Returns a dictionary describing the configuration requirements for the tool.
This tool does not require any specific configuration.
"""
return {}

View File

@@ -1,5 +1,5 @@
import requests
from application.tools.base import Tool
from application.agents.tools.base import Tool
class TelegramTool(Tool):

View File

@@ -0,0 +1,61 @@
import json
import logging
logger = logging.getLogger(__name__)
class ToolActionParser:
def __init__(self, llm_type):
self.llm_type = llm_type
self.parsers = {
"OpenAILLM": self._parse_openai_llm,
"GoogleLLM": self._parse_google_llm,
}
def parse_args(self, call):
parser = self.parsers.get(self.llm_type, self._parse_openai_llm)
return parser(call)
def _parse_openai_llm(self, call):
try:
call_args = json.loads(call.arguments)
tool_parts = call.name.split("_")
# If the tool name doesn't contain an underscore, it's likely a hallucinated tool
if len(tool_parts) < 2:
logger.warning(f"Invalid tool name format: {call.name}. Expected format: action_name_tool_id")
return None, None, None
tool_id = tool_parts[-1]
action_name = "_".join(tool_parts[:-1])
# Validate that tool_id looks like a numerical ID
if not tool_id.isdigit():
logger.warning(f"Tool ID '{tool_id}' is not numerical. This might be a hallucinated tool call.")
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):
try:
call_args = call.arguments
tool_parts = call.name.split("_")
# If the tool name doesn't contain an underscore, it's likely a hallucinated tool
if len(tool_parts) < 2:
logger.warning(f"Invalid tool name format: {call.name}. Expected format: action_name_tool_id")
return None, None, None
tool_id = tool_parts[-1]
action_name = "_".join(tool_parts[:-1])
# Validate that tool_id looks like a numerical ID
if not tool_id.isdigit():
logger.warning(f"Tool ID '{tool_id}' is not numerical. This might be a hallucinated tool call.")
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

@@ -3,7 +3,7 @@ import inspect
import os
import pkgutil
from application.tools.base import Tool
from application.agents.tools.base import Tool
class ToolManager:
@@ -13,13 +13,11 @@ class ToolManager:
self.load_tools()
def load_tools(self):
tools_dir = os.path.join(os.path.dirname(__file__), "implementations")
tools_dir = os.path.join(os.path.dirname(__file__))
for finder, name, ispkg in pkgutil.iter_modules([tools_dir]):
if name == "base" or name.startswith("__"):
continue
module = importlib.import_module(
f"application.tools.implementations.{name}"
)
module = importlib.import_module(f"application.agents.tools.{name}")
for member_name, obj in inspect.getmembers(module, inspect.isclass):
if issubclass(obj, Tool) and obj is not Tool:
tool_config = self.config.get(name, {})
@@ -27,9 +25,7 @@ class ToolManager:
def load_tool(self, tool_name, tool_config):
self.config[tool_name] = tool_config
module = importlib.import_module(
f"application.tools.implementations.{tool_name}"
)
module = importlib.import_module(f"application.agents.tools.{tool_name}")
for member_name, obj in inspect.getmembers(module, inspect.isclass):
if issubclass(obj, Tool) and obj is not Tool:
return obj(tool_config)

View File

@@ -0,0 +1,7 @@
from flask_restx import Api
api = Api(
version="1.0",
title="DocsGPT API",
description="API for DocsGPT",
)

View File

@@ -0,0 +1,19 @@
from flask import Blueprint
from application.api import api
from application.api.answer.routes.answer import AnswerResource
from application.api.answer.routes.base import answer_ns
from application.api.answer.routes.stream import StreamResource
answer = Blueprint("answer", __name__)
api.add_namespace(answer_ns)
def init_answer_routes():
api.add_resource(StreamResource, "/stream")
api.add_resource(AnswerResource, "/api/answer")
init_answer_routes()

View File

@@ -1,646 +0,0 @@
import asyncio
import datetime
import json
import logging
import os
import sys
import traceback
from bson.dbref import DBRef
from bson.objectid import ObjectId
from flask import Blueprint, current_app, make_response, request, Response
from flask_restx import fields, Namespace, Resource
from application.core.mongo_db import MongoDB
from application.core.settings import settings
from application.error import bad_request
from application.extensions import api
from application.llm.llm_creator import LLMCreator
from application.retriever.retriever_creator import RetrieverCreator
from application.utils import check_required_fields, limit_chat_history
logger = logging.getLogger(__name__)
mongo = MongoDB.get_client()
db = mongo["docsgpt"]
conversations_collection = db["conversations"]
sources_collection = db["sources"]
prompts_collection = db["prompts"]
api_key_collection = db["api_keys"]
user_logs_collection = db["user_logs"]
answer = Blueprint("answer", __name__)
answer_ns = Namespace("answer", description="Answer related operations", path="/")
api.add_namespace(answer_ns)
gpt_model = ""
# to have some kind of default behaviour
if settings.LLM_NAME == "openai":
gpt_model = "gpt-4o-mini"
elif settings.LLM_NAME == "anthropic":
gpt_model = "claude-2"
elif settings.LLM_NAME == "groq":
gpt_model = "llama3-8b-8192"
if settings.MODEL_NAME: # in case there is particular model name configured
gpt_model = settings.MODEL_NAME
# load the prompts
current_dir = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
with open(os.path.join(current_dir, "prompts", "chat_combine_default.txt"), "r") as f:
chat_combine_template = f.read()
with open(os.path.join(current_dir, "prompts", "chat_reduce_prompt.txt"), "r") as f:
chat_reduce_template = f.read()
with open(os.path.join(current_dir, "prompts", "chat_combine_creative.txt"), "r") as f:
chat_combine_creative = f.read()
with open(os.path.join(current_dir, "prompts", "chat_combine_strict.txt"), "r") as f:
chat_combine_strict = f.read()
api_key_set = settings.API_KEY is not None
embeddings_key_set = settings.EMBEDDINGS_KEY is not None
async def async_generate(chain, question, chat_history):
result = await chain.arun({"question": question, "chat_history": chat_history})
return result
def run_async_chain(chain, question, chat_history):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
result = {}
try:
answer = loop.run_until_complete(async_generate(chain, question, chat_history))
finally:
loop.close()
result["answer"] = answer
return result
def get_data_from_api_key(api_key):
data = api_key_collection.find_one({"key": api_key})
# # Raise custom exception if the API key is not found
if data is None:
raise Exception("Invalid API Key, please generate new key", 401)
if "retriever" not in data:
data["retriever"] = None
if "source" in data and isinstance(data["source"], DBRef):
source_doc = db.dereference(data["source"])
data["source"] = str(source_doc["_id"])
if "retriever" in source_doc:
data["retriever"] = source_doc["retriever"]
else:
data["source"] = {}
return data
def get_retriever(source_id: str):
doc = sources_collection.find_one({"_id": ObjectId(source_id)})
if doc is None:
raise Exception("Source document does not exist", 404)
retriever_name = None if "retriever" not in doc else doc["retriever"]
return retriever_name
def is_azure_configured():
return (
settings.OPENAI_API_BASE
and settings.OPENAI_API_VERSION
and settings.AZURE_DEPLOYMENT_NAME
)
def save_conversation(conversation_id, question, response, source_log_docs, llm,index=None):
if conversation_id is not None and index is not None:
conversations_collection.update_one(
{"_id": ObjectId(conversation_id), f"queries.{index}": {"$exists": True}},
{
"$set": {
f"queries.{index}.prompt": question,
f"queries.{index}.response": response,
f"queries.{index}.sources": source_log_docs,
}
}
)
##remove following queries from the array
conversations_collection.update_one(
{"_id": ObjectId(conversation_id), f"queries.{index}": {"$exists": True}},
{
"$push":{
"queries":{
"$each":[],
"$slice":index+1
}
}
}
)
elif conversation_id is not None and conversation_id != "None":
conversations_collection.update_one(
{"_id": ObjectId(conversation_id)},
{
"$push": {
"queries": {
"prompt": question,
"response": response,
"sources": source_log_docs,
}
}
},
)
else:
# create new conversation
# generate summary
messages_summary = [
{
"role": "assistant",
"content": "Summarise following conversation in no more than 3 "
"words, respond ONLY with the summary, use the same "
"language as the system",
},
{
"role": "user",
"content": "Summarise following conversation in no more than 3 words, "
"respond ONLY with the summary, use the same language as the "
"system \n\nUser: "
+ question
+ "\n\n"
+ "AI: "
+ response,
},
]
completion = llm.gen(model=gpt_model, messages=messages_summary, max_tokens=30)
conversation_id = conversations_collection.insert_one(
{
"user": "local",
"date": datetime.datetime.utcnow(),
"name": completion,
"queries": [
{
"prompt": question,
"response": response,
"sources": source_log_docs,
}
],
}
).inserted_id
return conversation_id
def get_prompt(prompt_id):
if prompt_id == "default":
prompt = chat_combine_template
elif prompt_id == "creative":
prompt = chat_combine_creative
elif prompt_id == "strict":
prompt = chat_combine_strict
else:
prompt = prompts_collection.find_one({"_id": ObjectId(prompt_id)})["content"]
return prompt
def complete_stream(
question, retriever, conversation_id, user_api_key, isNoneDoc=False,index=None
):
try:
response_full = ""
source_log_docs = []
answer = retriever.gen()
sources = retriever.search()
for source in sources:
if "text" in source:
source["text"] = source["text"][:100].strip() + "..."
if len(sources) > 0:
data = json.dumps({"type": "source", "source": sources})
yield f"data: {data}\n\n"
for line in answer:
if "answer" in line:
response_full += str(line["answer"])
data = json.dumps(line)
yield f"data: {data}\n\n"
elif "source" in line:
source_log_docs.append(line["source"])
if isNoneDoc:
for doc in source_log_docs:
doc["source"] = "None"
llm = LLMCreator.create_llm(
settings.LLM_NAME, api_key=settings.API_KEY, user_api_key=user_api_key
)
if user_api_key is None:
conversation_id = save_conversation(
conversation_id, question, response_full, source_log_docs, llm,index
)
# send data.type = "end" to indicate that the stream has ended as json
data = json.dumps({"type": "id", "id": str(conversation_id)})
yield f"data: {data}\n\n"
retriever_params = retriever.get_params()
user_logs_collection.insert_one(
{
"action": "stream_answer",
"level": "info",
"user": "local",
"api_key": user_api_key,
"question": question,
"response": response_full,
"sources": source_log_docs,
"retriever_params": retriever_params,
"timestamp": datetime.datetime.now(datetime.timezone.utc),
}
)
data = json.dumps({"type": "end"})
yield f"data: {data}\n\n"
except Exception as e:
print("\033[91merr", str(e), file=sys.stderr)
traceback.print_exc()
data = json.dumps(
{
"type": "error",
"error": "Please try again later. We apologize for any inconvenience.",
"error_exception": str(e),
}
)
yield f"data: {data}\n\n"
return
@answer_ns.route("/stream")
class Stream(Resource):
stream_model = api.model(
"StreamModel",
{
"question": fields.String(
required=True, description="Question to be asked"
),
"history": fields.List(
fields.String, required=False, description="Chat history"
),
"conversation_id": fields.String(
required=False, description="Conversation ID"
),
"prompt_id": fields.String(
required=False, default="default", description="Prompt ID"
),
"chunks": fields.Integer(
required=False, default=2, description="Number of chunks"
),
"token_limit": fields.Integer(required=False, description="Token limit"),
"retriever": fields.String(required=False, description="Retriever type"),
"api_key": fields.String(required=False, description="API key"),
"active_docs": fields.String(
required=False, description="Active documents"
),
"isNoneDoc": fields.Boolean(
required=False, description="Flag indicating if no document is used"
),
"index":fields.Integer(
required=False, description="The position where query is to be updated"
),
},
)
@api.expect(stream_model)
@api.doc(description="Stream a response based on the question and retriever")
def post(self):
data = request.get_json()
required_fields = ["question"]
if "index" in data:
required_fields = ["question","conversation_id"]
missing_fields = check_required_fields(data, required_fields)
if missing_fields:
return missing_fields
try:
question = data["question"]
history = limit_chat_history(json.loads(data.get("history", [])), gpt_model=gpt_model)
conversation_id = data.get("conversation_id")
prompt_id = data.get("prompt_id", "default")
index=data.get("index",None)
chunks = int(data.get("chunks", 2))
token_limit = data.get("token_limit", settings.DEFAULT_MAX_HISTORY)
retriever_name = data.get("retriever", "classic")
if "api_key" in data:
data_key = get_data_from_api_key(data["api_key"])
chunks = int(data_key.get("chunks", 2))
prompt_id = data_key.get("prompt_id", "default")
source = {"active_docs": data_key.get("source")}
retriever_name = data_key.get("retriever", retriever_name)
user_api_key = data["api_key"]
elif "active_docs" in data:
source = {"active_docs": data["active_docs"]}
retriever_name = get_retriever(data["active_docs"]) or retriever_name
user_api_key = None
else:
source = {}
user_api_key = None
current_app.logger.info(
f"/stream - request_data: {data}, source: {source}",
extra={"data": json.dumps({"request_data": data, "source": source})},
)
prompt = get_prompt(prompt_id)
if "isNoneDoc" in data and data["isNoneDoc"] is True:
chunks = 0
retriever = RetrieverCreator.create_retriever(
retriever_name,
question=question,
source=source,
chat_history=history,
prompt=prompt,
chunks=chunks,
token_limit=token_limit,
gpt_model=gpt_model,
user_api_key=user_api_key,
)
return Response(
complete_stream(
question=question,
retriever=retriever,
conversation_id=conversation_id,
user_api_key=user_api_key,
isNoneDoc=data.get("isNoneDoc"),
index=index,
),
mimetype="text/event-stream",
)
except ValueError:
message = "Malformed request body"
print("\033[91merr", str(message), file=sys.stderr)
return Response(
error_stream_generate(message),
status=400,
mimetype="text/event-stream",
)
except Exception as e:
current_app.logger.error(
f"/stream - error: {str(e)} - traceback: {traceback.format_exc()}",
extra={"error": str(e), "traceback": traceback.format_exc()},
)
message = e.args[0]
status_code = 400
# Custom exceptions with two arguments, index 1 as status code
if len(e.args) >= 2:
status_code = e.args[1]
return Response(
error_stream_generate(message),
status=status_code,
mimetype="text/event-stream",
)
def error_stream_generate(err_response):
data = json.dumps({"type": "error", "error": err_response})
yield f"data: {data}\n\n"
@answer_ns.route("/api/answer")
class Answer(Resource):
answer_model = api.model(
"AnswerModel",
{
"question": fields.String(
required=True, description="The question to answer"
),
"history": fields.List(
fields.String, required=False, description="Conversation history"
),
"conversation_id": fields.String(
required=False, description="Conversation ID"
),
"prompt_id": fields.String(
required=False, default="default", description="Prompt ID"
),
"chunks": fields.Integer(
required=False, default=2, description="Number of chunks"
),
"token_limit": fields.Integer(required=False, description="Token limit"),
"retriever": fields.String(required=False, description="Retriever type"),
"api_key": fields.String(required=False, description="API key"),
"active_docs": fields.String(
required=False, description="Active documents"
),
"isNoneDoc": fields.Boolean(
required=False, description="Flag indicating if no document is used"
),
},
)
@api.expect(answer_model)
@api.doc(description="Provide an answer based on the question and retriever")
def post(self):
data = request.get_json()
required_fields = ["question"]
missing_fields = check_required_fields(data, required_fields)
if missing_fields:
return missing_fields
try:
question = data["question"]
history = limit_chat_history(json.loads(data.get("history", [])), gpt_model=gpt_model)
conversation_id = data.get("conversation_id")
prompt_id = data.get("prompt_id", "default")
chunks = int(data.get("chunks", 2))
token_limit = data.get("token_limit", settings.DEFAULT_MAX_HISTORY)
retriever_name = data.get("retriever", "classic")
if "api_key" in data:
data_key = get_data_from_api_key(data["api_key"])
chunks = int(data_key.get("chunks", 2))
prompt_id = data_key.get("prompt_id", "default")
source = {"active_docs": data_key.get("source")}
retriever_name = data_key.get("retriever", retriever_name)
user_api_key = data["api_key"]
elif "active_docs" in data:
source = {"active_docs": data["active_docs"]}
retriever_name = get_retriever(data["active_docs"]) or retriever_name
user_api_key = None
else:
source = {}
user_api_key = None
prompt = get_prompt(prompt_id)
current_app.logger.info(
f"/api/answer - request_data: {data}, source: {source}",
extra={"data": json.dumps({"request_data": data, "source": source})},
)
retriever = RetrieverCreator.create_retriever(
retriever_name,
question=question,
source=source,
chat_history=history,
prompt=prompt,
chunks=chunks,
token_limit=token_limit,
gpt_model=gpt_model,
user_api_key=user_api_key,
)
source_log_docs = []
response_full = ""
for line in retriever.gen():
if "source" in line:
source_log_docs.append(line["source"])
elif "answer" in line:
response_full += line["answer"]
if data.get("isNoneDoc"):
for doc in source_log_docs:
doc["source"] = "None"
llm = LLMCreator.create_llm(
settings.LLM_NAME, api_key=settings.API_KEY, user_api_key=user_api_key
)
result = {"answer": response_full, "sources": source_log_docs}
result["conversation_id"] = str(
save_conversation(
conversation_id, question, response_full, source_log_docs, llm
)
)
retriever_params = retriever.get_params()
user_logs_collection.insert_one(
{
"action": "api_answer",
"level": "info",
"user": "local",
"api_key": user_api_key,
"question": question,
"response": response_full,
"sources": source_log_docs,
"retriever_params": retriever_params,
"timestamp": datetime.datetime.now(datetime.timezone.utc),
}
)
except Exception as e:
current_app.logger.error(
f"/api/answer - error: {str(e)} - traceback: {traceback.format_exc()}",
extra={"error": str(e), "traceback": traceback.format_exc()},
)
return bad_request(500, str(e))
return make_response(result, 200)
@answer_ns.route("/api/search")
class Search(Resource):
search_model = api.model(
"SearchModel",
{
"question": fields.String(
required=True, description="The question to search"
),
"chunks": fields.Integer(
required=False, default=2, description="Number of chunks"
),
"api_key": fields.String(
required=False, description="API key for authentication"
),
"active_docs": fields.String(
required=False, description="Active documents for retrieval"
),
"retriever": fields.String(required=False, description="Retriever type"),
"token_limit": fields.Integer(
required=False, description="Limit for tokens"
),
"isNoneDoc": fields.Boolean(
required=False, description="Flag indicating if no document is used"
),
},
)
@api.expect(search_model)
@api.doc(
description="Search for relevant documents based on the question and retriever"
)
def post(self):
data = request.get_json()
required_fields = ["question"]
missing_fields = check_required_fields(data, required_fields)
if missing_fields:
return missing_fields
try:
question = data["question"]
chunks = int(data.get("chunks", 2))
token_limit = data.get("token_limit", settings.DEFAULT_MAX_HISTORY)
retriever_name = data.get("retriever", "classic")
if "api_key" in data:
data_key = get_data_from_api_key(data["api_key"])
chunks = int(data_key.get("chunks", 2))
source = {"active_docs": data_key.get("source")}
user_api_key = data["api_key"]
elif "active_docs" in data:
source = {"active_docs": data["active_docs"]}
user_api_key = None
else:
source = {}
user_api_key = None
current_app.logger.info(
f"/api/answer - request_data: {data}, source: {source}",
extra={"data": json.dumps({"request_data": data, "source": source})},
)
retriever = RetrieverCreator.create_retriever(
retriever_name,
question=question,
source=source,
chat_history=[],
prompt="default",
chunks=chunks,
token_limit=token_limit,
gpt_model=gpt_model,
user_api_key=user_api_key,
)
docs = retriever.search()
retriever_params = retriever.get_params()
user_logs_collection.insert_one(
{
"action": "api_search",
"level": "info",
"user": "local",
"api_key": user_api_key,
"question": question,
"sources": docs,
"retriever_params": retriever_params,
"timestamp": datetime.datetime.now(datetime.timezone.utc),
}
)
if data.get("isNoneDoc"):
for doc in docs:
doc["source"] = "None"
except Exception as e:
current_app.logger.error(
f"/api/search - error: {str(e)} - traceback: {traceback.format_exc()}",
extra={"error": str(e), "traceback": traceback.format_exc()},
)
return bad_request(500, str(e))
return make_response(docs, 200)

View File

@@ -0,0 +1,122 @@
import logging
import traceback
from flask import make_response, request
from flask_restx import fields, Resource
from application.api import api
from application.api.answer.routes.base import answer_ns, BaseAnswerResource
from application.api.answer.services.stream_processor import StreamProcessor
logger = logging.getLogger(__name__)
@answer_ns.route("/api/answer")
class AnswerResource(Resource, BaseAnswerResource):
def __init__(self, *args, **kwargs):
Resource.__init__(self, *args, **kwargs)
BaseAnswerResource.__init__(self)
answer_model = answer_ns.model(
"AnswerModel",
{
"question": fields.String(
required=True, description="Question to be asked"
),
"history": fields.List(
fields.String,
required=False,
description="Conversation history (only for new conversations)",
),
"conversation_id": fields.String(
required=False,
description="Existing conversation ID (loads history)",
),
"prompt_id": fields.String(
required=False, default="default", description="Prompt ID"
),
"chunks": fields.Integer(
required=False, default=2, description="Number of chunks"
),
"token_limit": fields.Integer(required=False, description="Token limit"),
"retriever": fields.String(required=False, description="Retriever type"),
"api_key": fields.String(required=False, description="API key"),
"active_docs": fields.String(
required=False, description="Active documents"
),
"isNoneDoc": fields.Boolean(
required=False, description="Flag indicating if no document is used"
),
"save_conversation": fields.Boolean(
required=False,
default=True,
description="Whether to save the conversation",
),
},
)
@api.expect(answer_model)
@api.doc(description="Provide a response based on the question and retriever")
def post(self):
data = request.get_json()
if error := self.validate_request(data):
return error
decoded_token = getattr(request, "decoded_token", None)
processor = StreamProcessor(data, decoded_token)
try:
processor.initialize()
if not processor.decoded_token:
return make_response({"error": "Unauthorized"}, 401)
agent = processor.create_agent()
retriever = processor.create_retriever()
stream = self.complete_stream(
question=data["question"],
agent=agent,
retriever=retriever,
conversation_id=processor.conversation_id,
user_api_key=processor.agent_config.get("user_api_key"),
decoded_token=processor.decoded_token,
isNoneDoc=data.get("isNoneDoc"),
index=None,
should_save_conversation=data.get("save_conversation", True),
)
stream_result = self.process_response_stream(stream)
if len(stream_result) == 7:
(
conversation_id,
response,
sources,
tool_calls,
thought,
error,
structured_info,
) = stream_result
else:
conversation_id, response, sources, tool_calls, thought, error = (
stream_result
)
structured_info = None
if error:
return make_response({"error": error}, 400)
result = {
"conversation_id": conversation_id,
"answer": response,
"sources": sources,
"tool_calls": tool_calls,
"thought": thought,
}
if structured_info:
result.update(structured_info)
except Exception as e:
logger.error(
f"/api/answer - error: {str(e)} - traceback: {traceback.format_exc()}",
extra={"error": str(e), "traceback": traceback.format_exc()},
)
return make_response({"error": str(e)}, 500)
return make_response(result, 200)

View File

@@ -0,0 +1,263 @@
import datetime
import json
import logging
from typing import Any, Dict, Generator, List, Optional
from flask import Response
from flask_restx import Namespace
from application.api.answer.services.conversation_service import ConversationService
from application.core.mongo_db import MongoDB
from application.core.settings import settings
from application.llm.llm_creator import LLMCreator
from application.utils import check_required_fields, get_gpt_model
logger = logging.getLogger(__name__)
answer_ns = Namespace("answer", description="Answer related operations", path="/")
class BaseAnswerResource:
"""Shared base class for answer endpoints"""
def __init__(self):
mongo = MongoDB.get_client()
db = mongo[settings.MONGO_DB_NAME]
self.user_logs_collection = db["user_logs"]
self.gpt_model = get_gpt_model()
self.conversation_service = ConversationService()
def validate_request(
self, data: Dict[str, Any], require_conversation_id: bool = False
) -> Optional[Response]:
"""Common request validation"""
required_fields = ["question"]
if require_conversation_id:
required_fields.append("conversation_id")
if missing_fields := check_required_fields(data, required_fields):
return missing_fields
return None
def complete_stream(
self,
question: str,
agent: Any,
retriever: Any,
conversation_id: Optional[str],
user_api_key: Optional[str],
decoded_token: Dict[str, Any],
isNoneDoc: bool = False,
index: Optional[int] = None,
should_save_conversation: bool = True,
attachment_ids: Optional[List[str]] = None,
agent_id: Optional[str] = None,
is_shared_usage: bool = False,
shared_token: Optional[str] = None,
) -> Generator[str, None, None]:
"""
Generator function that streams the complete conversation response.
Args:
question: The user's question
agent: The agent instance
retriever: The retriever instance
conversation_id: Existing conversation ID
user_api_key: User's API key if any
decoded_token: Decoded JWT token
isNoneDoc: Flag for document-less responses
index: Index of message to update
should_save_conversation: Whether to persist the conversation
attachment_ids: List of attachment IDs
agent_id: ID of agent used
is_shared_usage: Flag for shared agent usage
shared_token: Token for shared agent
Yields:
Server-sent event strings
"""
try:
response_full, thought, source_log_docs, tool_calls = "", "", [], []
is_structured = False
schema_info = None
structured_chunks = []
for line in agent.gen(query=question, retriever=retriever):
if "answer" in line:
response_full += str(line["answer"])
if line.get("structured"):
is_structured = True
schema_info = line.get("schema")
structured_chunks.append(line["answer"])
else:
data = json.dumps({"type": "answer", "answer": line["answer"]})
yield f"data: {data}\n\n"
elif "sources" in line:
truncated_sources = []
source_log_docs = line["sources"]
for source in line["sources"]:
truncated_source = source.copy()
if "text" in truncated_source:
truncated_source["text"] = (
truncated_source["text"][:100].strip() + "..."
)
truncated_sources.append(truncated_source)
if truncated_sources:
data = json.dumps(
{"type": "source", "source": truncated_sources}
)
yield f"data: {data}\n\n"
elif "tool_calls" in line:
tool_calls = line["tool_calls"]
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 is_structured and structured_chunks:
structured_data = {
"type": "structured_answer",
"answer": response_full,
"structured": True,
"schema": schema_info,
}
data = json.dumps(structured_data)
yield f"data: {data}\n\n"
if isNoneDoc:
for doc in source_log_docs:
doc["source"] = "None"
llm = LLMCreator.create_llm(
settings.LLM_PROVIDER,
api_key=settings.API_KEY,
user_api_key=user_api_key,
decoded_token=decoded_token,
)
if should_save_conversation:
conversation_id = self.conversation_service.save_conversation(
conversation_id,
question,
response_full,
thought,
source_log_docs,
tool_calls,
llm,
self.gpt_model,
decoded_token,
index=index,
api_key=user_api_key,
agent_id=agent_id,
is_shared_usage=is_shared_usage,
shared_token=shared_token,
attachment_ids=attachment_ids,
)
else:
conversation_id = None
id_data = {"type": "id", "id": str(conversation_id)}
data = json.dumps(id_data)
yield f"data: {data}\n\n"
retriever_params = retriever.get_params()
log_data = {
"action": "stream_answer",
"level": "info",
"user": decoded_token.get("sub"),
"api_key": user_api_key,
"question": question,
"response": response_full,
"sources": source_log_docs,
"retriever_params": retriever_params,
"attachments": attachment_ids,
"timestamp": datetime.datetime.now(datetime.timezone.utc),
}
if is_structured:
log_data["structured_output"] = True
if schema_info:
log_data["schema"] = schema_info
# clean up text fields to be no longer than 10000 characters
for key, value in log_data.items():
if isinstance(value, str) and len(value) > 10000:
log_data[key] = value[:10000]
self.user_logs_collection.insert_one(log_data)
# End of stream
data = json.dumps({"type": "end"})
yield f"data: {data}\n\n"
except Exception as e:
logger.error(f"Error in stream: {str(e)}", exc_info=True)
data = json.dumps(
{
"type": "error",
"error": "Please try again later. We apologize for any inconvenience.",
}
)
yield f"data: {data}\n\n"
return
def process_response_stream(self, stream):
"""Process the stream response for non-streaming endpoint"""
conversation_id = ""
response_full = ""
source_log_docs = []
tool_calls = []
thought = ""
stream_ended = False
is_structured = False
schema_info = None
for line in stream:
try:
event_data = line.replace("data: ", "").strip()
event = json.loads(event_data)
if event["type"] == "id":
conversation_id = event["id"]
elif event["type"] == "answer":
response_full += event["answer"]
elif event["type"] == "structured_answer":
response_full = event["answer"]
is_structured = True
schema_info = event.get("schema")
elif event["type"] == "source":
source_log_docs = event["source"]
elif event["type"] == "tool_calls":
tool_calls = event["tool_calls"]
elif event["type"] == "thought":
thought = event["thought"]
elif event["type"] == "error":
logger.error(f"Error from stream: {event['error']}")
return None, None, None, None, event["error"]
elif event["type"] == "end":
stream_ended = True
except (json.JSONDecodeError, KeyError) as e:
logger.warning(f"Error parsing stream event: {e}, line: {line}")
continue
if not stream_ended:
logger.error("Stream ended unexpectedly without an 'end' event.")
return None, None, None, None, "Stream ended unexpectedly"
result = (
conversation_id,
response_full,
source_log_docs,
tool_calls,
thought,
None,
)
if is_structured:
result = result + ({"structured": True, "schema": schema_info},)
return result
def error_stream_generate(self, err_response):
data = json.dumps({"type": "error", "error": err_response})
yield f"data: {data}\n\n"

View File

@@ -0,0 +1,117 @@
import logging
import traceback
from flask import request, Response
from flask_restx import fields, Resource
from application.api import api
from application.api.answer.routes.base import answer_ns, BaseAnswerResource
from application.api.answer.services.stream_processor import StreamProcessor
logger = logging.getLogger(__name__)
@answer_ns.route("/stream")
class StreamResource(Resource, BaseAnswerResource):
def __init__(self, *args, **kwargs):
Resource.__init__(self, *args, **kwargs)
BaseAnswerResource.__init__(self)
stream_model = answer_ns.model(
"StreamModel",
{
"question": fields.String(
required=True, description="Question to be asked"
),
"history": fields.List(
fields.String,
required=False,
description="Conversation history (only for new conversations)",
),
"conversation_id": fields.String(
required=False,
description="Existing conversation ID (loads history)",
),
"prompt_id": fields.String(
required=False, default="default", description="Prompt ID"
),
"chunks": fields.Integer(
required=False, default=2, description="Number of chunks"
),
"token_limit": fields.Integer(required=False, description="Token limit"),
"retriever": fields.String(required=False, description="Retriever type"),
"api_key": fields.String(required=False, description="API key"),
"active_docs": fields.String(
required=False, description="Active documents"
),
"isNoneDoc": fields.Boolean(
required=False, description="Flag indicating if no document is used"
),
"index": fields.Integer(
required=False, description="Index of the query to update"
),
"save_conversation": fields.Boolean(
required=False,
default=True,
description="Whether to save the conversation",
),
"attachments": fields.List(
fields.String, required=False, description="List of attachment IDs"
),
},
)
@api.expect(stream_model)
@api.doc(description="Stream a response based on the question and retriever")
def post(self):
data = request.get_json()
if error := self.validate_request(data, "index" in data):
return error
decoded_token = getattr(request, "decoded_token", None)
processor = StreamProcessor(data, decoded_token)
try:
processor.initialize()
agent = processor.create_agent()
retriever = processor.create_retriever()
return Response(
self.complete_stream(
question=data["question"],
agent=agent,
retriever=retriever,
conversation_id=processor.conversation_id,
user_api_key=processor.agent_config.get("user_api_key"),
decoded_token=processor.decoded_token,
isNoneDoc=data.get("isNoneDoc"),
index=data.get("index"),
should_save_conversation=data.get("save_conversation", True),
attachment_ids=data.get("attachments", []),
agent_id=data.get("agent_id"),
is_shared_usage=processor.is_shared_usage,
shared_token=processor.shared_token,
),
mimetype="text/event-stream",
)
except ValueError as e:
message = "Malformed request body"
logger.error(
f"/stream - error: {message} - specific error: {str(e)} - traceback: {traceback.format_exc()}",
extra={"error": str(e), "traceback": traceback.format_exc()},
)
return Response(
self.error_stream_generate(message),
status=400,
mimetype="text/event-stream",
)
except Exception as e:
logger.error(
f"/stream - error: {str(e)} - traceback: {traceback.format_exc()}",
extra={"error": str(e), "traceback": traceback.format_exc()},
)
return Response(
self.error_stream_generate("Unknown error occurred"),
status=400,
mimetype="text/event-stream",
)

View File

@@ -0,0 +1,180 @@
import logging
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional
from application.core.mongo_db import MongoDB
from application.core.settings import settings
from bson import ObjectId
logger = logging.getLogger(__name__)
class ConversationService:
def __init__(self):
mongo = MongoDB.get_client()
db = mongo[settings.MONGO_DB_NAME]
self.conversations_collection = db["conversations"]
self.agents_collection = db["agents"]
def get_conversation(
self, conversation_id: str, user_id: str
) -> Optional[Dict[str, Any]]:
"""Retrieve a conversation with proper access control"""
if not conversation_id or not user_id:
return None
try:
conversation = self.conversations_collection.find_one(
{
"_id": ObjectId(conversation_id),
"$or": [{"user": user_id}, {"shared_with": user_id}],
}
)
if not conversation:
logger.warning(
f"Conversation not found or unauthorized - ID: {conversation_id}, User: {user_id}"
)
return None
conversation["_id"] = str(conversation["_id"])
return conversation
except Exception as e:
logger.error(f"Error fetching conversation: {str(e)}", exc_info=True)
return None
def save_conversation(
self,
conversation_id: Optional[str],
question: str,
response: str,
thought: str,
sources: List[Dict[str, Any]],
tool_calls: List[Dict[str, Any]],
llm: Any,
gpt_model: str,
decoded_token: Dict[str, Any],
index: Optional[int] = None,
api_key: Optional[str] = None,
agent_id: Optional[str] = None,
is_shared_usage: bool = False,
shared_token: Optional[str] = None,
attachment_ids: Optional[List[str]] = None,
) -> str:
"""Save or update a conversation in the database"""
user_id = decoded_token.get("sub")
if not user_id:
raise ValueError("User ID not found in token")
current_time = datetime.now(timezone.utc)
# clean up in sources array such that we save max 1k characters for text part
for source in sources:
if "text" in source and isinstance(source["text"], str):
source["text"] = source["text"][:1000]
if conversation_id is not None and index is not None:
# Update existing conversation with new query
result = self.conversations_collection.update_one(
{
"_id": ObjectId(conversation_id),
"user": user_id,
f"queries.{index}": {"$exists": True},
},
{
"$set": {
f"queries.{index}.prompt": question,
f"queries.{index}.response": response,
f"queries.{index}.thought": thought,
f"queries.{index}.sources": sources,
f"queries.{index}.tool_calls": tool_calls,
f"queries.{index}.timestamp": current_time,
f"queries.{index}.attachments": attachment_ids,
}
},
)
if result.matched_count == 0:
raise ValueError("Conversation not found or unauthorized")
self.conversations_collection.update_one(
{
"_id": ObjectId(conversation_id),
"user": user_id,
f"queries.{index}": {"$exists": True},
},
{"$push": {"queries": {"$each": [], "$slice": index + 1}}},
)
return conversation_id
elif conversation_id:
# Append new message to existing conversation
result = self.conversations_collection.update_one(
{"_id": ObjectId(conversation_id), "user": user_id},
{
"$push": {
"queries": {
"prompt": question,
"response": response,
"thought": thought,
"sources": sources,
"tool_calls": tool_calls,
"timestamp": current_time,
"attachments": attachment_ids,
}
}
},
)
if result.matched_count == 0:
raise ValueError("Conversation not found or unauthorized")
return conversation_id
else:
# Create new conversation
messages_summary = [
{
"role": "assistant",
"content": "Summarise following conversation in no more than 3 "
"words, respond ONLY with the summary, use the same "
"language as the user query",
},
{
"role": "user",
"content": "Summarise following conversation in no more than 3 words, "
"respond ONLY with the summary, use the same language as the "
"user query \n\nUser: " + question + "\n\n" + "AI: " + response,
},
]
completion = llm.gen(
model=gpt_model, messages=messages_summary, max_tokens=30
)
conversation_data = {
"user": user_id,
"date": current_time,
"name": completion,
"queries": [
{
"prompt": question,
"response": response,
"thought": thought,
"sources": sources,
"tool_calls": tool_calls,
"timestamp": current_time,
"attachments": attachment_ids,
}
],
}
if api_key:
if agent_id:
conversation_data["agent_id"] = agent_id
if is_shared_usage:
conversation_data["is_shared_usage"] = is_shared_usage
conversation_data["shared_token"] = shared_token
agent = self.agents_collection.find_one({"key": api_key})
if agent:
conversation_data["api_key"] = agent["key"]
result = self.conversations_collection.insert_one(conversation_data)
return str(result.inserted_id)

View File

@@ -0,0 +1,277 @@
import datetime
import json
import logging
import os
from pathlib import Path
from typing import Any, Dict, Optional
from bson.dbref import DBRef
from bson.objectid import ObjectId
from application.agents.agent_creator import AgentCreator
from application.api.answer.services.conversation_service import ConversationService
from application.core.mongo_db import MongoDB
from application.core.settings import settings
from application.retriever.retriever_creator import RetrieverCreator
from application.utils import get_gpt_model, limit_chat_history
logger = logging.getLogger(__name__)
def get_prompt(prompt_id: str, prompts_collection=None) -> str:
"""
Get a prompt by preset name or MongoDB ID
"""
current_dir = Path(__file__).resolve().parents[3]
prompts_dir = current_dir / "prompts"
preset_mapping = {
"default": "chat_combine_default.txt",
"creative": "chat_combine_creative.txt",
"strict": "chat_combine_strict.txt",
"reduce": "chat_reduce_prompt.txt",
}
if prompt_id in preset_mapping:
file_path = os.path.join(prompts_dir, preset_mapping[prompt_id])
try:
with open(file_path, "r") as f:
return f.read()
except FileNotFoundError:
raise FileNotFoundError(f"Prompt file not found: {file_path}")
try:
if prompts_collection is None:
mongo = MongoDB.get_client()
db = mongo[settings.MONGO_DB_NAME]
prompts_collection = db["prompts"]
prompt_doc = prompts_collection.find_one({"_id": ObjectId(prompt_id)})
if not prompt_doc:
raise ValueError(f"Prompt with ID {prompt_id} not found")
return prompt_doc["content"]
except Exception as e:
raise ValueError(f"Invalid prompt ID: {prompt_id}") from e
class StreamProcessor:
def __init__(
self, request_data: Dict[str, Any], decoded_token: Optional[Dict[str, Any]]
):
mongo = MongoDB.get_client()
self.db = mongo[settings.MONGO_DB_NAME]
self.agents_collection = self.db["agents"]
self.attachments_collection = self.db["attachments"]
self.prompts_collection = self.db["prompts"]
self.data = request_data
self.decoded_token = decoded_token
self.initial_user_id = (
self.decoded_token.get("sub") if self.decoded_token is not None else None
)
self.conversation_id = self.data.get("conversation_id")
self.source = (
{"active_docs": self.data["active_docs"]}
if "active_docs" in self.data
else {}
)
self.attachments = []
self.history = []
self.agent_config = {}
self.retriever_config = {}
self.is_shared_usage = False
self.shared_token = None
self.gpt_model = get_gpt_model()
self.conversation_service = ConversationService()
def initialize(self):
"""Initialize all required components for processing"""
self._configure_retriever()
self._configure_agent()
self._load_conversation_history()
self._process_attachments()
def _load_conversation_history(self):
"""Load conversation history either from DB or request"""
if self.conversation_id and self.initial_user_id:
conversation = self.conversation_service.get_conversation(
self.conversation_id, self.initial_user_id
)
if not conversation:
raise ValueError("Conversation not found or unauthorized")
self.history = [
{"prompt": query["prompt"], "response": query["response"]}
for query in conversation.get("queries", [])
]
else:
self.history = limit_chat_history(
json.loads(self.data.get("history", "[]")), gpt_model=self.gpt_model
)
def _process_attachments(self):
"""Process any attachments in the request"""
attachment_ids = self.data.get("attachments", [])
self.attachments = self._get_attachments_content(
attachment_ids, self.initial_user_id
)
def _get_attachments_content(self, attachment_ids, user_id):
"""
Retrieve content from attachment documents based on their IDs.
"""
if not attachment_ids:
return []
attachments = []
for attachment_id in attachment_ids:
try:
attachment_doc = self.attachments_collection.find_one(
{"_id": ObjectId(attachment_id), "user": user_id}
)
if attachment_doc:
attachments.append(attachment_doc)
except Exception as e:
logger.error(
f"Error retrieving attachment {attachment_id}: {e}", exc_info=True
)
return attachments
def _get_agent_key(self, agent_id: Optional[str], user_id: Optional[str]) -> tuple:
"""Get API key for agent with access control"""
if not agent_id:
return None, False, None
try:
agent = self.agents_collection.find_one({"_id": ObjectId(agent_id)})
if agent is None:
raise Exception("Agent not found")
is_owner = agent.get("user") == user_id
is_shared_with_user = agent.get(
"shared_publicly", False
) or user_id in agent.get("shared_with", [])
if not (is_owner or is_shared_with_user):
raise Exception("Unauthorized access to the agent")
if is_owner:
self.agents_collection.update_one(
{"_id": ObjectId(agent_id)},
{
"$set": {
"lastUsedAt": datetime.datetime.now(datetime.timezone.utc)
}
},
)
return str(agent["key"]), not is_owner, agent.get("shared_token")
except Exception as e:
logger.error(f"Error in get_agent_key: {str(e)}", exc_info=True)
raise
def _get_data_from_api_key(self, api_key: str) -> Dict[str, Any]:
data = self.agents_collection.find_one({"key": api_key})
if not data:
raise Exception("Invalid API Key, please generate a new key", 401)
source = data.get("source")
if isinstance(source, DBRef):
source_doc = self.db.dereference(source)
data["source"] = str(source_doc["_id"])
data["retriever"] = source_doc.get("retriever", data.get("retriever"))
data["chunks"] = source_doc.get("chunks", data.get("chunks"))
else:
data["source"] = None
return data
def _configure_agent(self):
"""Configure the agent based on request data"""
agent_id = self.data.get("agent_id")
self.agent_key, self.is_shared_usage, self.shared_token = self._get_agent_key(
agent_id, self.initial_user_id
)
api_key = self.data.get("api_key")
if api_key:
data_key = self._get_data_from_api_key(api_key)
self.agent_config.update(
{
"prompt_id": data_key.get("prompt_id", "default"),
"agent_type": data_key.get("agent_type", settings.AGENT_NAME),
"user_api_key": api_key,
"json_schema": data_key.get("json_schema"),
}
)
self.initial_user_id = data_key.get("user")
self.decoded_token = {"sub": data_key.get("user")}
if data_key.get("source"):
self.source = {"active_docs": data_key["source"]}
if data_key.get("retriever"):
self.retriever_config["retriever_name"] = data_key["retriever"]
if data_key.get("chunks") is not None:
self.retriever_config["chunks"] = data_key["chunks"]
elif self.agent_key:
data_key = self._get_data_from_api_key(self.agent_key)
self.agent_config.update(
{
"prompt_id": data_key.get("prompt_id", "default"),
"agent_type": data_key.get("agent_type", settings.AGENT_NAME),
"user_api_key": self.agent_key,
"json_schema": data_key.get("json_schema"),
}
)
self.decoded_token = (
self.decoded_token
if self.is_shared_usage
else {"sub": data_key.get("user")}
)
if data_key.get("source"):
self.source = {"active_docs": data_key["source"]}
if data_key.get("retriever"):
self.retriever_config["retriever_name"] = data_key["retriever"]
if data_key.get("chunks") is not None:
self.retriever_config["chunks"] = data_key["chunks"]
else:
self.agent_config.update(
{
"prompt_id": self.data.get("prompt_id", "default"),
"agent_type": settings.AGENT_NAME,
"user_api_key": None,
"json_schema": None,
}
)
def _configure_retriever(self):
"""Configure the retriever based on request data"""
self.retriever_config = {
"retriever_name": self.data.get("retriever", "classic"),
"chunks": int(self.data.get("chunks", 2)),
"token_limit": self.data.get("token_limit", settings.DEFAULT_MAX_HISTORY),
}
if "isNoneDoc" in self.data and self.data["isNoneDoc"]:
self.retriever_config["chunks"] = 0
def create_agent(self):
"""Create and return the configured agent"""
return AgentCreator.create_agent(
self.agent_config["agent_type"],
endpoint="stream",
llm_name=settings.LLM_PROVIDER,
gpt_model=self.gpt_model,
api_key=settings.API_KEY,
user_api_key=self.agent_config["user_api_key"],
prompt=get_prompt(self.agent_config["prompt_id"], self.prompts_collection),
chat_history=self.history,
decoded_token=self.decoded_token,
attachments=self.attachments,
json_schema=self.agent_config.get("json_schema"),
)
def create_retriever(self):
"""Create and return the configured retriever"""
return RetrieverCreator.create_retriever(
self.retriever_config["retriever_name"],
source=self.source,
chat_history=self.history,
prompt=get_prompt(self.agent_config["prompt_id"], self.prompts_collection),
chunks=self.retriever_config["chunks"],
token_limit=self.retriever_config["token_limit"],
gpt_model=self.gpt_model,
user_api_key=self.agent_config["user_api_key"],
decoded_token=self.decoded_token,
)

View File

@@ -0,0 +1,627 @@
import datetime
import json
import logging
from bson.objectid import ObjectId
from flask import (
Blueprint,
current_app,
jsonify,
make_response,
request
)
from flask_restx import fields, Namespace, Resource
from application.api.user.tasks import (
ingest_connector_task,
)
from application.core.mongo_db import MongoDB
from application.core.settings import settings
from application.api import api
from application.utils import (
check_required_fields
)
from application.parser.connectors.connector_creator import ConnectorCreator
mongo = MongoDB.get_client()
db = mongo[settings.MONGO_DB_NAME]
sources_collection = db["sources"]
sessions_collection = db["connector_sessions"]
connector = Blueprint("connector", __name__)
connectors_ns = Namespace("connectors", description="Connector operations", path="/")
api.add_namespace(connectors_ns)
@connectors_ns.route("/api/connectors/upload")
class UploadConnector(Resource):
@api.expect(
api.model(
"ConnectorUploadModel",
{
"user": fields.String(required=True, description="User ID"),
"source": fields.String(
required=True, description="Source type (google_drive, github, etc.)"
),
"name": fields.String(required=True, description="Job name"),
"data": fields.String(required=True, description="Configuration data"),
"repo_url": fields.String(description="GitHub repository URL"),
},
)
)
@api.doc(
description="Uploads connector source for vectorization",
)
def post(self):
decoded_token = request.decoded_token
if not decoded_token:
return make_response(jsonify({"success": False}), 401)
data = request.form
required_fields = ["user", "source", "name", "data"]
missing_fields = check_required_fields(data, required_fields)
if missing_fields:
return missing_fields
try:
config = json.loads(data["data"])
source_data = None
sync_frequency = config.get("sync_frequency", "never")
if data["source"] == "github":
source_data = config.get("repo_url")
elif data["source"] in ["crawler", "url"]:
source_data = config.get("url")
elif data["source"] == "reddit":
source_data = config
elif data["source"] in ConnectorCreator.get_supported_connectors():
session_token = config.get("session_token")
if not session_token:
return make_response(jsonify({
"success": False,
"error": f"Missing session_token in {data['source']} configuration"
}), 400)
file_ids = config.get("file_ids", [])
if isinstance(file_ids, str):
file_ids = [id.strip() for id in file_ids.split(',') if id.strip()]
elif not isinstance(file_ids, list):
file_ids = []
folder_ids = config.get("folder_ids", [])
if isinstance(folder_ids, str):
folder_ids = [id.strip() for id in folder_ids.split(',') if id.strip()]
elif not isinstance(folder_ids, list):
folder_ids = []
config["file_ids"] = file_ids
config["folder_ids"] = folder_ids
task = ingest_connector_task.delay(
job_name=data["name"],
user=decoded_token.get("sub"),
source_type=data["source"],
session_token=session_token,
file_ids=file_ids,
folder_ids=folder_ids,
recursive=config.get("recursive", False),
retriever=config.get("retriever", "classic"),
sync_frequency=sync_frequency
)
return make_response(jsonify({"success": True, "task_id": task.id}), 200)
task = ingest_connector_task.delay(
source_data=source_data,
job_name=data["name"],
user=decoded_token.get("sub"),
loader=data["source"],
sync_frequency=sync_frequency
)
except Exception as err:
current_app.logger.error(
f"Error uploading connector source: {err}", exc_info=True
)
return make_response(jsonify({"success": False}), 400)
return make_response(jsonify({"success": True, "task_id": task.id}), 200)
@connectors_ns.route("/api/connectors/task_status")
class ConnectorTaskStatus(Resource):
task_status_model = api.model(
"ConnectorTaskStatusModel",
{"task_id": fields.String(required=True, description="Task ID")},
)
@api.expect(task_status_model)
@api.doc(description="Get connector task status")
def get(self):
task_id = request.args.get("task_id")
if not task_id:
return make_response(
jsonify({"success": False, "message": "Task ID is required"}), 400
)
try:
from application.celery_init import celery
task = celery.AsyncResult(task_id)
task_meta = task.info
print(f"Task status: {task.status}")
if not isinstance(
task_meta, (dict, list, str, int, float, bool, type(None))
):
task_meta = str(task_meta)
except Exception as err:
current_app.logger.error(f"Error getting task status: {err}", exc_info=True)
return make_response(jsonify({"success": False}), 400)
return make_response(jsonify({"status": task.status, "result": task_meta}), 200)
@connectors_ns.route("/api/connectors/sources")
class ConnectorSources(Resource):
@api.doc(description="Get connector sources")
def get(self):
decoded_token = request.decoded_token
if not decoded_token:
return make_response(jsonify({"success": False}), 401)
user = decoded_token.get("sub")
try:
sources = sources_collection.find({"user": user, "type": "connector"}).sort("date", -1)
connector_sources = []
for source in sources:
connector_sources.append({
"id": str(source["_id"]),
"name": source.get("name"),
"date": source.get("date"),
"type": source.get("type"),
"source": source.get("source"),
"tokens": source.get("tokens", ""),
"retriever": source.get("retriever", "classic"),
"syncFrequency": source.get("sync_frequency", ""),
})
except Exception as err:
current_app.logger.error(f"Error retrieving connector sources: {err}", exc_info=True)
return make_response(jsonify({"success": False}), 400)
return make_response(jsonify(connector_sources), 200)
@connectors_ns.route("/api/connectors/delete")
class DeleteConnectorSource(Resource):
@api.doc(
description="Delete a connector source",
params={"source_id": "The source ID to delete"},
)
def delete(self):
decoded_token = request.decoded_token
if not decoded_token:
return make_response(jsonify({"success": False}), 401)
source_id = request.args.get("source_id")
if not source_id:
return make_response(
jsonify({"success": False, "message": "source_id is required"}), 400
)
try:
result = sources_collection.delete_one(
{"_id": ObjectId(source_id), "user": decoded_token.get("sub")}
)
if result.deleted_count == 0:
return make_response(
jsonify({"success": False, "message": "Source not found"}), 404
)
except Exception as err:
current_app.logger.error(
f"Error deleting connector source: {err}", exc_info=True
)
return make_response(jsonify({"success": False}), 400)
return make_response(jsonify({"success": True}), 200)
@connectors_ns.route("/api/connectors/auth")
class ConnectorAuth(Resource):
@api.doc(description="Get connector OAuth authorization URL", params={"provider": "Connector provider (e.g., google_drive)"})
def get(self):
try:
provider = request.args.get('provider') or request.args.get('source')
if not provider:
return make_response(jsonify({"success": False, "error": "Missing provider"}), 400)
if not ConnectorCreator.is_supported(provider):
return make_response(jsonify({"success": False, "error": f"Unsupported provider: {provider}"}), 400)
import uuid
state = str(uuid.uuid4())
auth = ConnectorCreator.create_auth(provider)
authorization_url = auth.get_authorization_url(state=state)
return make_response(jsonify({
"success": True,
"authorization_url": authorization_url,
"state": state
}), 200)
except Exception as e:
current_app.logger.error(f"Error generating connector auth URL: {e}")
return make_response(jsonify({"success": False, "error": str(e)}), 500)
@connectors_ns.route("/api/connectors/callback")
class ConnectorsCallback(Resource):
@api.doc(description="Handle OAuth callback for external connectors")
def get(self):
"""Handle OAuth callback for external connectors"""
try:
from application.parser.connectors.connector_creator import ConnectorCreator
from flask import request, redirect
import uuid
provider = request.args.get('provider', 'google_drive')
authorization_code = request.args.get('code')
_ = request.args.get('state')
error = request.args.get('error')
if error:
return redirect(f"/api/connectors/callback-status?status=error&message=OAuth+error:+{error}.+Please+try+again+and+make+sure+to+grant+all+requested+permissions,+including+offline+access.&provider={provider}")
if not authorization_code:
return redirect(f"/api/connectors/callback-status?status=error&message=Authorization+code+not+provided.+Please+complete+the+authorization+process+and+make+sure+to+grant+offline+access.&provider={provider}")
try:
auth = ConnectorCreator.create_auth(provider)
token_info = auth.exchange_code_for_tokens(authorization_code)
session_token = str(uuid.uuid4())
try:
credentials = auth.create_credentials_from_token_info(token_info)
service = auth.build_drive_service(credentials)
user_info = service.about().get(fields="user").execute()
user_email = user_info.get('user', {}).get('emailAddress', 'Connected User')
except Exception as e:
current_app.logger.warning(f"Could not get user info: {e}")
user_email = 'Connected User'
sanitized_token_info = {
"access_token": token_info.get("access_token"),
"refresh_token": token_info.get("refresh_token"),
"token_uri": token_info.get("token_uri"),
"expiry": token_info.get("expiry"),
"scopes": token_info.get("scopes")
}
user_id = request.decoded_token.get("sub") if getattr(request, "decoded_token", None) else None
sessions_collection.insert_one({
"session_token": session_token,
"user": user_id,
"token_info": sanitized_token_info,
"created_at": datetime.datetime.now(datetime.timezone.utc),
"user_email": user_email,
"provider": provider
})
# Redirect to success page with session token and user email
return redirect(f"/api/connectors/callback-status?status=success&message=Authentication+successful&provider={provider}&session_token={session_token}&user_email={user_email}")
except Exception as e:
current_app.logger.error(f"Error exchanging code for tokens: {str(e)}", exc_info=True)
return redirect(f"/api/connectors/callback-status?status=error&message=Failed+to+exchange+authorization+code+for+tokens:+{str(e)}&provider={provider}")
except Exception as e:
current_app.logger.error(f"Error handling connector callback: {e}")
return redirect(f"/api/connectors/callback-status?status=error&message=Failed+to+complete+connector+authentication:+{str(e)}.+Please+try+again+and+make+sure+to+grant+all+requested+permissions,+including+offline+access.")
@connectors_ns.route("/api/connectors/refresh")
class ConnectorRefresh(Resource):
@api.expect(api.model("ConnectorRefreshModel", {"provider": fields.String(required=True), "refresh_token": fields.String(required=True)}))
@api.doc(description="Refresh connector access token")
def post(self):
try:
data = request.get_json()
provider = data.get('provider')
refresh_token = data.get('refresh_token')
if not provider or not refresh_token:
return make_response(jsonify({"success": False, "error": "provider and refresh_token are required"}), 400)
auth = ConnectorCreator.create_auth(provider)
token_info = auth.refresh_access_token(refresh_token)
return make_response(jsonify({"success": True, "token_info": token_info}), 200)
except Exception as e:
current_app.logger.error(f"Error refreshing token for connector: {e}")
return make_response(jsonify({"success": False, "error": str(e)}), 500)
@connectors_ns.route("/api/connectors/files")
class ConnectorFiles(Resource):
@api.expect(api.model("ConnectorFilesModel", {"provider": fields.String(required=True), "session_token": fields.String(required=True), "folder_id": fields.String(required=False), "limit": fields.Integer(required=False), "page_token": fields.String(required=False)}))
@api.doc(description="List files from a connector provider (supports pagination)")
def post(self):
try:
data = request.get_json()
provider = data.get('provider')
session_token = data.get('session_token')
folder_id = data.get('folder_id')
limit = data.get('limit', 10)
page_token = data.get('page_token')
if not provider or not session_token:
return make_response(jsonify({"success": False, "error": "provider and session_token are required"}), 400)
decoded_token = request.decoded_token
if not decoded_token:
return make_response(jsonify({"success": False, "error": "Unauthorized"}), 401)
user = decoded_token.get('sub')
session = sessions_collection.find_one({"session_token": session_token, "user": user})
if not session:
return make_response(jsonify({"success": False, "error": "Invalid or unauthorized session"}), 401)
loader = ConnectorCreator.create_connector(provider, session_token)
documents = loader.load_data({
'limit': limit,
'list_only': True,
'session_token': session_token,
'folder_id': folder_id,
'page_token': page_token
})
files = []
for doc in documents[:limit]:
metadata = doc.extra_info
modified_time = metadata.get('modified_time')
if modified_time:
date_part = modified_time.split('T')[0]
time_part = modified_time.split('T')[1].split('.')[0].split('Z')[0]
formatted_time = f"{date_part} {time_part}"
else:
formatted_time = None
files.append({
'id': doc.doc_id,
'name': metadata.get('file_name', 'Unknown File'),
'type': metadata.get('mime_type', 'unknown'),
'size': metadata.get('size', None),
'modifiedTime': formatted_time
})
next_token = getattr(loader, 'next_page_token', None)
has_more = bool(next_token)
return make_response(jsonify({"success": True, "files": files, "total": len(files), "next_page_token": next_token, "has_more": has_more}), 200)
except Exception as e:
current_app.logger.error(f"Error loading connector files: {e}")
return make_response(jsonify({"success": False, "error": f"Failed to load files: {str(e)}"}), 500)
@connectors_ns.route("/api/connectors/validate-session")
class ConnectorValidateSession(Resource):
@api.expect(api.model("ConnectorValidateSessionModel", {"provider": fields.String(required=True), "session_token": fields.String(required=True)}))
@api.doc(description="Validate connector session token and return user info")
def post(self):
try:
data = request.get_json()
provider = data.get('provider')
session_token = data.get('session_token')
if not provider or not session_token:
return make_response(jsonify({"success": False, "error": "provider and session_token are required"}), 400)
decoded_token = request.decoded_token
if not decoded_token:
return make_response(jsonify({"success": False, "error": "Unauthorized"}), 401)
user = decoded_token.get('sub')
session = sessions_collection.find_one({"session_token": session_token, "user": user})
if not session or "token_info" not in session:
return make_response(jsonify({"success": False, "error": "Invalid or expired session"}), 401)
token_info = session["token_info"]
auth = ConnectorCreator.create_auth(provider)
is_expired = auth.is_token_expired(token_info)
return make_response(jsonify({
"success": True,
"expired": is_expired,
"user_email": session.get('user_email', 'Connected User')
}), 200)
except Exception as e:
current_app.logger.error(f"Error validating connector session: {e}")
return make_response(jsonify({"success": False, "error": str(e)}), 500)
@connectors_ns.route("/api/connectors/disconnect")
class ConnectorDisconnect(Resource):
@api.expect(api.model("ConnectorDisconnectModel", {"provider": fields.String(required=True), "session_token": fields.String(required=False)}))
@api.doc(description="Disconnect a connector session")
def post(self):
try:
data = request.get_json()
provider = data.get('provider')
session_token = data.get('session_token')
if not provider:
return make_response(jsonify({"success": False, "error": "provider is required"}), 400)
if session_token:
sessions_collection.delete_one({"session_token": session_token})
return make_response(jsonify({"success": True}), 200)
except Exception as e:
current_app.logger.error(f"Error disconnecting connector session: {e}")
return make_response(jsonify({"success": False, "error": str(e)}), 500)
@connectors_ns.route("/api/connectors/sync")
class ConnectorSync(Resource):
@api.expect(
api.model(
"ConnectorSyncModel",
{
"source_id": fields.String(required=True, description="Source ID to sync"),
"session_token": fields.String(required=True, description="Authentication token")
},
)
)
@api.doc(description="Sync connector source to check for modifications")
def post(self):
decoded_token = request.decoded_token
if not decoded_token:
return make_response(jsonify({"success": False}), 401)
try:
data = request.get_json()
source_id = data.get('source_id')
session_token = data.get('session_token')
if not all([source_id, session_token]):
return make_response(
jsonify({
"success": False,
"error": "source_id and session_token are required"
}),
400
)
source = sources_collection.find_one({"_id": ObjectId(source_id)})
if not source:
return make_response(
jsonify({
"success": False,
"error": "Source not found"
}),
404
)
if source.get('user') != decoded_token.get('sub'):
return make_response(
jsonify({
"success": False,
"error": "Unauthorized access to source"
}),
403
)
remote_data = {}
try:
if source.get('remote_data'):
remote_data = json.loads(source.get('remote_data'))
except json.JSONDecodeError:
current_app.logger.error(f"Invalid remote_data format for source {source_id}")
remote_data = {}
source_type = remote_data.get('provider')
if not source_type:
return make_response(
jsonify({
"success": False,
"error": "Source provider not found in remote_data"
}),
400
)
# Extract configuration from remote_data
file_ids = remote_data.get('file_ids', [])
folder_ids = remote_data.get('folder_ids', [])
recursive = remote_data.get('recursive', True)
# Start the sync task
task = ingest_connector_task.delay(
job_name=source.get('name'),
user=decoded_token.get('sub'),
source_type=source_type,
session_token=session_token,
file_ids=file_ids,
folder_ids=folder_ids,
recursive=recursive,
retriever=source.get('retriever', 'classic'),
operation_mode="sync",
doc_id=source_id,
sync_frequency=source.get('sync_frequency', 'never')
)
return make_response(
jsonify({
"success": True,
"task_id": task.id
}),
200
)
except Exception as err:
current_app.logger.error(
f"Error syncing connector source: {err}",
exc_info=True
)
return make_response(
jsonify({
"success": False,
"error": str(err)
}),
400
)
@connectors_ns.route("/api/connectors/callback-status")
class ConnectorCallbackStatus(Resource):
@api.doc(description="Return HTML page with connector authentication status")
def get(self):
"""Return HTML page with connector authentication status"""
try:
status = request.args.get('status', 'error')
message = request.args.get('message', '')
provider = request.args.get('provider', 'connector')
session_token = request.args.get('session_token', '')
user_email = request.args.get('user_email', '')
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<title>{provider.replace('_', ' ').title()} Authentication</title>
<style>
body {{ font-family: Arial, sans-serif; text-align: center; padding: 40px; }}
.container {{ max-width: 600px; margin: 0 auto; }}
.success {{ color: #4CAF50; }}
.error {{ color: #F44336; }}
</style>
<script>
window.onload = function() {{
const status = "{status}";
const sessionToken = "{session_token}";
const userEmail = "{user_email}";
if (status === "success" && window.opener) {{
window.opener.postMessage({{
type: '{provider}_auth_success',
session_token: sessionToken,
user_email: userEmail
}}, '*');
setTimeout(() => window.close(), 3000);
}}
}};
</script>
</head>
<body>
<div class="container">
<h2>{provider.replace('_', ' ').title()} Authentication</h2>
<div class="{status}">
<p>{message}</p>
{f'<p>Connected as: {user_email}</p>' if status == 'success' else ''}
</div>
<p><small>You can close this window. {f"Your {provider.replace('_', ' ').title()} is now connected and ready to use." if status == 'success' else ''}</small></p>
</div>
</body>
</html>
"""
return make_response(html_content, 200, {'Content-Type': 'text/html'})
except Exception as e:
current_app.logger.error(f"Error rendering callback status page: {e}")
return make_response("Authentication error occurred", 500, {'Content-Type': 'text/html'})

View File

@@ -1,14 +1,18 @@
import os
import datetime
import json
from flask import Blueprint, request, send_from_directory
from werkzeug.utils import secure_filename
from bson.objectid import ObjectId
import logging
from application.core.mongo_db import MongoDB
from application.core.settings import settings
from application.storage.storage_creator import StorageCreator
logger = logging.getLogger(__name__)
mongo = MongoDB.get_client()
db = mongo["docsgpt"]
db = mongo[settings.MONGO_DB_NAME]
conversations_collection = db["conversations"]
sources_collection = db["sources"]
@@ -34,37 +38,52 @@ def upload_index_files():
"""Upload two files(index.faiss, index.pkl) to the user's folder."""
if "user" not in request.form:
return {"status": "no user"}
user = secure_filename(request.form["user"])
user = request.form["user"]
if "name" not in request.form:
return {"status": "no name"}
job_name = secure_filename(request.form["name"])
tokens = secure_filename(request.form["tokens"])
retriever = secure_filename(request.form["retriever"])
id = secure_filename(request.form["id"])
type = secure_filename(request.form["type"])
job_name = request.form["name"]
tokens = request.form["tokens"]
retriever = request.form["retriever"]
id = request.form["id"]
type = request.form["type"]
remote_data = request.form["remote_data"] if "remote_data" in request.form else None
sync_frequency = secure_filename(request.form["sync_frequency"]) if "sync_frequency" in request.form else None
sync_frequency = request.form["sync_frequency"] if "sync_frequency" in request.form else None
file_path = request.form.get("file_path")
directory_structure = request.form.get("directory_structure")
if directory_structure:
try:
directory_structure = json.loads(directory_structure)
except Exception:
logger.error("Error parsing directory_structure")
directory_structure = {}
else:
directory_structure = {}
save_dir = os.path.join(current_dir, "indexes", str(id))
storage = StorageCreator.get_storage()
index_base_path = f"indexes/{id}"
if settings.VECTOR_STORE == "faiss":
if "file_faiss" not in request.files:
print("No file part")
logger.error("No file_faiss part")
return {"status": "no file"}
file_faiss = request.files["file_faiss"]
if file_faiss.filename == "":
return {"status": "no file name"}
if "file_pkl" not in request.files:
print("No file part")
logger.error("No file_pkl part")
return {"status": "no file"}
file_pkl = request.files["file_pkl"]
if file_pkl.filename == "":
return {"status": "no file name"}
# saves index files
if not os.path.exists(save_dir):
os.makedirs(save_dir)
file_faiss.save(os.path.join(save_dir, "index.faiss"))
file_pkl.save(os.path.join(save_dir, "index.pkl"))
# Save index files to storage
faiss_storage_path = f"{index_base_path}/index.faiss"
pkl_storage_path = f"{index_base_path}/index.pkl"
storage.save_file(file_faiss, faiss_storage_path)
storage.save_file(file_pkl, pkl_storage_path)
existing_entry = sources_collection.find_one({"_id": ObjectId(id)})
if existing_entry:
@@ -82,6 +101,8 @@ def upload_index_files():
"retriever": retriever,
"remote_data": remote_data,
"sync_frequency": sync_frequency,
"file_path": file_path,
"directory_structure": directory_structure,
}
},
)
@@ -99,6 +120,8 @@ def upload_index_files():
"retriever": retriever,
"remote_data": remote_data,
"sync_frequency": sync_frequency,
"file_path": file_path,
"directory_structure": directory_structure,
}
)
return {"status": "ok"}

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,18 @@
from datetime import timedelta
from application.celery_init import celery
from application.worker import ingest_worker, remote_worker, sync_worker
from application.worker import (
agent_webhook_worker,
attachment_worker,
ingest_worker,
remote_worker,
sync_worker,
)
@celery.task(bind=True)
def ingest(self, directory, formats, name_job, filename, user):
resp = ingest_worker(self, directory, formats, name_job, filename, user)
def ingest(self, directory, formats, job_name, user, file_path, filename):
resp = ingest_worker(self, directory, formats, job_name, file_path, filename, user)
return resp
@@ -16,12 +22,64 @@ def ingest_remote(self, source_data, job_name, user, loader):
return resp
@celery.task(bind=True)
def reingest_source_task(self, source_id, user):
from application.worker import reingest_source_worker
resp = reingest_source_worker(self, source_id, user)
return resp
@celery.task(bind=True)
def schedule_syncs(self, frequency):
resp = sync_worker(self, frequency)
return resp
@celery.task(bind=True)
def store_attachment(self, file_info, user):
resp = attachment_worker(self, file_info, user)
return resp
@celery.task(bind=True)
def process_agent_webhook(self, agent_id, payload):
resp = agent_webhook_worker(self, agent_id, payload)
return resp
@celery.task(bind=True)
def ingest_connector_task(
self,
job_name,
user,
source_type,
session_token=None,
file_ids=None,
folder_ids=None,
recursive=True,
retriever="classic",
operation_mode="upload",
doc_id=None,
sync_frequency="never"
):
from application.worker import ingest_connector
resp = ingest_connector(
self,
job_name,
user,
source_type,
session_token=session_token,
file_ids=file_ids,
folder_ids=folder_ids,
recursive=recursive,
retriever=retriever,
operation_mode=operation_mode,
doc_id=doc_id,
sync_frequency=sync_frequency
)
return resp
@celery.on_after_configure.connect
def setup_periodic_tasks(sender, **kwargs):
sender.add_periodic_task(

View File

@@ -1,28 +1,37 @@
import os
import platform
import uuid
import dotenv
from flask import Flask, redirect, request
from flask import Flask, jsonify, redirect, request
from jose import jwt
from application.auth import handle_auth
from application.api.answer.routes import answer
from application.api.internal.routes import internal
from application.api.user.routes import user
from application.celery_init import celery
from application.core.logging_config import setup_logging
from application.core.settings import settings
from application.extensions import api
setup_logging()
from application.api import api # noqa: E402
from application.api.answer import answer # noqa: E402
from application.api.internal.routes import internal # noqa: E402
from application.api.user.routes import user # noqa: E402
from application.api.connector.routes import connector # noqa: E402
from application.celery_init import celery # noqa: E402
from application.core.settings import settings # noqa: E402
if platform.system() == "Windows":
import pathlib
pathlib.PosixPath = pathlib.WindowsPath
dotenv.load_dotenv()
setup_logging()
app = Flask(__name__)
app.register_blueprint(user)
app.register_blueprint(answer)
app.register_blueprint(internal)
app.register_blueprint(connector)
app.config.update(
UPLOAD_FOLDER="inputs",
CELERY_BROKER_URL=settings.CELERY_BROKER_URL,
@@ -32,6 +41,24 @@ app.config.update(
celery.config_from_object("application.celeryconfig")
api.init_app(app)
if settings.AUTH_TYPE in ("simple_jwt", "session_jwt") and not settings.JWT_SECRET_KEY:
key_file = ".jwt_secret_key"
try:
with open(key_file, "r") as f:
settings.JWT_SECRET_KEY = f.read().strip()
except FileNotFoundError:
new_key = os.urandom(32).hex()
with open(key_file, "w") as f:
f.write(new_key)
settings.JWT_SECRET_KEY = new_key
except Exception as e:
raise RuntimeError(f"Failed to setup JWT_SECRET_KEY: {e}")
SIMPLE_JWT_TOKEN = None
if settings.AUTH_TYPE == "simple_jwt":
payload = {"sub": "local"}
SIMPLE_JWT_TOKEN = jwt.encode(payload, settings.JWT_SECRET_KEY, algorithm="HS256")
print(f"Generated Simple JWT Token: {SIMPLE_JWT_TOKEN}")
@app.route("/")
def home():
@@ -41,11 +68,46 @@ def home():
return "Welcome to DocsGPT Backend!"
@app.route("/api/config")
def get_config():
response = {
"auth_type": settings.AUTH_TYPE,
"requires_auth": settings.AUTH_TYPE in ["simple_jwt", "session_jwt"],
}
return jsonify(response)
@app.route("/api/generate_token")
def generate_token():
if settings.AUTH_TYPE == "session_jwt":
new_user_id = str(uuid.uuid4())
token = jwt.encode(
{"sub": new_user_id}, settings.JWT_SECRET_KEY, algorithm="HS256"
)
return jsonify({"token": token})
return jsonify({"error": "Token generation not allowed in current auth mode"}), 400
@app.before_request
def authenticate_request():
if request.method == "OPTIONS":
return "", 200
decoded_token = handle_auth(request)
if not decoded_token:
request.decoded_token = None
elif "error" in decoded_token:
return jsonify(decoded_token), 401
else:
request.decoded_token = decoded_token
@app.after_request
def after_request(response):
response.headers.add("Access-Control-Allow-Origin", "*")
response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization")
response.headers.add("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE,OPTIONS")
response.headers.add("Access-Control-Allow-Headers", "Content-Type, Authorization")
response.headers.add(
"Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"
)
return response

28
application/auth.py Normal file
View File

@@ -0,0 +1,28 @@
from jose import jwt
from application.core.settings import settings
def handle_auth(request, data={}):
if settings.AUTH_TYPE in ["simple_jwt", "session_jwt"]:
jwt_token = request.headers.get("Authorization")
if not jwt_token:
return None
jwt_token = jwt_token.replace("Bearer ", "")
try:
decoded_token = jwt.decode(
jwt_token,
settings.JWT_SECRET_KEY,
algorithms=["HS256"],
options={"verify_exp": False},
)
return decoded_token
except Exception as e:
return {
"message": f"Authentication error: {str(e)}",
"error": "invalid_token",
}
else:
return {"sub": "local"}

View File

@@ -11,21 +11,25 @@ from application.utils import get_hash
logger = logging.getLogger(__name__)
_redis_instance = None
_redis_creation_failed = False
_instance_lock = Lock()
def get_redis_instance():
global _redis_instance
if _redis_instance is None:
global _redis_instance, _redis_creation_failed
if _redis_instance is None and not _redis_creation_failed:
with _instance_lock:
if _redis_instance is None:
if _redis_instance is None and not _redis_creation_failed:
try:
_redis_instance = redis.Redis.from_url(
settings.CACHE_REDIS_URL, socket_connect_timeout=2
)
except ValueError as e:
logger.error(f"Invalid Redis URL: {e}")
_redis_creation_failed = True # Stop future attempts
_redis_instance = None
except redis.ConnectionError as e:
logger.error(f"Redis connection error: {e}")
_redis_instance = None
_redis_instance = None # Keep trying for connection errors
return _redis_instance
@@ -41,36 +45,48 @@ def gen_cache_key(messages, model="docgpt", tools=None):
def gen_cache(func):
def wrapper(self, model, messages, stream, tools=None, *args, **kwargs):
if tools is not None:
return func(self, model, messages, stream, tools, *args, **kwargs)
try:
cache_key = gen_cache_key(messages, model, tools)
redis_client = get_redis_instance()
if redis_client:
try:
cached_response = redis_client.get(cache_key)
if cached_response:
return cached_response.decode("utf-8")
except redis.ConnectionError as e:
logger.error(f"Redis connection error: {e}")
result = func(self, model, messages, stream, tools, *args, **kwargs)
if redis_client and isinstance(result, str):
try:
redis_client.set(cache_key, result, ex=1800)
except redis.ConnectionError as e:
logger.error(f"Redis connection error: {e}")
return result
except ValueError as e:
logger.error(e)
return "Error: No user message found in the conversation to generate a cache key."
logger.error(f"Cache key generation failed: {e}")
return func(self, model, messages, stream, tools, *args, **kwargs)
redis_client = get_redis_instance()
if redis_client:
try:
cached_response = redis_client.get(cache_key)
if cached_response:
return cached_response.decode("utf-8")
except Exception as e:
logger.error(f"Error getting cached response: {e}", exc_info=True)
result = func(self, model, messages, stream, tools, *args, **kwargs)
if redis_client and isinstance(result, str):
try:
redis_client.set(cache_key, result, ex=1800)
except Exception as e:
logger.error(f"Error setting cache: {e}", exc_info=True)
return result
return wrapper
def stream_cache(func):
def wrapper(self, model, messages, stream, tools=None, *args, **kwargs):
cache_key = gen_cache_key(messages, model, tools)
logger.info(f"Stream cache key: {cache_key}")
if tools is not None:
yield from func(self, model, messages, stream, tools, *args, **kwargs)
return
try:
cache_key = gen_cache_key(messages, model, tools)
except ValueError as e:
logger.error(f"Cache key generation failed: {e}")
yield from func(self, model, messages, stream, tools, *args, **kwargs)
return
redis_client = get_redis_instance()
if redis_client:
@@ -81,23 +97,21 @@ def stream_cache(func):
cached_response = json.loads(cached_response.decode("utf-8"))
for chunk in cached_response:
yield chunk
time.sleep(0.03)
time.sleep(0.03) # Simulate streaming delay
return
except redis.ConnectionError as e:
logger.error(f"Redis connection error: {e}")
except Exception as e:
logger.error(f"Error getting cached stream: {e}", exc_info=True)
result = func(self, model, messages, stream, tools=tools, *args, **kwargs)
stream_cache_data = []
for chunk in result:
stream_cache_data.append(chunk)
for chunk in func(self, model, messages, stream, tools, *args, **kwargs):
yield chunk
stream_cache_data.append(str(chunk))
if redis_client:
try:
redis_client.set(cache_key, json.dumps(stream_cache_data), ex=1800)
logger.info(f"Stream cache saved for key: {cache_key}")
except redis.ConnectionError as e:
logger.error(f"Redis connection error: {e}")
except Exception as e:
logger.error(f"Error setting stream cache: {e}", exc_info=True)
return wrapper

View File

@@ -1,26 +1,51 @@
import os
from pathlib import Path
from typing import Optional
import os
from pydantic_settings import BaseSettings
current_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
current_dir = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
class Settings(BaseSettings):
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
AUTH_TYPE: Optional[str] = None # simple_jwt, session_jwt, or None
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"
MODEL_PATH: str = os.path.join(current_dir, "models/docsgpt-7b-f16.gguf")
MONGO_DB_NAME: str = "docsgpt"
LLM_PATH: str = os.path.join(current_dir, "models/docsgpt-7b-f16.gguf")
DEFAULT_MAX_HISTORY: int = 150
MODEL_TOKEN_LIMITS: dict = {"gpt-4o-mini": 128000, "gpt-3.5-turbo": 4096, "claude-2": 1e5}
LLM_TOKEN_LIMITS: dict = {
"gpt-4o-mini": 128000,
"gpt-3.5-turbo": 4096,
"claude-2": 1e5,
"gemini-2.0-flash-exp": 1e6,
}
UPLOAD_FOLDER: str = "inputs"
PARSE_PDF_AS_IMAGE: bool = False
VECTOR_STORE: str = "faiss" # "faiss" or "elasticsearch" or "qdrant" or "milvus" or "lancedb"
RETRIEVERS_ENABLED: list = ["classic_rag", "duckduck_search"] # also brave_search
PARSE_IMAGE_REMOTE: bool = False
VECTOR_STORE: str = (
"faiss" # "faiss" or "elasticsearch" or "qdrant" or "milvus" or "lancedb"
)
RETRIEVERS_ENABLED: list = ["classic_rag"]
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
# Google Drive integration
GOOGLE_CLIENT_ID: Optional[str] = None # Replace with your actual Google OAuth client ID
GOOGLE_CLIENT_SECRET: Optional[str] = None# Replace with your actual Google OAuth client secret
CONNECTOR_REDIRECT_BASE_URI: Optional[str] = "http://127.0.0.1:7091/api/connectors/callback"
##append ?provider={provider_name} in your Provider console like http://127.0.0.1:7091/api/connectors/callback?provider=google_drive
# LLM Cache
CACHE_REDIS_URL: str = "redis://localhost:6379/2"
@@ -28,12 +53,18 @@ class Settings(BaseSettings):
API_URL: str = "http://localhost:7091" # backend url for celery worker
API_KEY: Optional[str] = None # LLM api key
EMBEDDINGS_KEY: Optional[str] = None # api key for embeddings (if using openai, just copy API_KEY)
EMBEDDINGS_KEY: Optional[str] = (
None # api key for embeddings (if using openai, just copy API_KEY)
)
OPENAI_API_BASE: Optional[str] = None # azure openai api base url
OPENAI_API_VERSION: Optional[str] = None # azure openai api version
AZURE_DEPLOYMENT_NAME: Optional[str] = None # azure deployment name for answering
AZURE_EMBEDDINGS_DEPLOYMENT_NAME: Optional[str] = None # azure deployment name for embeddings
OPENAI_BASE_URL: Optional[str] = None # openai base url for open ai compatable models
AZURE_EMBEDDINGS_DEPLOYMENT_NAME: Optional[str] = (
None # azure deployment name for embeddings
)
OPENAI_BASE_URL: Optional[str] = (
None # openai base url for open ai compatable models
)
# elasticsearch
ELASTIC_CLOUD_ID: Optional[str] = None # cloud id for elasticsearch
@@ -65,18 +96,25 @@ class Settings(BaseSettings):
QDRANT_HOST: Optional[str] = None
QDRANT_PATH: Optional[str] = None
QDRANT_DISTANCE_FUNC: str = "Cosine"
# PGVector vectorstore config
PGVECTOR_CONNECTION_STRING: Optional[str] = None
# Milvus vectorstore config
MILVUS_COLLECTION_NAME: Optional[str] = "docsgpt"
MILVUS_URI: Optional[str] = "./milvus_local.db" # milvus lite version as default
MILVUS_URI: Optional[str] = "./milvus_local.db" # milvus lite version as default
MILVUS_TOKEN: Optional[str] = ""
# LanceDB vectorstore config
LANCEDB_PATH: str = "/tmp/lancedb" # Path where LanceDB stores its local data
LANCEDB_TABLE_NAME: Optional[str] = "docsgpts" # Name of the table to use for storing vectors
BRAVE_SEARCH_API_KEY: Optional[str] = None
LANCEDB_TABLE_NAME: Optional[str] = (
"docsgpts" # Name of the table to use for storing vectors
)
FLASK_DEBUG_MODE: bool = False
STORAGE_TYPE: str = "local" # local or s3
URL_STRATEGY: str = "backend" # backend or s3
JWT_SECRET_KEY: str = ""
path = Path(__file__).parent.parent.absolute()

View File

@@ -1,7 +0,0 @@
from flask_restx import Api
api = Api(
version="1.0",
title="DocsGPT API",
description="API for DocsGPT",
)

View File

@@ -1,52 +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):
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")
@@ -54,3 +119,26 @@ class BaseLLM(ABC):
def _supports_tools(self):
raise NotImplementedError("Subclass must implement _supports_tools method")
def supports_structured_output(self):
"""Check if the LLM supports structured output/JSON schema enforcement"""
return hasattr(self, "_supports_structured_output") and callable(
getattr(self, "_supports_structured_output")
)
def _supports_structured_output(self):
return False
def prepare_structured_output_format(self, json_schema):
"""Prepare structured output format specific to the LLM provider"""
_ = json_schema
return None
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
"""
return []

View File

@@ -1,34 +1,131 @@
from application.llm.base import BaseLLM
import json
import requests
from application.core.settings import settings
from application.llm.base import BaseLLM
class DocsGPTAPILLM(BaseLLM):
def __init__(self, api_key=None, user_api_key=None, *args, **kwargs):
from openai import OpenAI
super().__init__(*args, **kwargs)
self.api_key = api_key
self.client = OpenAI(api_key="sk-docsgpt-public", base_url="https://oai.arc53.com")
self.user_api_key = user_api_key
self.endpoint = "https://llm.arc53.com"
self.api_key = api_key
def _raw_gen(self, baseself, model, messages, stream=False, *args, **kwargs):
response = requests.post(
f"{self.endpoint}/answer", json={"messages": messages, "max_new_tokens": 30}
)
response_clean = response.json()["a"].replace("###", "")
def _clean_messages_openai(self, messages):
cleaned_messages = []
for message in messages:
role = message.get("role")
content = message.get("content")
return response_clean
if role == "model":
role = "assistant"
def _raw_gen_stream(self, baseself, model, messages, stream=True, *args, **kwargs):
response = requests.post(
f"{self.endpoint}/stream",
json={"messages": messages, "max_new_tokens": 256},
stream=True,
)
if role and content is not None:
if isinstance(content, str):
cleaned_messages.append({"role": role, "content": content})
elif isinstance(content, list):
for item in content:
if "text" in item:
cleaned_messages.append(
{"role": role, "content": item["text"]}
)
elif "function_call" in item:
tool_call = {
"id": item["function_call"]["call_id"],
"type": "function",
"function": {
"name": item["function_call"]["name"],
"arguments": json.dumps(
item["function_call"]["args"]
),
},
}
cleaned_messages.append(
{
"role": "assistant",
"content": None,
"tool_calls": [tool_call],
}
)
elif "function_response" in item:
cleaned_messages.append(
{
"role": "tool",
"tool_call_id": item["function_response"][
"call_id"
],
"content": json.dumps(
item["function_response"]["response"]["result"]
),
}
)
else:
raise ValueError(
f"Unexpected content dictionary format: {item}"
)
else:
raise ValueError(f"Unexpected content type: {type(content)}")
for line in response.iter_lines():
if line:
data_str = line.decode("utf-8")
if data_str.startswith("data: "):
data = json.loads(data_str[6:])
yield data["a"]
return cleaned_messages
def _raw_gen(
self,
baseself,
model,
messages,
stream=False,
tools=None,
engine=settings.AZURE_DEPLOYMENT_NAME,
**kwargs,
):
messages = self._clean_messages_openai(messages)
if tools:
response = self.client.chat.completions.create(
model="docsgpt",
messages=messages,
stream=stream,
tools=tools,
**kwargs,
)
return response.choices[0]
else:
response = self.client.chat.completions.create(
model="docsgpt", messages=messages, stream=stream, **kwargs
)
return response.choices[0].message.content
def _raw_gen_stream(
self,
baseself,
model,
messages,
stream=True,
tools=None,
engine=settings.AZURE_DEPLOYMENT_NAME,
**kwargs,
):
messages = self._clean_messages_openai(messages)
if tools:
response = self.client.chat.completions.create(
model="docsgpt",
messages=messages,
stream=stream,
tools=tools,
**kwargs,
)
else:
response = self.client.chat.completions.create(
model="docsgpt", messages=messages, stream=stream, **kwargs
)
for line in response:
if len(line.choices) > 0 and line.choices[0].delta.content is not None and len(line.choices[0].delta.content) > 0:
yield line.choices[0].delta.content
elif len(line.choices) > 0:
yield line.choices[0]
def _supports_tools(self):
return True

View File

@@ -1,7 +1,13 @@
import json
import logging
from google import genai
from google.genai import types
from application.core.settings import settings
from application.llm.base import BaseLLM
from application.storage.storage_creator import StorageCreator
class GoogleLLM(BaseLLM):
@@ -9,6 +15,132 @@ class GoogleLLM(BaseLLM):
super().__init__(*args, **kwargs)
self.api_key = api_key
self.user_api_key = user_api_key
self.client = genai.Client(api_key=self.api_key)
self.storage = StorageCreator.get_storage()
def get_supported_attachment_types(self):
"""
Return a list of MIME types supported by Google Gemini for file uploads.
Returns:
list: List of supported MIME types
"""
return [
"application/pdf",
"image/png",
"image/jpeg",
"image/jpg",
"image/webp",
"image/gif",
]
def prepare_messages_with_attachments(self, messages, attachments=None):
"""
Process attachments using Google AI's file API for more efficient handling.
Args:
messages (list): List of message dictionaries.
attachments (list): List of attachment dictionaries with content and metadata.
Returns:
list: Messages formatted with file references for Google AI API.
"""
if not attachments:
return messages
prepared_messages = messages.copy()
# Find the user message to attach files to the last one
user_message_index = None
for i in range(len(prepared_messages) - 1, -1, -1):
if prepared_messages[i].get("role") == "user":
user_message_index = i
break
if user_message_index is None:
user_message = {"role": "user", "content": []}
prepared_messages.append(user_message)
user_message_index = len(prepared_messages) - 1
if isinstance(prepared_messages[user_message_index].get("content"), str):
text_content = prepared_messages[user_message_index]["content"]
prepared_messages[user_message_index]["content"] = [
{"type": "text", "text": text_content}
]
elif not isinstance(prepared_messages[user_message_index].get("content"), list):
prepared_messages[user_message_index]["content"] = []
files = []
for attachment in attachments:
mime_type = attachment.get("mime_type")
if mime_type in self.get_supported_attachment_types():
try:
file_uri = self._upload_file_to_google(attachment)
logging.info(
f"GoogleLLM: Successfully uploaded file, got URI: {file_uri}"
)
files.append({"file_uri": file_uri, "mime_type": mime_type})
except Exception as e:
logging.error(
f"GoogleLLM: Error uploading file: {e}", exc_info=True
)
if "content" in attachment:
prepared_messages[user_message_index]["content"].append(
{
"type": "text",
"text": f"[File could not be processed: {attachment.get('path', 'unknown')}]",
}
)
if files:
logging.info(f"GoogleLLM: Adding {len(files)} files to message")
prepared_messages[user_message_index]["content"].append({"files": files})
return prepared_messages
def _upload_file_to_google(self, attachment):
"""
Upload a file to Google AI and return the file URI.
Args:
attachment (dict): Attachment dictionary with path and metadata.
Returns:
str: Google AI file URI for the uploaded file.
"""
if "google_file_uri" in attachment:
return attachment["google_file_uri"]
file_path = attachment.get("path")
if not file_path:
raise ValueError("No file path provided in attachment")
if not self.storage.file_exists(file_path):
raise FileNotFoundError(f"File not found: {file_path}")
try:
file_uri = self.storage.process_file(
file_path,
lambda local_path, **kwargs: self.client.files.upload(
file=local_path
).uri,
)
from application.core.mongo_db import MongoDB
mongo = MongoDB.get_client()
db = mongo[settings.MONGO_DB_NAME]
attachments_collection = db["attachments"]
if "_id" in attachment:
attachments_collection.update_one(
{"_id": attachment["_id"]}, {"$set": {"google_file_uri": file_uri}}
)
return file_uri
except Exception as e:
logging.error(f"Error uploading file to Google AI: {e}", exc_info=True)
raise
def _clean_messages_google(self, messages):
cleaned_messages = []
@@ -22,11 +154,11 @@ class GoogleLLM(BaseLLM):
parts = []
if role and content is not None:
if isinstance(content, str):
parts = [types.Part.from_text(content)]
parts = [types.Part.from_text(text=content)]
elif isinstance(content, list):
for item in content:
if "text" in item:
parts.append(types.Part.from_text(item["text"]))
parts.append(types.Part.from_text(text=item["text"]))
elif "function_call" in item:
parts.append(
types.Part.from_function_call(
@@ -41,6 +173,14 @@ class GoogleLLM(BaseLLM):
response=item["function_response"]["response"],
)
)
elif "files" in item:
for file_data in item["files"]:
parts.append(
types.Part.from_uri(
file_uri=file_data["file_uri"],
mime_type=file_data["mime_type"],
)
)
else:
raise ValueError(
f"Unexpected content dictionary format:{item}"
@@ -99,6 +239,7 @@ class GoogleLLM(BaseLLM):
stream=False,
tools=None,
formatting="openai",
response_schema=None,
**kwargs,
):
client = genai.Client(api_key=self.api_key)
@@ -112,16 +253,21 @@ class GoogleLLM(BaseLLM):
if tools:
cleaned_tools = self._clean_tools_format(tools)
config.tools = cleaned_tools
response = client.models.generate_content(
model=model,
contents=messages,
config=config,
)
# Add response schema for structured output if provided
if response_schema:
config.response_schema = response_schema
config.response_mime_type = "application/json"
response = client.models.generate_content(
model=model,
contents=messages,
config=config,
)
if tools:
return response
else:
response = client.models.generate_content(
model=model, contents=messages, config=config
)
return response.text
def _raw_gen_stream(
@@ -132,6 +278,7 @@ class GoogleLLM(BaseLLM):
stream=True,
tools=None,
formatting="openai",
response_schema=None,
**kwargs,
):
client = genai.Client(api_key=self.api_key)
@@ -146,14 +293,114 @@ class GoogleLLM(BaseLLM):
cleaned_tools = self._clean_tools_format(tools)
config.tools = cleaned_tools
# Add response schema for structured output if provided
if response_schema:
config.response_schema = response_schema
config.response_mime_type = "application/json"
# Check if we have both tools and file attachments
has_attachments = False
for message in messages:
for part in message.parts:
if hasattr(part, "file_data") and part.file_data is not None:
has_attachments = True
break
if has_attachments:
break
logging.info(
f"GoogleLLM: Starting stream generation. Model: {model}, Messages: {json.dumps(messages, default=str)}, Has attachments: {has_attachments}"
)
response = client.models.generate_content_stream(
model=model,
contents=messages,
config=config,
)
for chunk in response:
if chunk.text is not None:
if hasattr(chunk, "candidates") and chunk.candidates:
for candidate in chunk.candidates:
if candidate.content and candidate.content.parts:
for part in candidate.content.parts:
if part.function_call:
yield part
elif part.text:
yield part.text
elif hasattr(chunk, "text"):
yield chunk.text
def _supports_tools(self):
return True
def _supports_structured_output(self):
return True
def prepare_structured_output_format(self, json_schema):
if not json_schema:
return None
type_map = {
"object": "OBJECT",
"array": "ARRAY",
"string": "STRING",
"integer": "INTEGER",
"number": "NUMBER",
"boolean": "BOOLEAN",
}
def convert(schema):
if not isinstance(schema, dict):
return schema
result = {}
schema_type = schema.get("type")
if schema_type:
result["type"] = type_map.get(schema_type.lower(), schema_type.upper())
for key in [
"description",
"nullable",
"enum",
"minItems",
"maxItems",
"required",
"propertyOrdering",
]:
if key in schema:
result[key] = schema[key]
if "format" in schema:
format_value = schema["format"]
if schema_type == "string":
if format_value == "date":
result["format"] = "date-time"
elif format_value in ["enum", "date-time"]:
result["format"] = format_value
else:
result["format"] = format_value
if "properties" in schema:
result["properties"] = {
k: convert(v) for k, v in schema["properties"].items()
}
if "propertyOrdering" not in result and result.get("type") == "OBJECT":
result["propertyOrdering"] = list(result["properties"].keys())
if "items" in schema:
result["items"] = convert(schema["items"])
for field in ["anyOf", "oneOf", "allOf"]:
if field in schema:
result[field] = [convert(s) for s in schema[field]]
return result
try:
return convert(json_schema)
except Exception as e:
logging.error(
f"Error preparing structured output format for Google: {e}",
exc_info=True,
)
return None

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:
handler_class = OpenAILLMHandler
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

@@ -7,6 +7,7 @@ from application.llm.anthropic import AnthropicLLM
from application.llm.docsgpt_provider import DocsGPTAPILLM
from application.llm.premai import PremAILLM
from application.llm.google_ai import GoogleLLM
from application.llm.novita import NovitaLLM
class LLMCreator:
@@ -20,12 +21,15 @@ class LLMCreator:
"docsgpt": DocsGPTAPILLM,
"premai": PremAILLM,
"groq": GroqLLM,
"google": GoogleLLM
"google": GoogleLLM,
"novita": NovitaLLM,
}
@classmethod
def create_llm(cls, type, api_key, user_api_key, *args, **kwargs):
def create_llm(cls, type, api_key, user_api_key, decoded_token, *args, **kwargs):
llm_class = cls.llms.get(type.lower())
if not llm_class:
raise ValueError(f"No LLM class found for type {type}")
return llm_class(api_key, user_api_key, *args, **kwargs)
return llm_class(
api_key, user_api_key, decoded_token=decoded_token, *args, **kwargs
)

32
application/llm/novita.py Normal file
View File

@@ -0,0 +1,32 @@
from application.llm.base import BaseLLM
from openai import OpenAI
class NovitaLLM(BaseLLM):
def __init__(self, api_key=None, user_api_key=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.client = OpenAI(api_key=api_key, base_url="https://api.novita.ai/v3/openai")
self.api_key = api_key
self.user_api_key = user_api_key
def _raw_gen(self, baseself, model, messages, stream=False, tools=None, **kwargs):
if tools:
response = self.client.chat.completions.create(
model=model, messages=messages, stream=stream, tools=tools, **kwargs
)
return response.choices[0]
else:
response = self.client.chat.completions.create(
model=model, messages=messages, stream=stream, **kwargs
)
return response.choices[0].message.content
def _raw_gen_stream(
self, baseself, model, messages, stream=True, tools=None, **kwargs
):
response = self.client.chat.completions.create(
model=model, messages=messages, stream=stream, **kwargs
)
for line in response:
if line.choices[0].delta.content is not None:
yield line.choices[0].delta.content

View File

@@ -1,5 +1,10 @@
import base64
import json
import logging
from application.core.settings import settings
from application.llm.base import BaseLLM
from application.storage.storage_creator import StorageCreator
class OpenAILLM(BaseLLM):
@@ -8,12 +13,101 @@ class OpenAILLM(BaseLLM):
from openai import OpenAI
super().__init__(*args, **kwargs)
if settings.OPENAI_BASE_URL:
if (
isinstance(settings.OPENAI_BASE_URL, str)
and settings.OPENAI_BASE_URL.strip()
):
self.client = OpenAI(api_key=api_key, base_url=settings.OPENAI_BASE_URL)
else:
self.client = OpenAI(api_key=api_key)
DEFAULT_OPENAI_API_BASE = "https://api.openai.com/v1"
self.client = OpenAI(api_key=api_key, base_url=DEFAULT_OPENAI_API_BASE)
self.api_key = api_key
self.user_api_key = user_api_key
self.storage = StorageCreator.get_storage()
def _clean_messages_openai(self, messages):
cleaned_messages = []
for message in messages:
role = message.get("role")
content = message.get("content")
if role == "model":
role = "assistant"
if role and content is not None:
if isinstance(content, str):
cleaned_messages.append({"role": role, "content": content})
elif isinstance(content, list):
for item in content:
if "text" in item:
cleaned_messages.append(
{"role": role, "content": item["text"]}
)
elif "function_call" in item:
tool_call = {
"id": item["function_call"]["call_id"],
"type": "function",
"function": {
"name": item["function_call"]["name"],
"arguments": json.dumps(
item["function_call"]["args"]
),
},
}
cleaned_messages.append(
{
"role": "assistant",
"content": None,
"tool_calls": [tool_call],
}
)
elif "function_response" in item:
cleaned_messages.append(
{
"role": "tool",
"tool_call_id": item["function_response"][
"call_id"
],
"content": json.dumps(
item["function_response"]["response"]["result"]
),
}
)
elif isinstance(item, dict):
content_parts = []
if "text" in item:
content_parts.append(
{"type": "text", "text": item["text"]}
)
elif (
"type" in item
and item["type"] == "text"
and "text" in item
):
content_parts.append(item)
elif (
"type" in item
and item["type"] == "file"
and "file" in item
):
content_parts.append(item)
elif (
"type" in item
and item["type"] == "image_url"
and "image_url" in item
):
content_parts.append(item)
cleaned_messages.append(
{"role": role, "content": content_parts}
)
else:
raise ValueError(
f"Unexpected content dictionary format: {item}"
)
else:
raise ValueError(f"Unexpected content type: {type(content)}")
return cleaned_messages
def _raw_gen(
self,
@@ -23,17 +117,29 @@ class OpenAILLM(BaseLLM):
stream=False,
tools=None,
engine=settings.AZURE_DEPLOYMENT_NAME,
response_format=None,
**kwargs,
):
messages = self._clean_messages_openai(messages)
request_params = {
"model": model,
"messages": messages,
"stream": stream,
**kwargs,
}
if tools:
request_params["tools"] = tools
if response_format:
request_params["response_format"] = response_format
response = self.client.chat.completions.create(**request_params)
if tools:
response = self.client.chat.completions.create(
model=model, messages=messages, stream=stream, tools=tools, **kwargs
)
return response.choices[0]
else:
response = self.client.chat.completions.create(
model=model, messages=messages, stream=stream, **kwargs
)
return response.choices[0].message.content
def _raw_gen_stream(
@@ -44,34 +150,274 @@ class OpenAILLM(BaseLLM):
stream=True,
tools=None,
engine=settings.AZURE_DEPLOYMENT_NAME,
response_format=None,
**kwargs,
):
response = self.client.chat.completions.create(
model=model, messages=messages, stream=stream, **kwargs
)
messages = self._clean_messages_openai(messages)
request_params = {
"model": model,
"messages": messages,
"stream": stream,
**kwargs,
}
if tools:
request_params["tools"] = tools
if response_format:
request_params["response_format"] = response_format
response = self.client.chat.completions.create(**request_params)
for line in response:
if line.choices[0].delta.content is not None:
if (
len(line.choices) > 0
and line.choices[0].delta.content is not None
and len(line.choices[0].delta.content) > 0
):
yield line.choices[0].delta.content
elif len(line.choices) > 0:
yield line.choices[0]
def _supports_tools(self):
return True
def _supports_structured_output(self):
return True
def prepare_structured_output_format(self, json_schema):
if not json_schema:
return None
try:
def add_additional_properties_false(schema_obj):
if isinstance(schema_obj, dict):
schema_copy = schema_obj.copy()
if schema_copy.get("type") == "object":
schema_copy["additionalProperties"] = False
# Ensure 'required' includes all properties for OpenAI strict mode
if "properties" in schema_copy:
schema_copy["required"] = list(
schema_copy["properties"].keys()
)
for key, value in schema_copy.items():
if key == "properties" and isinstance(value, dict):
schema_copy[key] = {
prop_name: add_additional_properties_false(prop_schema)
for prop_name, prop_schema in value.items()
}
elif key == "items" and isinstance(value, dict):
schema_copy[key] = add_additional_properties_false(value)
elif key in ["anyOf", "oneOf", "allOf"] and isinstance(
value, list
):
schema_copy[key] = [
add_additional_properties_false(sub_schema)
for sub_schema in value
]
return schema_copy
return schema_obj
processed_schema = add_additional_properties_false(json_schema)
result = {
"type": "json_schema",
"json_schema": {
"name": processed_schema.get("name", "response"),
"description": processed_schema.get(
"description", "Structured response"
),
"schema": processed_schema,
"strict": True,
},
}
return result
except Exception as e:
logging.error(f"Error preparing structured output format: {e}")
return None
def get_supported_attachment_types(self):
"""
Return a list of MIME types supported by OpenAI for file uploads.
Returns:
list: List of supported MIME types
"""
return [
"application/pdf",
"image/png",
"image/jpeg",
"image/jpg",
"image/webp",
"image/gif",
]
def prepare_messages_with_attachments(self, messages, attachments=None):
"""
Process attachments using OpenAI's file API for more efficient handling.
Args:
messages (list): List of message dictionaries.
attachments (list): List of attachment dictionaries with content and metadata.
Returns:
list: Messages formatted with file references for OpenAI API.
"""
if not attachments:
return messages
prepared_messages = messages.copy()
# Find the user message to attach file_id to the last one
user_message_index = None
for i in range(len(prepared_messages) - 1, -1, -1):
if prepared_messages[i].get("role") == "user":
user_message_index = i
break
if user_message_index is None:
user_message = {"role": "user", "content": []}
prepared_messages.append(user_message)
user_message_index = len(prepared_messages) - 1
if isinstance(prepared_messages[user_message_index].get("content"), str):
text_content = prepared_messages[user_message_index]["content"]
prepared_messages[user_message_index]["content"] = [
{"type": "text", "text": text_content}
]
elif not isinstance(prepared_messages[user_message_index].get("content"), list):
prepared_messages[user_message_index]["content"] = []
for attachment in attachments:
mime_type = attachment.get("mime_type")
if mime_type and mime_type.startswith("image/"):
try:
base64_image = self._get_base64_image(attachment)
prepared_messages[user_message_index]["content"].append(
{
"type": "image_url",
"image_url": {
"url": f"data:{mime_type};base64,{base64_image}"
},
}
)
except Exception as e:
logging.error(
f"Error processing image attachment: {e}", exc_info=True
)
if "content" in attachment:
prepared_messages[user_message_index]["content"].append(
{
"type": "text",
"text": f"[Image could not be processed: {attachment.get('path', 'unknown')}]",
}
)
# Handle PDFs using the file API
elif mime_type == "application/pdf":
try:
file_id = self._upload_file_to_openai(attachment)
prepared_messages[user_message_index]["content"].append(
{"type": "file", "file": {"file_id": file_id}}
)
except Exception as e:
logging.error(f"Error uploading PDF to OpenAI: {e}", exc_info=True)
if "content" in attachment:
prepared_messages[user_message_index]["content"].append(
{
"type": "text",
"text": f"File content:\n\n{attachment['content']}",
}
)
return prepared_messages
def _get_base64_image(self, attachment):
"""
Convert an image file to base64 encoding.
Args:
attachment (dict): Attachment dictionary with path and metadata.
Returns:
str: Base64-encoded image data.
"""
file_path = attachment.get("path")
if not file_path:
raise ValueError("No file path provided in attachment")
try:
with self.storage.get_file(file_path) as image_file:
return base64.b64encode(image_file.read()).decode("utf-8")
except FileNotFoundError:
raise FileNotFoundError(f"File not found: {file_path}")
def _upload_file_to_openai(self, attachment):
"""
Upload a file to OpenAI and return the file_id.
Args:
attachment (dict): Attachment dictionary with path and metadata.
Expected keys:
- path: Path to the file
- id: Optional MongoDB ID for caching
Returns:
str: OpenAI file_id for the uploaded file.
"""
import logging
if "openai_file_id" in attachment:
return attachment["openai_file_id"]
file_path = attachment.get("path")
if not self.storage.file_exists(file_path):
raise FileNotFoundError(f"File not found: {file_path}")
try:
file_id = self.storage.process_file(
file_path,
lambda local_path, **kwargs: self.client.files.create(
file=open(local_path, "rb"), purpose="assistants"
).id,
)
from application.core.mongo_db import MongoDB
mongo = MongoDB.get_client()
db = mongo[settings.MONGO_DB_NAME]
attachments_collection = db["attachments"]
if "_id" in attachment:
attachments_collection.update_one(
{"_id": attachment["_id"]}, {"$set": {"openai_file_id": file_id}}
)
return file_id
except Exception as e:
logging.error(f"Error uploading file to OpenAI: {e}", exc_info=True)
raise
class AzureOpenAILLM(OpenAILLM):
def __init__(
self, openai_api_key, openai_api_base, openai_api_version, deployment_name
):
super().__init__(openai_api_key)
def __init__(self, api_key, user_api_key, *args, **kwargs):
super().__init__(api_key)
self.api_base = (settings.OPENAI_API_BASE,)
self.api_version = (settings.OPENAI_API_VERSION,)
self.deployment_name = (settings.AZURE_DEPLOYMENT_NAME,)
from openai import AzureOpenAI
self.client = AzureOpenAI(
api_key=openai_api_key,
api_key=api_key,
api_version=settings.OPENAI_API_VERSION,
api_base=settings.OPENAI_API_BASE,
deployment_name=settings.AZURE_DEPLOYMENT_NAME,
azure_endpoint=settings.OPENAI_API_BASE,
)

161
application/logging.py Normal file
View File

@@ -0,0 +1,161 @@
import datetime
import functools
import inspect
import logging
import uuid
from typing import Any, Callable, Dict, Generator, List
from application.core.mongo_db import MongoDB
from application.core.settings import settings
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
class LogContext:
def __init__(self, endpoint, activity_id, user, api_key, query):
self.endpoint = endpoint
self.activity_id = activity_id
self.user = user
self.api_key = api_key
self.query = query
self.stacks = []
def build_stack_data(
obj: Any,
include_attributes: List[str] = None,
exclude_attributes: List[str] = None,
custom_data: Dict = None,
) -> Dict:
if obj is None:
raise ValueError("The 'obj' parameter cannot be None")
data = {}
if include_attributes is None:
include_attributes = []
for name, value in inspect.getmembers(obj):
if (
not name.startswith("_")
and not inspect.ismethod(value)
and not inspect.isfunction(value)
):
include_attributes.append(name)
for attr_name in include_attributes:
if exclude_attributes and attr_name in exclude_attributes:
continue
try:
attr_value = getattr(obj, attr_name)
if attr_value is not None:
if isinstance(attr_value, (int, float, str, bool)):
data[attr_name] = attr_value
elif isinstance(attr_value, list):
if all(isinstance(item, dict) for item in attr_value):
data[attr_name] = attr_value
elif all(hasattr(item, "__dict__") for item in attr_value):
data[attr_name] = [item.__dict__ for item in attr_value]
else:
data[attr_name] = [str(item) for item in attr_value]
elif isinstance(attr_value, dict):
data[attr_name] = {k: str(v) for k, v in attr_value.items()}
except AttributeError as e:
logging.warning(f"AttributeError while accessing {attr_name}: {e}")
except AttributeError:
pass
if custom_data:
data.update(custom_data)
return data
def log_activity() -> Callable:
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
activity_id = str(uuid.uuid4())
data = build_stack_data(args[0])
endpoint = data.get("endpoint", "")
user = data.get("user", "local")
api_key = data.get("user_api_key", "")
query = kwargs.get("query", getattr(args[0], "query", ""))
context = LogContext(endpoint, activity_id, user, api_key, query)
kwargs["log_context"] = context
logging.info(
f"Starting activity: {endpoint} - {activity_id} - User: {user}"
)
generator = func(*args, **kwargs)
yield from _consume_and_log(generator, context)
return wrapper
return decorator
def _consume_and_log(generator: Generator, context: "LogContext"):
try:
for item in generator:
yield item
except Exception as e:
logging.exception(f"Error in {context.endpoint} - {context.activity_id}: {e}")
context.stacks.append({"component": "error", "data": {"message": str(e)}})
_log_to_mongodb(
endpoint=context.endpoint,
activity_id=context.activity_id,
user=context.user,
api_key=context.api_key,
query=context.query,
stacks=context.stacks,
level="error",
)
raise
finally:
_log_to_mongodb(
endpoint=context.endpoint,
activity_id=context.activity_id,
user=context.user,
api_key=context.api_key,
query=context.query,
stacks=context.stacks,
level="info",
)
def _log_to_mongodb(
endpoint: str,
activity_id: str,
user: str,
api_key: str,
query: str,
stacks: List[Dict],
level: str,
) -> None:
try:
mongo = MongoDB.get_client()
db = mongo[settings.MONGO_DB_NAME]
user_logs_collection = db["stack_logs"]
log_entry = {
"endpoint": endpoint,
"id": activity_id,
"level": level,
"user": user,
"api_key": api_key,
"query": query,
"stacks": stacks,
"timestamp": datetime.datetime.now(datetime.timezone.utc),
}
# clean up text fields to be no longer than 10000 characters
for key, value in log_entry.items():
if isinstance(value, str) and len(value) > 10000:
log_entry[key] = value[:10000]
user_logs_collection.insert_one(log_entry)
logging.debug(f"Logged activity to MongoDB: {activity_id}")
except Exception as e:
logging.error(f"Failed to log to MongoDB: {e}", exc_info=True)

View File

@@ -32,16 +32,7 @@ class Chunker:
header, body = "", text # No header, treat entire text as body
return header, body
def combine_documents(self, doc: Document, next_doc: Document) -> Document:
combined_text = doc.text + " " + next_doc.text
combined_token_count = len(self.encoding.encode(combined_text))
new_doc = Document(
text=combined_text,
doc_id=doc.doc_id,
embedding=doc.embedding,
extra_info={**(doc.extra_info or {}), "token_count": combined_token_count}
)
return new_doc
def split_document(self, doc: Document) -> List[Document]:
split_docs = []
@@ -82,26 +73,11 @@ class Chunker:
processed_docs.append(doc)
i += 1
elif token_count < self.min_tokens:
if i + 1 < len(documents):
next_doc = documents[i + 1]
next_tokens = self.encoding.encode(next_doc.text)
if token_count + len(next_tokens) <= self.max_tokens:
# Combine small documents
combined_doc = self.combine_documents(doc, next_doc)
processed_docs.append(combined_doc)
i += 2
else:
# Keep the small document as is if adding next_doc would exceed max_tokens
doc.extra_info = doc.extra_info or {}
doc.extra_info["token_count"] = token_count
processed_docs.append(doc)
i += 1
else:
# No next document to combine with; add the small document as is
doc.extra_info = doc.extra_info or {}
doc.extra_info["token_count"] = token_count
processed_docs.append(doc)
i += 1
doc.extra_info = doc.extra_info or {}
doc.extra_info["token_count"] = token_count
processed_docs.append(doc)
i += 1
else:
# Split large documents
processed_docs.extend(self.split_document(doc))

View File

@@ -0,0 +1,18 @@
"""
External knowledge base connectors for DocsGPT.
This module contains connectors for external knowledge bases and document storage systems
that require authentication and specialized handling, separate from simple web scrapers.
"""
from .base import BaseConnectorAuth, BaseConnectorLoader
from .connector_creator import ConnectorCreator
from .google_drive import GoogleDriveAuth, GoogleDriveLoader
__all__ = [
'BaseConnectorAuth',
'BaseConnectorLoader',
'ConnectorCreator',
'GoogleDriveAuth',
'GoogleDriveLoader'
]

View File

@@ -0,0 +1,129 @@
"""
Base classes for external knowledge base connectors.
This module provides minimal abstract base classes that define the essential
interface for external knowledge base connectors.
"""
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional
from application.parser.schema.base import Document
class BaseConnectorAuth(ABC):
"""
Abstract base class for connector authentication.
Defines the minimal interface that all connector authentication
implementations must follow.
"""
@abstractmethod
def get_authorization_url(self, state: Optional[str] = None) -> str:
"""
Generate authorization URL for OAuth flows.
Args:
state: Optional state parameter for CSRF protection
Returns:
Authorization URL
"""
pass
@abstractmethod
def exchange_code_for_tokens(self, authorization_code: str) -> Dict[str, Any]:
"""
Exchange authorization code for access tokens.
Args:
authorization_code: Authorization code from OAuth callback
Returns:
Dictionary containing token information
"""
pass
@abstractmethod
def refresh_access_token(self, refresh_token: str) -> Dict[str, Any]:
"""
Refresh an expired access token.
Args:
refresh_token: Refresh token
Returns:
Dictionary containing refreshed token information
"""
pass
@abstractmethod
def is_token_expired(self, token_info: Dict[str, Any]) -> bool:
"""
Check if a token is expired.
Args:
token_info: Token information dictionary
Returns:
True if token is expired, False otherwise
"""
pass
class BaseConnectorLoader(ABC):
"""
Abstract base class for connector loaders.
Defines the minimal interface that all connector loader
implementations must follow.
"""
@abstractmethod
def __init__(self, session_token: str):
"""
Initialize the connector loader.
Args:
session_token: Authentication session token
"""
pass
@abstractmethod
def load_data(self, inputs: Dict[str, Any]) -> List[Document]:
"""
Load documents from the external knowledge base.
Args:
inputs: Configuration dictionary containing:
- file_ids: Optional list of specific file IDs to load
- folder_ids: Optional list of folder IDs to browse/download
- limit: Maximum number of items to return
- list_only: If True, return metadata without content
- recursive: Whether to recursively process folders
Returns:
List of Document objects
"""
pass
@abstractmethod
def download_to_directory(self, local_dir: str, source_config: Dict[str, Any] = None) -> Dict[str, Any]:
"""
Download files/folders to a local directory.
Args:
local_dir: Local directory path to download files to
source_config: Configuration for what to download
Returns:
Dictionary containing download results:
- files_downloaded: Number of files downloaded
- directory_path: Path where files were downloaded
- empty_result: Whether no files were downloaded
- source_type: Type of connector
- config_used: Configuration that was used
- error: Error message if download failed (optional)
"""
pass

View File

@@ -0,0 +1,81 @@
from application.parser.connectors.google_drive.loader import GoogleDriveLoader
from application.parser.connectors.google_drive.auth import GoogleDriveAuth
class ConnectorCreator:
"""
Factory class for creating external knowledge base connectors and auth providers.
These are different from remote loaders as they typically require
authentication and connect to external document storage systems.
"""
connectors = {
"google_drive": GoogleDriveLoader,
}
auth_providers = {
"google_drive": GoogleDriveAuth,
}
@classmethod
def create_connector(cls, connector_type, *args, **kwargs):
"""
Create a connector instance for the specified type.
Args:
connector_type: Type of connector to create (e.g., 'google_drive')
*args, **kwargs: Arguments to pass to the connector constructor
Returns:
Connector instance
Raises:
ValueError: If connector type is not supported
"""
connector_class = cls.connectors.get(connector_type.lower())
if not connector_class:
raise ValueError(f"No connector class found for type {connector_type}")
return connector_class(*args, **kwargs)
@classmethod
def create_auth(cls, connector_type):
"""
Create an auth provider instance for the specified connector type.
Args:
connector_type: Type of connector auth to create (e.g., 'google_drive')
Returns:
Auth provider instance
Raises:
ValueError: If connector type is not supported for auth
"""
auth_class = cls.auth_providers.get(connector_type.lower())
if not auth_class:
raise ValueError(f"No auth class found for type {connector_type}")
return auth_class()
@classmethod
def get_supported_connectors(cls):
"""
Get list of supported connector types.
Returns:
List of supported connector type strings
"""
return list(cls.connectors.keys())
@classmethod
def is_supported(cls, connector_type):
"""
Check if a connector type is supported.
Args:
connector_type: Type of connector to check
Returns:
True if supported, False otherwise
"""
return connector_type.lower() in cls.connectors

View File

@@ -0,0 +1,10 @@
"""
Google Drive connector for DocsGPT.
This module provides authentication and document loading capabilities for Google Drive.
"""
from .auth import GoogleDriveAuth
from .loader import GoogleDriveLoader
__all__ = ['GoogleDriveAuth', 'GoogleDriveLoader']

View File

@@ -0,0 +1,268 @@
import logging
import datetime
from typing import Optional, Dict, Any
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import Flow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
from application.core.settings import settings
from application.parser.connectors.base import BaseConnectorAuth
class GoogleDriveAuth(BaseConnectorAuth):
"""
Handles Google OAuth 2.0 authentication for Google Drive access.
"""
SCOPES = [
'https://www.googleapis.com/auth/drive.readonly',
'https://www.googleapis.com/auth/drive.metadata.readonly'
]
def __init__(self):
self.client_id = settings.GOOGLE_CLIENT_ID
self.client_secret = settings.GOOGLE_CLIENT_SECRET
self.redirect_uri = f"{settings.CONNECTOR_REDIRECT_BASE_URI}?provider=google_drive"
if not self.client_id or not self.client_secret:
raise ValueError("Google OAuth credentials not configured. Please set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in settings.")
def get_authorization_url(self, state: Optional[str] = None) -> str:
try:
flow = Flow.from_client_config(
{
"web": {
"client_id": self.client_id,
"client_secret": self.client_secret,
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"redirect_uris": [self.redirect_uri]
}
},
scopes=self.SCOPES
)
flow.redirect_uri = self.redirect_uri
authorization_url, _ = flow.authorization_url(
access_type='offline',
prompt='consent',
include_granted_scopes='true',
state=state
)
return authorization_url
except Exception as e:
logging.error(f"Error generating authorization URL: {e}")
raise
def exchange_code_for_tokens(self, authorization_code: str) -> Dict[str, Any]:
try:
if not authorization_code:
raise ValueError("Authorization code is required")
flow = Flow.from_client_config(
{
"web": {
"client_id": self.client_id,
"client_secret": self.client_secret,
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"redirect_uris": [self.redirect_uri]
}
},
scopes=self.SCOPES
)
flow.redirect_uri = self.redirect_uri
flow.fetch_token(code=authorization_code)
credentials = flow.credentials
if not credentials.refresh_token:
logging.warning("OAuth flow did not return a refresh_token.")
if not credentials.token:
raise ValueError("OAuth flow did not return an access token")
if not credentials.token_uri:
credentials.token_uri = "https://oauth2.googleapis.com/token"
if not credentials.client_id:
credentials.client_id = self.client_id
if not credentials.client_secret:
credentials.client_secret = self.client_secret
if not credentials.refresh_token:
raise ValueError(
"No refresh token received. This typically happens when offline access wasn't granted. "
)
return {
'access_token': credentials.token,
'refresh_token': credentials.refresh_token,
'token_uri': credentials.token_uri,
'client_id': credentials.client_id,
'client_secret': credentials.client_secret,
'scopes': credentials.scopes,
'expiry': credentials.expiry.isoformat() if credentials.expiry else None
}
except Exception as e:
logging.error(f"Error exchanging code for tokens: {e}")
raise
def refresh_access_token(self, refresh_token: str) -> Dict[str, Any]:
try:
if not refresh_token:
raise ValueError("Refresh token is required")
credentials = Credentials(
token=None,
refresh_token=refresh_token,
token_uri="https://oauth2.googleapis.com/token",
client_id=self.client_id,
client_secret=self.client_secret
)
from google.auth.transport.requests import Request
credentials.refresh(Request())
return {
'access_token': credentials.token,
'refresh_token': refresh_token,
'token_uri': credentials.token_uri,
'client_id': credentials.client_id,
'client_secret': credentials.client_secret,
'scopes': credentials.scopes,
'expiry': credentials.expiry.isoformat() if credentials.expiry else None
}
except Exception as e:
logging.error(f"Error refreshing access token: {e}", exc_info=True)
raise
def create_credentials_from_token_info(self, token_info: Dict[str, Any]) -> Credentials:
from application.core.settings import settings
access_token = token_info.get('access_token')
if not access_token:
raise ValueError("No access token found in token_info")
credentials = Credentials(
token=access_token,
refresh_token=token_info.get('refresh_token'),
token_uri= 'https://oauth2.googleapis.com/token',
client_id=settings.GOOGLE_CLIENT_ID,
client_secret=settings.GOOGLE_CLIENT_SECRET,
scopes=token_info.get('scopes', ['https://www.googleapis.com/auth/drive.readonly'])
)
if not credentials.token:
raise ValueError("Credentials created without valid access token")
return credentials
def build_drive_service(self, credentials: Credentials):
try:
if not credentials:
raise ValueError("No credentials provided")
if not credentials.token and not credentials.refresh_token:
raise ValueError("No access token or refresh token available. User must re-authorize with offline access.")
needs_refresh = credentials.expired or not credentials.token
if needs_refresh:
if credentials.refresh_token:
try:
from google.auth.transport.requests import Request
credentials.refresh(Request())
except Exception as refresh_error:
raise ValueError(f"Failed to refresh credentials: {refresh_error}")
else:
raise ValueError("No access token or refresh token available. User must re-authorize with offline access.")
return build('drive', 'v3', credentials=credentials)
except HttpError as e:
raise ValueError(f"Failed to build Google Drive service: HTTP {e.resp.status}")
except Exception as e:
raise ValueError(f"Failed to build Google Drive service: {str(e)}")
def is_token_expired(self, token_info):
if 'expiry' in token_info and token_info['expiry']:
try:
from dateutil import parser
# Google Drive provides timezone-aware ISO8601 dates
expiry_dt = parser.parse(token_info['expiry'])
current_time = datetime.datetime.now(datetime.timezone.utc)
return current_time >= expiry_dt - datetime.timedelta(seconds=60)
except Exception:
return True
if 'access_token' in token_info and token_info['access_token']:
return False
return True
def get_token_info_from_session(self, session_token: str) -> Dict[str, Any]:
try:
from application.core.mongo_db import MongoDB
from application.core.settings import settings
mongo = MongoDB.get_client()
db = mongo[settings.MONGO_DB_NAME]
sessions_collection = db["connector_sessions"]
session = sessions_collection.find_one({"session_token": session_token})
if not session:
raise ValueError(f"Invalid session token: {session_token}")
if "token_info" not in session:
raise ValueError("Session missing token information")
token_info = session["token_info"]
if not token_info:
raise ValueError("Invalid token information")
required_fields = ["access_token", "refresh_token"]
missing_fields = [field for field in required_fields if field not in token_info or not token_info.get(field)]
if missing_fields:
raise ValueError(f"Missing required token fields: {missing_fields}")
if 'client_id' not in token_info:
token_info['client_id'] = settings.GOOGLE_CLIENT_ID
if 'client_secret' not in token_info:
token_info['client_secret'] = settings.GOOGLE_CLIENT_SECRET
if 'token_uri' not in token_info:
token_info['token_uri'] = 'https://oauth2.googleapis.com/token'
return token_info
except Exception as e:
raise ValueError(f"Failed to retrieve Google Drive token information: {str(e)}")
def validate_credentials(self, credentials: Credentials) -> bool:
"""
Validate Google Drive credentials by making a test API call.
Args:
credentials: Google credentials object
Returns:
True if credentials are valid, False otherwise
"""
try:
service = self.build_drive_service(credentials)
service.about().get(fields="user").execute()
return True
except HttpError as e:
logging.error(f"HTTP error validating credentials: {e}")
return False
except Exception as e:
logging.error(f"Error validating credentials: {e}")
return False

View File

@@ -0,0 +1,536 @@
"""
Google Drive loader for DocsGPT.
Loads documents from Google Drive using Google Drive API.
"""
import io
import logging
import os
from typing import List, Dict, Any, Optional
from googleapiclient.http import MediaIoBaseDownload
from googleapiclient.errors import HttpError
from application.parser.connectors.base import BaseConnectorLoader
from application.parser.connectors.google_drive.auth import GoogleDriveAuth
from application.parser.schema.base import Document
class GoogleDriveLoader(BaseConnectorLoader):
SUPPORTED_MIME_TYPES = {
'application/pdf': '.pdf',
'application/vnd.google-apps.document': '.docx',
'application/vnd.google-apps.presentation': '.pptx',
'application/vnd.google-apps.spreadsheet': '.xlsx',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
'application/msword': '.doc',
'application/vnd.ms-powerpoint': '.ppt',
'application/vnd.ms-excel': '.xls',
'text/plain': '.txt',
'text/csv': '.csv',
'text/html': '.html',
'application/rtf': '.rtf',
'image/jpeg': '.jpg',
'image/jpg': '.jpg',
'image/png': '.png',
}
EXPORT_FORMATS = {
'application/vnd.google-apps.document': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.google-apps.presentation': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/vnd.google-apps.spreadsheet': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
}
def __init__(self, session_token: str):
self.auth = GoogleDriveAuth()
self.session_token = session_token
token_info = self.auth.get_token_info_from_session(session_token)
self.credentials = self.auth.create_credentials_from_token_info(token_info)
try:
self.service = self.auth.build_drive_service(self.credentials)
except Exception as e:
logging.warning(f"Could not build Google Drive service: {e}")
self.service = None
self.next_page_token = None
def _process_file(self, file_metadata: Dict[str, Any], load_content: bool = True) -> Optional[Document]:
try:
file_id = file_metadata.get('id')
file_name = file_metadata.get('name', 'Unknown')
mime_type = file_metadata.get('mimeType', 'application/octet-stream')
if mime_type not in self.SUPPORTED_MIME_TYPES and not mime_type.startswith('application/vnd.google-apps.'):
return None
if mime_type not in self.SUPPORTED_MIME_TYPES and not mime_type.startswith('application/vnd.google-apps.'):
logging.info(f"Skipping unsupported file type: {mime_type} for file {file_name}")
return None
# Google Drive provides timezone-aware ISO8601 dates
doc_metadata = {
'file_name': file_name,
'mime_type': mime_type,
'size': file_metadata.get('size', None),
'created_time': file_metadata.get('createdTime'),
'modified_time': file_metadata.get('modifiedTime'),
'parents': file_metadata.get('parents', []),
'source': 'google_drive'
}
if not load_content:
return Document(
text="",
doc_id=file_id,
extra_info=doc_metadata
)
content = self._download_file_content(file_id, mime_type)
if content is None:
logging.warning(f"Could not load content for file {file_name} ({file_id})")
return None
return Document(
text=content,
doc_id=file_id,
extra_info=doc_metadata
)
except Exception as e:
logging.error(f"Error processing file: {e}")
return None
def load_data(self, inputs: Dict[str, Any]) -> List[Document]:
session_token = inputs.get('session_token')
if session_token and session_token != self.session_token:
logging.warning("Session token in inputs differs from loader's session token. Using loader's session token.")
self.config = inputs
try:
documents: List[Document] = []
folder_id = inputs.get('folder_id')
file_ids = inputs.get('file_ids', [])
limit = inputs.get('limit', 100)
list_only = inputs.get('list_only', False)
load_content = not list_only
page_token = inputs.get('page_token')
self.next_page_token = None
if file_ids:
# Specific files requested: load them
for file_id in file_ids:
try:
doc = self._load_file_by_id(file_id, load_content=load_content)
if doc:
documents.append(doc)
elif hasattr(self, '_credential_refreshed') and self._credential_refreshed:
self._credential_refreshed = False
logging.info(f"Retrying load of file {file_id} after credential refresh")
doc = self._load_file_by_id(file_id, load_content=load_content)
if doc:
documents.append(doc)
except Exception as e:
logging.error(f"Error loading file {file_id}: {e}")
continue
else:
# Browsing mode: list immediate children of provided folder or root
parent_id = folder_id if folder_id else 'root'
documents = self._list_items_in_parent(parent_id, limit=limit, load_content=load_content, page_token=page_token)
logging.info(f"Loaded {len(documents)} documents from Google Drive")
return documents
except Exception as e:
logging.error(f"Error loading data from Google Drive: {e}", exc_info=True)
raise
def _load_file_by_id(self, file_id: str, load_content: bool = True) -> Optional[Document]:
self._ensure_service()
try:
file_metadata = self.service.files().get(
fileId=file_id,
fields='id,name,mimeType,size,createdTime,modifiedTime,parents'
).execute()
return self._process_file(file_metadata, load_content=load_content)
except HttpError as e:
logging.error(f"HTTP error loading file {file_id}: {e.resp.status} - {e.content}")
if e.resp.status in [401, 403]:
if hasattr(self.credentials, 'refresh_token') and self.credentials.refresh_token:
try:
from google.auth.transport.requests import Request
self.credentials.refresh(Request())
self._ensure_service()
return None
except Exception as refresh_error:
raise ValueError(f"Authentication failed and could not be refreshed: {refresh_error}")
else:
raise ValueError("Authentication failed and cannot be refreshed: missing refresh_token")
return None
except Exception as e:
logging.error(f"Error loading file {file_id}: {e}")
return None
def _list_items_in_parent(self, parent_id: str, limit: int = 100, load_content: bool = False, page_token: Optional[str] = None) -> List[Document]:
self._ensure_service()
documents: List[Document] = []
try:
query = f"'{parent_id}' in parents and trashed=false"
next_token_out: Optional[str] = None
while True:
page_size = 100
if limit:
remaining = max(0, limit - len(documents))
if remaining == 0:
break
page_size = min(100, remaining)
results = self.service.files().list(
q=query,
fields='nextPageToken,files(id,name,mimeType,size,createdTime,modifiedTime,parents)',
pageToken=page_token,
pageSize=page_size
).execute()
items = results.get('files', [])
for item in items:
mime_type = item.get('mimeType')
if mime_type == 'application/vnd.google-apps.folder':
doc_metadata = {
'file_name': item.get('name', 'Unknown'),
'mime_type': mime_type,
'size': item.get('size', None),
'created_time': item.get('createdTime'),
'modified_time': item.get('modifiedTime'),
'parents': item.get('parents', []),
'source': 'google_drive',
'is_folder': True
}
documents.append(Document(text="", doc_id=item.get('id'), extra_info=doc_metadata))
else:
doc = self._process_file(item, load_content=load_content)
if doc:
documents.append(doc)
if limit and len(documents) >= limit:
self.next_page_token = results.get('nextPageToken')
return documents
page_token = results.get('nextPageToken')
next_token_out = page_token
if not page_token:
break
self.next_page_token = next_token_out
return documents
except Exception as e:
logging.error(f"Error listing items under parent {parent_id}: {e}")
return documents
def _download_file_content(self, file_id: str, mime_type: str) -> Optional[str]:
if not self.credentials.token:
logging.warning("No access token in credentials, attempting to refresh")
if hasattr(self.credentials, 'refresh_token') and self.credentials.refresh_token:
try:
from google.auth.transport.requests import Request
self.credentials.refresh(Request())
logging.info("Credentials refreshed successfully")
self._ensure_service()
except Exception as e:
logging.error(f"Failed to refresh credentials: {e}")
raise ValueError("Authentication failed and cannot be refreshed: missing or invalid refresh_token")
else:
logging.error("No access token and no refresh_token available")
raise ValueError("Authentication failed and cannot be refreshed: missing refresh_token")
if self.credentials.expired:
logging.warning("Credentials are expired, attempting to refresh")
if hasattr(self.credentials, 'refresh_token') and self.credentials.refresh_token:
try:
from google.auth.transport.requests import Request
self.credentials.refresh(Request())
logging.info("Credentials refreshed successfully")
self._ensure_service()
except Exception as e:
logging.error(f"Failed to refresh expired credentials: {e}")
raise ValueError("Authentication failed and cannot be refreshed: expired credentials")
else:
logging.error("Credentials expired and no refresh_token available")
raise ValueError("Authentication failed and cannot be refreshed: missing refresh_token")
try:
if mime_type in self.EXPORT_FORMATS:
export_mime_type = self.EXPORT_FORMATS[mime_type]
request = self.service.files().export_media(
fileId=file_id,
mimeType=export_mime_type
)
else:
request = self.service.files().get_media(fileId=file_id)
file_io = io.BytesIO()
downloader = MediaIoBaseDownload(file_io, request)
done = False
while done is False:
try:
_, done = downloader.next_chunk()
except HttpError as e:
logging.error(f"HTTP error downloading file {file_id}: {e.resp.status} - {e.content}")
return None
except Exception as e:
logging.error(f"Error during download of file {file_id}: {e}")
return None
content_bytes = file_io.getvalue()
try:
content = content_bytes.decode('utf-8')
except UnicodeDecodeError:
try:
content = content_bytes.decode('latin-1')
except UnicodeDecodeError:
logging.error(f"Could not decode file {file_id} as text")
return None
return content
except HttpError as e:
logging.error(f"HTTP error downloading file {file_id}: {e.resp.status} - {e.content}")
if e.resp.status in [401, 403]:
logging.error(f"Authentication error downloading file {file_id}")
if hasattr(self.credentials, 'refresh_token') and self.credentials.refresh_token:
logging.info(f"Attempting to refresh credentials for file {file_id}")
try:
from google.auth.transport.requests import Request
self.credentials.refresh(Request())
logging.info("Credentials refreshed successfully")
self._credential_refreshed = True
self._ensure_service()
return None
except Exception as refresh_error:
logging.error(f"Error refreshing credentials: {refresh_error}")
raise ValueError(f"Authentication failed and could not be refreshed: {refresh_error}")
else:
logging.error("Cannot refresh credentials: missing refresh_token")
raise ValueError("Authentication failed and cannot be refreshed: missing refresh_token")
return None
except Exception as e:
logging.error(f"Error downloading file {file_id}: {e}")
return None
def _download_file_to_directory(self, file_id: str, local_dir: str) -> bool:
try:
self._ensure_service()
return self._download_single_file(file_id, local_dir)
except Exception as e:
logging.error(f"Error downloading file {file_id}: {e}", exc_info=True)
return False
def _ensure_service(self):
if not self.service:
try:
self.service = self.auth.build_drive_service(self.credentials)
except Exception as e:
raise ValueError(f"Cannot access Google Drive: {e}")
def _download_single_file(self, file_id: str, local_dir: str) -> bool:
file_metadata = self.service.files().get(
fileId=file_id,
fields='name,mimeType'
).execute()
file_name = file_metadata['name']
mime_type = file_metadata['mimeType']
if mime_type not in self.SUPPORTED_MIME_TYPES and not mime_type.startswith('application/vnd.google-apps.'):
return False
os.makedirs(local_dir, exist_ok=True)
full_path = os.path.join(local_dir, file_name)
if mime_type in self.EXPORT_FORMATS:
export_mime_type = self.EXPORT_FORMATS[mime_type]
request = self.service.files().export_media(
fileId=file_id,
mimeType=export_mime_type
)
extension = self._get_extension_for_mime_type(export_mime_type)
if not full_path.endswith(extension):
full_path += extension
else:
request = self.service.files().get_media(fileId=file_id)
with open(full_path, 'wb') as f:
downloader = MediaIoBaseDownload(f, request)
done = False
while not done:
_, done = downloader.next_chunk()
return True
def _download_folder_recursive(self, folder_id: str, local_dir: str, recursive: bool = True) -> int:
files_downloaded = 0
try:
os.makedirs(local_dir, exist_ok=True)
query = f"'{folder_id}' in parents and trashed=false"
page_token = None
while True:
results = self.service.files().list(
q=query,
fields='nextPageToken, files(id, name, mimeType)',
pageToken=page_token,
pageSize=1000
).execute()
items = results.get('files', [])
logging.info(f"Found {len(items)} items in folder {folder_id}")
for item in items:
item_name = item['name']
item_id = item['id']
mime_type = item['mimeType']
if mime_type == 'application/vnd.google-apps.folder':
if recursive:
# Create subfolder and recurse
subfolder_path = os.path.join(local_dir, item_name)
os.makedirs(subfolder_path, exist_ok=True)
subfolder_files = self._download_folder_recursive(
item_id,
subfolder_path,
recursive
)
files_downloaded += subfolder_files
logging.info(f"Downloaded {subfolder_files} files from subfolder {item_name}")
else:
# Download file
success = self._download_single_file(item_id, local_dir)
if success:
files_downloaded += 1
logging.info(f"Downloaded file: {item_name}")
else:
logging.warning(f"Failed to download file: {item_name}")
page_token = results.get('nextPageToken')
if not page_token:
break
return files_downloaded
except Exception as e:
logging.error(f"Error in _download_folder_recursive for folder {folder_id}: {e}", exc_info=True)
return files_downloaded
def _get_extension_for_mime_type(self, mime_type: str) -> str:
extensions = {
'application/pdf': '.pdf',
'text/plain': '.txt',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx',
'text/html': '.html',
'text/markdown': '.md',
}
return extensions.get(mime_type, '.bin')
def _download_folder_contents(self, folder_id: str, local_dir: str, recursive: bool = True) -> int:
try:
self._ensure_service()
return self._download_folder_recursive(folder_id, local_dir, recursive)
except Exception as e:
logging.error(f"Error downloading folder {folder_id}: {e}", exc_info=True)
return 0
def download_to_directory(self, local_dir: str, source_config: dict = None) -> dict:
if source_config is None:
source_config = {}
config = source_config if source_config else getattr(self, 'config', {})
files_downloaded = 0
try:
folder_ids = config.get('folder_ids', [])
file_ids = config.get('file_ids', [])
recursive = config.get('recursive', True)
self._ensure_service()
if file_ids:
if isinstance(file_ids, str):
file_ids = [file_ids]
for file_id in file_ids:
if self._download_file_to_directory(file_id, local_dir):
files_downloaded += 1
# Process folders
if folder_ids:
if isinstance(folder_ids, str):
folder_ids = [folder_ids]
for folder_id in folder_ids:
try:
folder_metadata = self.service.files().get(
fileId=folder_id,
fields='name'
).execute()
folder_name = folder_metadata.get('name', '')
folder_path = os.path.join(local_dir, folder_name)
os.makedirs(folder_path, exist_ok=True)
folder_files = self._download_folder_recursive(
folder_id,
folder_path,
recursive
)
files_downloaded += folder_files
logging.info(f"Downloaded {folder_files} files from folder {folder_name}")
except Exception as e:
logging.error(f"Error downloading folder {folder_id}: {e}", exc_info=True)
if not file_ids and not folder_ids:
raise ValueError("No folder_ids or file_ids provided for download")
return {
"files_downloaded": files_downloaded,
"directory_path": local_dir,
"empty_result": files_downloaded == 0,
"source_type": "google_drive",
"config_used": config
}
except Exception as e:
return {
"files_downloaded": files_downloaded,
"directory_path": local_dir,
"empty_result": True,
"source_type": "google_drive",
"config_used": config,
"error": str(e)
}

View File

@@ -6,6 +6,21 @@ from application.core.settings import settings
from application.vectorstore.vector_creator import VectorCreator
def sanitize_content(content: str) -> str:
"""
Remove NUL characters that can cause vector store ingestion to fail.
Args:
content (str): Raw content that may contain NUL characters
Returns:
str: Sanitized content with NUL characters removed
"""
if not content:
return content
return content.replace('\x00', '')
@retry(tries=10, delay=60)
def add_text_to_store_with_retry(store, doc, source_id):
"""
@@ -16,10 +31,13 @@ def add_text_to_store_with_retry(store, doc, source_id):
source_id: Unique identifier for the source.
"""
try:
# Sanitize content to remove NUL characters that cause ingestion failures
doc.page_content = sanitize_content(doc.page_content)
doc.metadata["source_id"] = str(source_id)
store.add_texts([doc.page_content], metadatas=[doc.metadata])
except Exception as e:
logging.error(f"Failed to add document with retry: {e}")
logging.error(f"Failed to add document with retry: {e}", exc_info=True)
raise
@@ -46,7 +64,7 @@ def embed_and_store_documents(docs, folder_name, source_id, task_status):
store = VectorCreator.create_vectorstore(
settings.VECTOR_STORE,
docs_init=docs_init,
source_id=folder_name,
source_id=source_id,
embeddings_key=os.getenv("EMBEDDINGS_KEY"),
)
else:
@@ -75,7 +93,7 @@ def embed_and_store_documents(docs, folder_name, source_id, task_status):
# Add document to vector store
add_text_to_store_with_retry(store, doc, source_id)
except Exception as e:
logging.error(f"Error embedding document {idx}: {e}")
logging.error(f"Error embedding document {idx}: {e}", exc_info=True)
logging.info(f"Saving progress at document {idx} out of {total_docs}")
store.save_local(folder_name)
break

View File

@@ -15,6 +15,7 @@ from application.parser.file.json_parser import JSONParser
from application.parser.file.pptx_parser import PPTXParser
from application.parser.file.image_parser import ImageParser
from application.parser.schema.base import Document
from application.utils import num_tokens_from_string
DEFAULT_FILE_EXTRACTOR: Dict[str, BaseParser] = {
".pdf": PDFParser(),
@@ -141,11 +142,12 @@ class SimpleDirectoryReader(BaseReader):
Returns:
List[Document]: A list of documents.
"""
data: Union[str, List[str]] = ""
data_list: List[str] = []
metadata_list = []
self.file_token_counts = {}
for input_file in self.input_files:
if input_file.suffix in self.file_extractor:
parser = self.file_extractor[input_file.suffix]
@@ -156,24 +158,48 @@ class SimpleDirectoryReader(BaseReader):
# do standard read
with open(input_file, "r", errors=self.errors) as f:
data = f.read()
# Prepare metadata for this file
if self.file_metadata is not None:
file_metadata = self.file_metadata(str(input_file))
# Calculate token count for this file
if isinstance(data, List):
file_tokens = sum(num_tokens_from_string(str(d)) for d in data)
else:
# Provide a default empty metadata
file_metadata = {'title': '', 'store': ''}
# TODO: Find a case with no metadata and check if breaks anything
file_tokens = num_tokens_from_string(str(data))
full_path = str(input_file.resolve())
self.file_token_counts[full_path] = file_tokens
base_metadata = {
'title': input_file.name,
'token_count': file_tokens,
}
if hasattr(self, 'input_dir'):
try:
relative_path = str(input_file.relative_to(self.input_dir))
base_metadata['source'] = relative_path
except ValueError:
base_metadata['source'] = str(input_file)
else:
base_metadata['source'] = str(input_file)
if self.file_metadata is not None:
custom_metadata = self.file_metadata(input_file.name)
base_metadata.update(custom_metadata)
if isinstance(data, List):
# Extend data_list with each item in the data list
data_list.extend([str(d) for d in data])
# For each item in the data list, add the file's metadata to metadata_list
metadata_list.extend([file_metadata for _ in data])
metadata_list.extend([base_metadata for _ in data])
else:
# Add the single piece of data to data_list
data_list.append(str(data))
# Add the file's metadata to metadata_list
metadata_list.append(file_metadata)
metadata_list.append(base_metadata)
# Build directory structure if input_dir is provided
if hasattr(self, 'input_dir'):
self.directory_structure = self.build_directory_structure(self.input_dir)
logging.info("Directory structure built successfully")
else:
self.directory_structure = {}
if concatenate:
return [Document("\n".join(data_list))]
@@ -181,3 +207,48 @@ class SimpleDirectoryReader(BaseReader):
return [Document(d, extra_info=m) for d, m in zip(data_list, metadata_list)]
else:
return [Document(d) for d in data_list]
def build_directory_structure(self, base_path):
"""Build a dictionary representing the directory structure.
Args:
base_path: The base path to start building the structure from.
Returns:
dict: A nested dictionary representing the directory structure.
"""
import mimetypes
def build_tree(path):
"""Helper function to recursively build the directory tree."""
result = {}
for item in path.iterdir():
if self.exclude_hidden and item.name.startswith('.'):
continue
if item.is_dir():
subtree = build_tree(item)
if subtree:
result[item.name] = subtree
else:
if self.required_exts is not None and item.suffix not in self.required_exts:
continue
full_path = str(item.resolve())
file_size_bytes = item.stat().st_size
mime_type = mimetypes.guess_type(item.name)[0] or "application/octet-stream"
file_info = {
"type": mime_type,
"size_bytes": file_size_bytes
}
if hasattr(self, 'file_token_counts') and full_path in self.file_token_counts:
file_info["token_count"] = self.file_token_counts[full_path]
result[item.name] = file_info
return result
return build_tree(Path(base_path))

View File

@@ -24,26 +24,27 @@ class PDFParser(BaseParser):
# alternatively you can use local vision capable LLM
with open(file, "rb") as file_loaded:
files = {'file': file_loaded}
response = requests.post(doc2md_service, files=files)
data = response.json()["markdown"]
response = requests.post(doc2md_service, files=files)
data = response.json()["markdown"]
return data
try:
import PyPDF2
from pypdf import PdfReader
except ImportError:
raise ValueError("PyPDF2 is required to read PDF files.")
raise ValueError("pypdf is required to read PDF files.")
text_list = []
with open(file, "rb") as fp:
# Create a PDF object
pdf = PyPDF2.PdfReader(fp)
pdf = PdfReader(fp)
# Get the number of pages in the PDF document
num_pages = len(pdf.pages)
# Iterate over every page
for page in range(num_pages):
for page_index in range(num_pages):
# Extract the text from the page
page_text = pdf.pages[page].extract_text()
page = pdf.pages[page_index]
page_text = page.extract_text()
text_list.append(page_text)
text = "\n".join(text_list)
@@ -66,4 +67,4 @@ class DocxParser(BaseParser):
text = docx2txt.process(file)
return text
return text

View File

@@ -8,6 +8,7 @@ import requests
from typing import Dict, Union
from application.parser.file.base_parser import BaseParser
from application.core.settings import settings
class ImageParser(BaseParser):
@@ -18,10 +19,13 @@ class ImageParser(BaseParser):
return {}
def parse_file(self, file: Path, errors: str = "ignore") -> Union[str, list[str]]:
doc2md_service = "https://llm.arc53.com/doc2md"
# alternatively you can use local vision capable LLM
with open(file, "rb") as file_loaded:
files = {'file': file_loaded}
response = requests.post(doc2md_service, files=files)
data = response.json()["markdown"]
if settings.PARSE_IMAGE_REMOTE:
doc2md_service = "https://llm.arc53.com/doc2md"
# alternatively you can use local vision capable LLM
with open(file, "rb") as file_loaded:
files = {'file': file_loaded}
response = requests.post(doc2md_service, files=files)
data = response.json()["markdown"]
else:
data = ""
return data

View File

@@ -73,7 +73,13 @@ class PandasCSVParser(BaseParser):
for more information.
Set to empty dict by default, this means pandas will try to figure
out the separators, table head, etc. on its own.
header_period (int): Controls how headers are included in output:
- 0: Headers only at the beginning
- 1: Headers in every row
- N > 1: Headers every N rows
header_prefix (str): Prefix for header rows. Default is "HEADERS: ".
"""
def __init__(
@@ -83,6 +89,8 @@ class PandasCSVParser(BaseParser):
col_joiner: str = ", ",
row_joiner: str = "\n",
pandas_config: dict = {},
header_period: int = 20,
header_prefix: str = "HEADERS: ",
**kwargs: Any
) -> None:
"""Init params."""
@@ -91,6 +99,8 @@ class PandasCSVParser(BaseParser):
self._col_joiner = col_joiner
self._row_joiner = row_joiner
self._pandas_config = pandas_config
self._header_period = header_period
self._header_prefix = header_prefix
def _init_parser(self) -> Dict:
"""Init parser."""
@@ -104,15 +114,26 @@ class PandasCSVParser(BaseParser):
raise ValueError("pandas module is required to read CSV files.")
df = pd.read_csv(file, **self._pandas_config)
headers = df.columns.tolist()
header_row = f"{self._header_prefix}{self._col_joiner.join(headers)}"
text_list = df.apply(
lambda row: (self._col_joiner).join(row.astype(str).tolist()), axis=1
).tolist()
if not self._concat_rows:
return df.apply(
lambda row: (self._col_joiner).join(row.astype(str).tolist()), axis=1
).tolist()
text_list = []
if self._header_period != 1:
text_list.append(header_row)
for i, row in df.iterrows():
if (self._header_period > 1 and i > 0 and i % self._header_period == 0):
text_list.append(header_row)
text_list.append(self._col_joiner.join(row.astype(str).tolist()))
if self._header_period == 1 and i < len(df) - 1:
text_list.append(header_row)
if self._concat_rows:
return (self._row_joiner).join(text_list)
else:
return text_list
return self._row_joiner.join(text_list)
class ExcelParser(BaseParser):
@@ -138,7 +159,13 @@ class ExcelParser(BaseParser):
for more information.
Set to empty dict by default, this means pandas will try to figure
out the table structure on its own.
header_period (int): Controls how headers are included in output:
- 0: Headers only at the beginning (default)
- 1: Headers in every row
- N > 1: Headers every N rows
header_prefix (str): Prefix for header rows. Default is "HEADERS: ".
"""
def __init__(
@@ -148,6 +175,8 @@ class ExcelParser(BaseParser):
col_joiner: str = ", ",
row_joiner: str = "\n",
pandas_config: dict = {},
header_period: int = 20,
header_prefix: str = "HEADERS: ",
**kwargs: Any
) -> None:
"""Init params."""
@@ -156,6 +185,8 @@ class ExcelParser(BaseParser):
self._col_joiner = col_joiner
self._row_joiner = row_joiner
self._pandas_config = pandas_config
self._header_period = header_period
self._header_prefix = header_prefix
def _init_parser(self) -> Dict:
"""Init parser."""
@@ -169,12 +200,22 @@ class ExcelParser(BaseParser):
raise ValueError("pandas module is required to read Excel files.")
df = pd.read_excel(file, **self._pandas_config)
headers = df.columns.tolist()
header_row = f"{self._header_prefix}{self._col_joiner.join(headers)}"
if not self._concat_rows:
return df.apply(
lambda row: (self._col_joiner).join(row.astype(str).tolist()), axis=1
).tolist()
text_list = []
if self._header_period != 1:
text_list.append(header_row)
text_list = df.apply(
lambda row: (self._col_joiner).join(row.astype(str).tolist()), axis=1
).tolist()
if self._concat_rows:
return (self._row_joiner).join(text_list)
else:
return text_list
for i, row in df.iterrows():
if (self._header_period > 1 and i > 0 and i % self._header_period == 0):
text_list.append(header_row)
text_list.append(self._col_joiner.join(row.astype(str).tolist()))
if self._header_period == 1 and i < len(df) - 1:
text_list.append(header_row)
return self._row_joiner.join(text_list)

View File

@@ -1,3 +1,4 @@
import logging
import requests
from urllib.parse import urlparse, urljoin
from bs4 import BeautifulSoup
@@ -42,7 +43,7 @@ class CrawlerLoader(BaseRemote):
)
)
except Exception as e:
print(f"Error processing URL {current_url}: {e}")
logging.error(f"Error processing URL {current_url}: {e}", exc_info=True)
continue
# Parse the HTML content to extract all links
@@ -61,4 +62,4 @@ class CrawlerLoader(BaseRemote):
if self.limit is not None and len(visited_urls) >= self.limit:
break
return loaded_content
return loaded_content

View File

@@ -6,6 +6,16 @@ from application.parser.remote.github_loader import GitHubLoader
class RemoteCreator:
"""
Factory class for creating remote content loaders.
These loaders fetch content from remote web sources like URLs,
sitemaps, web crawlers, social media platforms, etc.
For external knowledge base connectors (like Google Drive),
use ConnectorCreator instead.
"""
loaders = {
"url": WebLoader,
"sitemap": SitemapLoader,
@@ -18,5 +28,5 @@ class RemoteCreator:
def create_loader(cls, type, *args, **kwargs):
loader_class = cls.loaders.get(type.lower())
if not loader_class:
raise ValueError(f"No LLM class found for type {type}")
raise ValueError(f"No loader class found for type {type}")
return loader_class(*args, **kwargs)

View File

@@ -1,3 +1,4 @@
import logging
import requests
import re # Import regular expression library
import xml.etree.ElementTree as ET
@@ -32,7 +33,7 @@ class SitemapLoader(BaseRemote):
documents.extend(loader.load())
processed_urls += 1 # Increment the counter after processing each URL
except Exception as e:
print(f"Error processing URL {url}: {e}")
logging.error(f"Error processing URL {url}: {e}", exc_info=True)
continue
return documents

View File

@@ -1,3 +1,4 @@
import logging
from application.parser.remote.base import BaseRemote
from application.parser.schema.base import Document
from langchain_community.document_loaders import WebBaseLoader
@@ -39,6 +40,6 @@ class WebLoader(BaseRemote):
)
)
except Exception as e:
print(f"Error processing URL {url}: {e}")
logging.error(f"Error processing URL {url}: {e}", exc_info=True)
continue
return documents
return documents

View File

@@ -1,9 +1,15 @@
You are a DocsGPT, friendly and helpful AI assistant by Arc53 that provides help with documents. You give thorough answers with code examples if possible.
Use the following pieces of context to help answer the users question. If its not relevant to the question, provide friendly responses.
You have access to chat history, and can use it to help answer the question.
When using code examples, use the following format:
You are a helpful AI assistant, DocsGPT. You are proactive and helpful. Try to use tools, if they are available to you,
be proactive and fill in missing information.
Users can Upload documents for your context as attachments or sources via UI using the Conversation input box.
If appropriate, your answers can include code examples, formatted as follows:
```(language)
(code)
```
Users are also able to see charts and diagrams if you use them with valid mermaid syntax in your responses.
Try to respond with mermaid charts if visualization helps with users queries.
You effectively utilize chat history, ensuring relevant and tailored responses.
Try to use additional provided context if it's available, otherwise use your knowledge and tool capabilities.
Allow yourself to be very creative and use your imagination.
----------------
Possible additional context from uploaded sources:
{summaries}

View File

@@ -1,9 +1,14 @@
You are a helpful AI assistant, DocsGPT, specializing in document assistance, designed to offer detailed and informative responses.
You are a helpful AI assistant, DocsGPT. You are proactive and helpful. Try to use tools, if they are available to you,
be proactive and fill in missing information.
Users can Upload documents for your context as attachments or sources via UI using the Conversation input box.
If appropriate, your answers can include code examples, formatted as follows:
```(language)
(code)
```
Users are also able to see charts and diagrams if you use them with valid mermaid syntax in your responses.
Try to respond with mermaid charts if visualization helps with users queries.
You effectively utilize chat history, ensuring relevant and tailored responses.
If a question doesn't align with your context, you provide friendly and helpful replies.
Try to use additional provided context if it's available, otherwise use your knowledge and tool capabilities.
----------------
Possible additional context from uploaded sources:
{summaries}

View File

@@ -1,13 +1,17 @@
You are an AI Assistant, DocsGPT, adept at offering document assistance.
Your expertise lies in providing answer on top of provided context.
You can leverage the chat history if needed.
Answer the question based on the context below.
Keep the answer concise. Respond "Irrelevant context" if not sure about the answer.
If question is not related to the context, respond "Irrelevant context".
When using code examples, use the following format:
You are a helpful AI assistant, DocsGPT. You are proactive and helpful. Try to use tools, if they are available to you,
be proactive and fill in missing information.
Users can Upload documents for your context as attachments or sources via UI using the Conversation input box.
If appropriate, your answers can include code examples, formatted as follows:
```(language)
(code)
```
----------------
Context:
{summaries}
Users are also able to see charts and diagrams if you use them with valid mermaid syntax in your responses.
Try to respond with mermaid charts if visualization helps with users queries.
You effectively utilize chat history, ensuring relevant and tailored responses.
Use context provided below or use available tools tool capabilities to answer user queries.
If you dont have enough information from the context or tools, answer "I don't know" or "I don't have enough information".
Never make up information or provide false information!
Allow yourself to be very creative and use your imagination.
----------------
Context from uploaded sources:
{summaries}

View File

@@ -0,0 +1,3 @@
Query: {query}
Observations: {observations}
Now, using the insights from the observations, formulate a well-structured and precise final answer.

View File

@@ -0,0 +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 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}
Prompt: {prompt}
Observations(potentially previous tool calls): {observations}

View File

@@ -1,87 +1,80 @@
anthropic==0.40.0
boto3==1.35.97
beautifulsoup4==4.12.3
anthropic==0.49.0
boto3==1.38.18
beautifulsoup4==4.13.4
celery==5.4.0
dataclasses-json==0.6.7
docx2txt==0.8
duckduckgo-search==6.3.0
duckduckgo-search==7.5.2
ebooklib==0.18
elastic-transport==8.17.0
elasticsearch==8.17.0
escodegen==1.0.11
esprima==4.0.1
esutils==1.0.1
Flask==3.1.0
Flask==3.1.1
faiss-cpu==1.9.0.post1
flask-restx==1.3.0
google-genai==0.5.0
google-generativeai==0.8.3
google-genai==1.3.0
google-api-python-client==2.179.0
google-auth-httplib2==0.2.0
google-auth-oauthlib==1.2.2
gTTS==2.5.4
gunicorn==23.0.0
html2text==2024.2.26
javalang==0.13.0
jinja2==3.1.5
jinja2==3.1.6
jiter==0.8.2
jmespath==1.0.1
joblib==1.4.2
jsonpatch==1.33
jsonpointer==3.0.0
jsonschema==4.23.0
jsonschema-spec==0.2.4
jsonschema-specifications==2023.7.1
kombu==5.4.2
langchain==0.3.14
langchain-community==0.3.14
langchain-core==0.3.29
langchain-openai==0.3.0
langchain-text-splitters==0.3.5
langsmith==0.2.10
langchain==0.3.20
langchain-community==0.3.19
langchain-core==0.3.59
langchain-openai==0.3.16
langchain-text-splitters==0.3.8
langsmith==0.3.42
lazy-object-proxy==1.10.0
lxml==5.3.0
lxml==5.3.1
markupsafe==3.0.2
marshmallow==3.24.1
marshmallow==3.26.1
mpmath==1.3.0
multidict==6.1.0
multidict==6.4.3
mypy-extensions==1.0.0
networkx==3.4.2
numpy==2.2.1
openai==1.59.5
openapi-schema-validator==0.6.2
openapi-spec-validator==0.6.0
openapi3-parser==1.1.19
openai==1.78.1
openapi3-parser==1.1.21
orjson==3.10.14
packaging==24.1
packaging==24.2
pandas==2.2.3
openpyxl==3.1.5
pathable==0.4.4
pillow==11.1.0
portalocker==2.10.1
portalocker>=2.7.0,<3.0.0
prance==23.6.21.0
primp==0.10.0
prompt-toolkit==3.0.48
prompt-toolkit==3.0.51
protobuf==5.29.3
psycopg2-binary==2.9.10
py==1.11.0
pydantic==2.10.4
pydantic==2.10.6
pydantic-core==2.27.2
pydantic-settings==2.7.1
pymongo==4.10.1
pypdf2==3.0.1
pymongo==4.11.3
pypdf==5.5.0
python-dateutil==2.9.0.post0
python-dotenv==1.0.1
python-jose==3.4.0
python-pptx==1.0.2
qdrant-client==1.12.2
redis==5.2.1
referencing==0.30.2
referencing>=0.28.0,<0.31.0
regex==2024.11.6
requests==2.32.3
retry==0.9.2
sentence-transformers==3.3.1
tiktoken==0.8.0
tokenizers==0.21.0
torch==2.5.1
torch==2.7.0
tqdm==4.67.1
transformers==4.48.0
transformers==4.51.3
typing-extensions==4.12.2
typing-inspect==0.9.0
tzdata==2024.2
@@ -89,7 +82,7 @@ urllib3==2.3.0
vine==5.1.0
wcwidth==0.2.13
werkzeug==3.1.3
yarl==1.18.3
markdownify==0.14.1
yarl==1.20.0
markdownify==1.1.0
tldextract==5.1.3
websockets==14.1

View File

@@ -1,104 +0,0 @@
import json
from application.retriever.base import BaseRetriever
from application.core.settings import settings
from application.llm.llm_creator import LLMCreator
from langchain_community.tools import BraveSearch
class BraveRetSearch(BaseRetriever):
def __init__(
self,
question,
source,
chat_history,
prompt,
chunks=2,
token_limit=150,
gpt_model="docsgpt",
user_api_key=None,
):
self.question = question
self.source = source
self.chat_history = chat_history
self.prompt = prompt
self.chunks = chunks
self.gpt_model = gpt_model
self.token_limit = (
token_limit
if token_limit
< settings.MODEL_TOKEN_LIMITS.get(
self.gpt_model, settings.DEFAULT_MAX_HISTORY
)
else settings.MODEL_TOKEN_LIMITS.get(
self.gpt_model, settings.DEFAULT_MAX_HISTORY
)
)
self.user_api_key = user_api_key
def _get_data(self):
if self.chunks == 0:
docs = []
else:
search = BraveSearch.from_api_key(
api_key=settings.BRAVE_SEARCH_API_KEY,
search_kwargs={"count": int(self.chunks)},
)
results = search.run(self.question)
results = json.loads(results)
docs = []
for i in results:
try:
title = i["title"]
link = i["link"]
snippet = i["snippet"]
docs.append({"text": snippet, "title": title, "link": link})
except IndexError:
pass
if settings.LLM_NAME == "llama.cpp":
docs = [docs[0]]
return docs
def gen(self):
docs = self._get_data()
# join all page_content together with a newline
docs_together = "\n".join([doc["text"] for doc in docs])
p_chat_combine = self.prompt.replace("{summaries}", docs_together)
messages_combine = [{"role": "system", "content": p_chat_combine}]
for doc in docs:
yield {"source": doc}
if len(self.chat_history) > 0:
for i in self.chat_history:
if "prompt" in i and "response" in i:
messages_combine.append({"role": "user", "content": i["prompt"]})
messages_combine.append(
{"role": "assistant", "content": i["response"]}
)
messages_combine.append({"role": "user", "content": self.question})
llm = LLMCreator.create_llm(
settings.LLM_NAME, api_key=settings.API_KEY, user_api_key=self.user_api_key
)
completion = llm.gen_stream(model=self.gpt_model, messages=messages_combine)
for line in completion:
yield {"answer": str(line)}
def search(self):
return self._get_data()
def get_params(self):
return {
"question": self.question,
"source": self.source,
"chat_history": self.chat_history,
"prompt": self.prompt,
"chunks": self.chunks,
"token_limit": self.token_limit,
"gpt_model": self.gpt_model,
"user_api_key": self.user_api_key
}

View File

@@ -1,43 +1,85 @@
import logging
from application.core.settings import settings
from application.llm.llm_creator import LLMCreator
from application.retriever.base import BaseRetriever
from application.tools.agent import Agent
from application.vectorstore.vector_creator import VectorCreator
class ClassicRAG(BaseRetriever):
def __init__(
self,
question,
source,
chat_history,
prompt,
chat_history=None,
prompt="",
chunks=2,
token_limit=150,
gpt_model="docsgpt",
user_api_key=None,
llm_name=settings.LLM_PROVIDER,
api_key=settings.API_KEY,
decoded_token=None,
):
self.question = question
self.vectorstore = source["active_docs"] if "active_docs" in source else None
self.chat_history = chat_history
self.original_question = ""
self.chat_history = chat_history if chat_history is not None else []
self.prompt = prompt
self.chunks = chunks
self.gpt_model = gpt_model
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
)
)
self.user_api_key = user_api_key
self.llm_name = llm_name
self.api_key = api_key
self.llm = LLMCreator.create_llm(
self.llm_name,
api_key=self.api_key,
user_api_key=self.user_api_key,
decoded_token=decoded_token,
)
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):
if (
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
prompt = f"""Given the following conversation history:
{self.chat_history}
Rephrase the following user question to be a standalone search query
that captures all relevant context from the conversation:
"""
messages = [
{"role": "system", "content": prompt},
{"role": "user", "content": self.original_question},
]
try:
rephrased_query = self.llm.gen(model=self.gpt_model, messages=messages)
print(f"Rephrased query: {rephrased_query}")
return rephrased_query if rephrased_query else self.original_question
except Exception as e:
logging.error(f"Error rephrasing query: {e}", exc_info=True)
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(
@@ -61,47 +103,20 @@ class ClassicRAG(BaseRetriever):
return docs
def gen(self):
docs = self._get_data()
def gen():
pass
# join all page_content together with a newline
docs_together = "\n".join([doc["text"] for doc in docs])
p_chat_combine = self.prompt.replace("{summaries}", docs_together)
messages_combine = [{"role": "system", "content": p_chat_combine}]
for doc in docs:
yield {"source": doc}
if len(self.chat_history) > 0:
for i in self.chat_history:
if "prompt" in i and "response" in i:
messages_combine.append({"role": "user", "content": i["prompt"]})
messages_combine.append(
{"role": "assistant", "content": i["response"]}
)
messages_combine.append({"role": "user", "content": self.question})
# llm = LLMCreator.create_llm(
# settings.LLM_NAME, api_key=settings.API_KEY, user_api_key=self.user_api_key
# )
# completion = llm.gen_stream(model=self.gpt_model, messages=messages_combine)
agent = Agent(
llm_name=settings.LLM_NAME,
gpt_model=self.gpt_model,
api_key=settings.API_KEY,
user_api_key=self.user_api_key,
)
completion = agent.gen(messages_combine)
for line in completion:
yield {"answer": str(line)}
def search(self):
def search(self, query: str = ""):
if query:
self.original_question = query
self.question = self._rephrase_query()
return self._get_data()
def get_params(self):
return {
"question": self.question,
"question": self.original_question,
"rephrased_question": self.question,
"source": self.vectorstore,
"chat_history": self.chat_history,
"prompt": self.prompt,
"chunks": self.chunks,
"token_limit": self.token_limit,
"gpt_model": self.gpt_model,

View File

@@ -1,121 +0,0 @@
from application.retriever.base import BaseRetriever
from application.core.settings import settings
from application.llm.llm_creator import LLMCreator
from langchain_community.tools import DuckDuckGoSearchResults
from langchain_community.utilities import DuckDuckGoSearchAPIWrapper
class DuckDuckSearch(BaseRetriever):
def __init__(
self,
question,
source,
chat_history,
prompt,
chunks=2,
token_limit=150,
gpt_model="docsgpt",
user_api_key=None,
):
self.question = question
self.source = source
self.chat_history = chat_history
self.prompt = prompt
self.chunks = chunks
self.gpt_model = gpt_model
self.token_limit = (
token_limit
if token_limit
< settings.MODEL_TOKEN_LIMITS.get(
self.gpt_model, settings.DEFAULT_MAX_HISTORY
)
else settings.MODEL_TOKEN_LIMITS.get(
self.gpt_model, settings.DEFAULT_MAX_HISTORY
)
)
self.user_api_key = user_api_key
def _parse_lang_string(self, input_string):
result = []
current_item = ""
inside_brackets = False
for char in input_string:
if char == "[":
inside_brackets = True
elif char == "]":
inside_brackets = False
result.append(current_item)
current_item = ""
elif inside_brackets:
current_item += char
if inside_brackets:
result.append(current_item)
return result
def _get_data(self):
if self.chunks == 0:
docs = []
else:
wrapper = DuckDuckGoSearchAPIWrapper(max_results=self.chunks)
search = DuckDuckGoSearchResults(api_wrapper=wrapper)
results = search.run(self.question)
results = self._parse_lang_string(results)
docs = []
for i in results:
try:
text = i.split("title:")[0]
title = i.split("title:")[1].split("link:")[0]
link = i.split("link:")[1]
docs.append({"text": text, "title": title, "link": link})
except IndexError:
pass
if settings.LLM_NAME == "llama.cpp":
docs = [docs[0]]
return docs
def gen(self):
docs = self._get_data()
# join all page_content together with a newline
docs_together = "\n".join([doc["text"] for doc in docs])
p_chat_combine = self.prompt.replace("{summaries}", docs_together)
messages_combine = [{"role": "system", "content": p_chat_combine}]
for doc in docs:
yield {"source": doc}
if len(self.chat_history) > 0:
for i in self.chat_history:
if "prompt" in i and "response" in i:
messages_combine.append({"role": "user", "content": i["prompt"]})
messages_combine.append(
{"role": "assistant", "content": i["response"]}
)
messages_combine.append({"role": "user", "content": self.question})
llm = LLMCreator.create_llm(
settings.LLM_NAME, api_key=settings.API_KEY, user_api_key=self.user_api_key
)
completion = llm.gen_stream(model=self.gpt_model, messages=messages_combine)
for line in completion:
yield {"answer": str(line)}
def search(self):
return self._get_data()
def get_params(self):
return {
"question": self.question,
"source": self.source,
"chat_history": self.chat_history,
"prompt": self.prompt,
"chunks": self.chunks,
"token_limit": self.token_limit,
"gpt_model": self.gpt_model,
"user_api_key": self.user_api_key
}

View File

@@ -1,20 +1,16 @@
from application.retriever.classic_rag import ClassicRAG
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,
"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

124
application/storage/base.py Normal file
View File

@@ -0,0 +1,124 @@
"""Base storage class for file system abstraction."""
from abc import ABC, abstractmethod
from typing import BinaryIO, List, Callable
class BaseStorage(ABC):
"""Abstract base class for storage implementations."""
@abstractmethod
def save_file(self, file_data: BinaryIO, path: str, **kwargs) -> dict:
"""
Save a file to storage.
Args:
file_data: File-like object containing the data
path: Path where the file should be stored
Returns:
dict: A dictionary containing metadata about the saved file, including:
- 'path': The path where the file was saved
- 'storage_type': The type of storage (e.g., 'local', 's3')
- Other storage-specific metadata (e.g., 'uri', 'bucket_name', etc.)
"""
pass
@abstractmethod
def get_file(self, path: str) -> BinaryIO:
"""
Retrieve a file from storage.
Args:
path: Path to the file
Returns:
BinaryIO: File-like object containing the file data
"""
pass
@abstractmethod
def process_file(self, path: str, processor_func: Callable, **kwargs):
"""
Process a file using the provided processor function.
This method handles the details of retrieving the file and providing
it to the processor function in an appropriate way based on the storage type.
Args:
path: Path to the file
processor_func: Function that processes the file
**kwargs: Additional arguments to pass to the processor function
Returns:
The result of the processor function
"""
pass
@abstractmethod
def delete_file(self, path: str) -> bool:
"""
Delete a file from storage.
Args:
path: Path to the file
Returns:
bool: True if deletion was successful
"""
pass
@abstractmethod
def file_exists(self, path: str) -> bool:
"""
Check if a file exists.
Args:
path: Path to the file
Returns:
bool: True if the file exists
"""
pass
@abstractmethod
def list_files(self, directory: str) -> List[str]:
"""
List all files in a directory.
Args:
directory: Directory path to list
Returns:
List[str]: List of file paths
"""
pass
@abstractmethod
def is_directory(self, path: str) -> bool:
"""
Check if a path is a directory.
Args:
path: Path to check
Returns:
bool: True if the path is a directory
"""
pass
@abstractmethod
def remove_directory(self, directory: str) -> bool:
"""
Remove a directory and all its contents.
For local storage, this removes the directory and all files/subdirectories within it.
For S3 storage, this removes all objects with the directory path as a prefix.
Args:
directory: Directory path to remove
Returns:
bool: True if removal was successful, False otherwise
"""
pass

View File

@@ -0,0 +1,140 @@
"""Local file system implementation."""
import os
import shutil
from typing import BinaryIO, List, Callable
from application.storage.base import BaseStorage
class LocalStorage(BaseStorage):
"""Local file system storage implementation."""
def __init__(self, base_dir: str = None):
"""
Initialize local storage.
Args:
base_dir: Base directory for all operations. If None, uses current directory.
"""
self.base_dir = base_dir or os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
def _get_full_path(self, path: str) -> str:
"""Get absolute path by combining base_dir and path."""
if os.path.isabs(path):
return path
return os.path.join(self.base_dir, path)
def save_file(self, file_data: BinaryIO, path: str) -> dict:
"""Save a file to local storage."""
full_path = self._get_full_path(path)
os.makedirs(os.path.dirname(full_path), exist_ok=True)
if hasattr(file_data, 'save'):
file_data.save(full_path)
else:
with open(full_path, 'wb') as f:
shutil.copyfileobj(file_data, f)
return {
'storage_type': 'local'
}
def get_file(self, path: str) -> BinaryIO:
"""Get a file from local storage."""
full_path = self._get_full_path(path)
if not os.path.exists(full_path):
raise FileNotFoundError(f"File not found: {full_path}")
return open(full_path, 'rb')
def delete_file(self, path: str) -> bool:
"""Delete a file from local storage."""
full_path = self._get_full_path(path)
if not os.path.exists(full_path):
return False
os.remove(full_path)
return True
def file_exists(self, path: str) -> bool:
"""Check if a file exists in local storage."""
full_path = self._get_full_path(path)
return os.path.exists(full_path)
def list_files(self, directory: str) -> List[str]:
"""List all files in a directory in local storage."""
full_path = self._get_full_path(directory)
if not os.path.exists(full_path):
return []
result = []
for root, _, files in os.walk(full_path):
for file in files:
rel_path = os.path.relpath(os.path.join(root, file), self.base_dir)
result.append(rel_path)
return result
def process_file(self, path: str, processor_func: Callable, **kwargs):
"""
Process a file using the provided processor function.
For local storage, we can directly pass the full path to the processor.
Args:
path: Path to the file
processor_func: Function that processes the file
**kwargs: Additional arguments to pass to the processor function
Returns:
The result of the processor function
"""
full_path = self._get_full_path(path)
if not os.path.exists(full_path):
raise FileNotFoundError(f"File not found: {full_path}")
return processor_func(local_path=full_path, **kwargs)
def is_directory(self, path: str) -> bool:
"""
Check if a path is a directory in local storage.
Args:
path: Path to check
Returns:
bool: True if the path is a directory, False otherwise
"""
full_path = self._get_full_path(path)
return os.path.isdir(full_path)
def remove_directory(self, directory: str) -> bool:
"""
Remove a directory and all its contents from local storage.
Args:
directory: Directory path to remove
Returns:
bool: True if removal was successful, False otherwise
"""
full_path = self._get_full_path(directory)
if not os.path.exists(full_path):
return False
if not os.path.isdir(full_path):
return False
try:
shutil.rmtree(full_path)
return True
except (OSError, PermissionError):
return False

206
application/storage/s3.py Normal file
View File

@@ -0,0 +1,206 @@
"""S3 storage implementation."""
import io
import os
from typing import BinaryIO, Callable, List
import boto3
from application.core.settings import settings
from application.storage.base import BaseStorage
from botocore.exceptions import ClientError
class S3Storage(BaseStorage):
"""AWS S3 storage implementation."""
def __init__(self, bucket_name=None):
"""
Initialize S3 storage.
Args:
bucket_name: S3 bucket name (optional, defaults to settings)
"""
self.bucket_name = bucket_name or getattr(
settings, "S3_BUCKET_NAME", "docsgpt-test-bucket"
)
# Get credentials from settings
aws_access_key_id = getattr(settings, "SAGEMAKER_ACCESS_KEY", None)
aws_secret_access_key = getattr(settings, "SAGEMAKER_SECRET_KEY", None)
region_name = getattr(settings, "SAGEMAKER_REGION", None)
self.s3 = boto3.client(
"s3",
aws_access_key_id=aws_access_key_id,
aws_secret_access_key=aws_secret_access_key,
region_name=region_name,
)
def save_file(
self,
file_data: BinaryIO,
path: str,
storage_class: str = "INTELLIGENT_TIERING",
**kwargs,
) -> dict:
"""Save a file to S3 storage."""
self.s3.upload_fileobj(
file_data, self.bucket_name, path, ExtraArgs={"StorageClass": storage_class}
)
region = getattr(settings, "SAGEMAKER_REGION", None)
return {
"storage_type": "s3",
"bucket_name": self.bucket_name,
"uri": f"s3://{self.bucket_name}/{path}",
"region": region,
}
def get_file(self, path: str) -> BinaryIO:
"""Get a file from S3 storage."""
if not self.file_exists(path):
raise FileNotFoundError(f"File not found: {path}")
file_obj = io.BytesIO()
self.s3.download_fileobj(self.bucket_name, path, file_obj)
file_obj.seek(0)
return file_obj
def delete_file(self, path: str) -> bool:
"""Delete a file from S3 storage."""
try:
self.s3.delete_object(Bucket=self.bucket_name, Key=path)
return True
except ClientError:
return False
def file_exists(self, path: str) -> bool:
"""Check if a file exists in S3 storage."""
try:
self.s3.head_object(Bucket=self.bucket_name, Key=path)
return True
except ClientError:
return False
def list_files(self, directory: str) -> List[str]:
"""List all files in a directory in S3 storage."""
# Ensure directory ends with a slash if it's not empty
if directory and not directory.endswith("/"):
directory += "/"
result = []
paginator = self.s3.get_paginator("list_objects_v2")
pages = paginator.paginate(Bucket=self.bucket_name, Prefix=directory)
for page in pages:
if "Contents" in page:
for obj in page["Contents"]:
result.append(obj["Key"])
return result
def process_file(self, path: str, processor_func: Callable, **kwargs):
"""
Process a file using the provided processor function.
Args:
path: Path to the file
processor_func: Function that processes the file
**kwargs: Additional arguments to pass to the processor function
Returns:
The result of the processor function
"""
import logging
import tempfile
if not self.file_exists(path):
raise FileNotFoundError(f"File not found in S3: {path}")
with tempfile.NamedTemporaryFile(
suffix=os.path.splitext(path)[1], delete=True
) as temp_file:
try:
# Download the file from S3 to the temporary file
self.s3.download_fileobj(self.bucket_name, path, temp_file)
temp_file.flush()
return processor_func(local_path=temp_file.name, **kwargs)
except Exception as e:
logging.error(f"Error processing S3 file {path}: {e}", exc_info=True)
raise
def is_directory(self, path: str) -> bool:
"""
Check if a path is a directory in S3 storage.
In S3, directories are virtual concepts. A path is considered a directory
if there are objects with the path as a prefix.
Args:
path: Path to check
Returns:
bool: True if the path is a directory, False otherwise
"""
# Ensure path ends with a slash if not empty
if path and not path.endswith('/'):
path += '/'
response = self.s3.list_objects_v2(
Bucket=self.bucket_name,
Prefix=path,
MaxKeys=1
)
return 'Contents' in response
def remove_directory(self, directory: str) -> bool:
"""
Remove a directory and all its contents from S3 storage.
In S3, this removes all objects with the directory path as a prefix.
Since S3 doesn't have actual directories, this effectively removes
all files within the virtual directory structure.
Args:
directory: Directory path to remove
Returns:
bool: True if removal was successful, False otherwise
"""
# Ensure directory ends with a slash if not empty
if directory and not directory.endswith('/'):
directory += '/'
try:
# Get all objects with the directory prefix
objects_to_delete = []
paginator = self.s3.get_paginator('list_objects_v2')
pages = paginator.paginate(Bucket=self.bucket_name, Prefix=directory)
for page in pages:
if 'Contents' in page:
for obj in page['Contents']:
objects_to_delete.append({'Key': obj['Key']})
if not objects_to_delete:
return False
batch_size = 1000
for i in range(0, len(objects_to_delete), batch_size):
batch = objects_to_delete[i:i + batch_size]
response = self.s3.delete_objects(
Bucket=self.bucket_name,
Delete={'Objects': batch}
)
if 'Errors' in response and response['Errors']:
return False
return True
except ClientError:
return False

View File

@@ -0,0 +1,32 @@
"""Storage factory for creating different storage implementations."""
from typing import Dict, Type
from application.storage.base import BaseStorage
from application.storage.local import LocalStorage
from application.storage.s3 import S3Storage
from application.core.settings import settings
class StorageCreator:
storages: Dict[str, Type[BaseStorage]] = {
"local": LocalStorage,
"s3": S3Storage,
}
_instance = None
@classmethod
def get_storage(cls) -> BaseStorage:
if cls._instance is None:
storage_type = getattr(settings, "STORAGE_TYPE", "local")
cls._instance = cls.create_storage(storage_type)
return cls._instance
@classmethod
def create_storage(cls, type_name: str, *args, **kwargs) -> BaseStorage:
storage_class = cls.storages.get(type_name.lower())
if not storage_class:
raise ValueError(f"No storage implementation found for type {type_name}")
return storage_class(*args, **kwargs)

View File

@@ -1,160 +0,0 @@
from application.core.mongo_db import MongoDB
from application.llm.llm_creator import LLMCreator
from application.tools.llm_handler import get_llm_handler
from application.tools.tool_action_parser import ToolActionParser
from application.tools.tool_manager import ToolManager
class Agent:
def __init__(self, llm_name, gpt_model, api_key, user_api_key=None):
# Initialize the LLM with the provided parameters
self.llm = LLMCreator.create_llm(
llm_name, api_key=api_key, user_api_key=user_api_key
)
self.llm_handler = get_llm_handler(llm_name)
self.gpt_model = gpt_model
# Static tool configuration (to be replaced later)
self.tools = []
self.tool_config = {}
def _get_user_tools(self, user="local"):
mongo = MongoDB.get_client()
db = mongo["docsgpt"]
user_tools_collection = db["user_tools"]
user_tools = user_tools_collection.find({"user": user, "status": True})
user_tools = list(user_tools)
tools_by_id = {str(tool["_id"]): tool for tool in user_tools}
return tools_by_id
def _build_tool_parameters(self, action):
params = {"type": "object", "properties": {}, "required": []}
for param_type in ["query_params", "headers", "body", "parameters"]:
if param_type in action and action[param_type].get("properties"):
for k, v in action[param_type]["properties"].items():
if v.get("filled_by_llm", True):
params["properties"][k] = {
key: value
for key, value in v.items()
if key != "filled_by_llm" and key != "value"
}
params["required"].append(k)
return params
def _prepare_tools(self, tools_dict):
self.tools = [
{
"type": "function",
"function": {
"name": f"{action['name']}_{tool_id}",
"description": action["description"],
"parameters": self._build_tool_parameters(action),
},
}
for tool_id, tool in tools_dict.items()
for action in (
tool["config"]["actions"].values()
if tool["name"] == "api_tool"
else tool["actions"]
)
if action.get("active", True)
]
def _execute_tool_action(self, tools_dict, call):
parser = ToolActionParser(self.llm.__class__.__name__)
tool_id, action_name, call_args = parser.parse_args(call)
tool_data = tools_dict[tool_id]
action_data = (
tool_data["config"]["actions"][action_name]
if tool_data["name"] == "api_tool"
else next(
action
for action in tool_data["actions"]
if action["name"] == action_name
)
)
query_params, headers, body, parameters = {}, {}, {}, {}
param_types = {
"query_params": query_params,
"headers": headers,
"body": body,
"parameters": parameters,
}
for param_type, target_dict in param_types.items():
if param_type in action_data and action_data[param_type].get("properties"):
for param, details in action_data[param_type]["properties"].items():
if param not in call_args and "value" in details:
target_dict[param] = details["value"]
for param, value in call_args.items():
for param_type, target_dict in param_types.items():
if param_type in action_data and param in action_data[param_type].get(
"properties", {}
):
target_dict[param] = value
tm = ToolManager(config={})
tool = tm.load_tool(
tool_data["name"],
tool_config=(
{
"url": tool_data["config"]["actions"][action_name]["url"],
"method": tool_data["config"]["actions"][action_name]["method"],
"headers": headers,
"query_params": query_params,
}
if tool_data["name"] == "api_tool"
else tool_data["config"]
),
)
if tool_data["name"] == "api_tool":
print(
f"Executing api: {action_name} with query_params: {query_params}, headers: {headers}, body: {body}"
)
result = tool.execute_action(action_name, **body)
else:
print(f"Executing tool: {action_name} with args: {call_args}")
result = tool.execute_action(action_name, **parameters)
call_id = getattr(call, "id", None)
return result, call_id
def _simple_tool_agent(self, messages):
tools_dict = self._get_user_tools()
self._prepare_tools(tools_dict)
resp = self.llm.gen(model=self.gpt_model, messages=messages, tools=self.tools)
if isinstance(resp, str):
yield resp
return
if hasattr(resp, "message") and hasattr(resp.message, "content"):
yield resp.message.content
return
resp = self.llm_handler.handle_response(self, resp, tools_dict, messages)
if isinstance(resp, str):
yield resp
elif hasattr(resp, "message") and hasattr(resp.message, "content"):
yield resp.message.content
else:
completion = self.llm.gen_stream(
model=self.gpt_model, messages=messages, tools=self.tools
)
for line in completion:
yield line
return
def gen(self, messages):
if self.llm.supports_tools():
resp = self._simple_tool_agent(messages)
for line in resp:
yield line
else:
resp = self.llm.gen_stream(model=self.gpt_model, messages=messages)
for line in resp:
yield line

View File

@@ -1,97 +0,0 @@
import json
from abc import ABC, abstractmethod
class LLMHandler(ABC):
@abstractmethod
def handle_response(self, agent, resp, tools_dict, messages, **kwargs):
pass
class OpenAILLMHandler(LLMHandler):
def handle_response(self, agent, resp, tools_dict, messages):
while 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:
tool_response, call_id = agent._execute_tool_action(
tools_dict, call
)
messages.append(
{
"role": "tool",
"content": str(tool_response),
"tool_call_id": call_id,
}
)
except Exception as e:
messages.append(
{
"role": "tool",
"content": f"Error executing tool: {str(e)}",
"tool_call_id": call_id,
}
)
resp = agent.llm.gen(
model=agent.gpt_model, messages=messages, tools=agent.tools
)
return resp
class GoogleLLMHandler(LLMHandler):
def handle_response(self, agent, resp, tools_dict, messages):
from google.genai import types
while True:
response = agent.llm.gen(
model=agent.gpt_model, messages=messages, tools=agent.tools
)
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
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
def get_llm_handler(llm_type):
handlers = {
"openai": OpenAILLMHandler(),
"google": GoogleLLMHandler(),
}
return handlers.get(llm_type, OpenAILLMHandler())

View File

@@ -1,26 +0,0 @@
import json
class ToolActionParser:
def __init__(self, llm_type):
self.llm_type = llm_type
self.parsers = {
"OpenAILLM": self._parse_openai_llm,
"GoogleLLM": self._parse_google_llm,
}
def parse_args(self, call):
parser = self.parsers.get(self.llm_type, self._parse_openai_llm)
return parser(call)
def _parse_openai_llm(self, call):
call_args = json.loads(call.function.arguments)
tool_id = call.function.name.split("_")[-1]
action_name = call.function.name.rsplit("_", 1)[0]
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]
return tool_id, action_name, call_args

View File

@@ -1,17 +1,24 @@
import sys
from datetime import datetime
from application.core.mongo_db import MongoDB
from application.utils import num_tokens_from_string, num_tokens_from_object_or_list
from application.core.settings import settings
from application.utils import num_tokens_from_object_or_list, num_tokens_from_string
mongo = MongoDB.get_client()
db = mongo["docsgpt"]
db = mongo[settings.MONGO_DB_NAME]
usage_collection = db["token_usage"]
def update_token_usage(user_api_key, token_usage):
def update_token_usage(decoded_token, user_api_key, token_usage):
if "pytest" in sys.modules:
return
if decoded_token:
user_id = decoded_token["sub"]
else:
user_id = None
usage_data = {
"user_id": user_id,
"api_key": user_api_key,
"prompt_tokens": token_usage["prompt_tokens"],
"generated_tokens": token_usage["generated_tokens"],
@@ -24,14 +31,17 @@ def gen_token_usage(func):
def wrapper(self, model, messages, stream, tools, **kwargs):
for message in messages:
if message["content"]:
self.token_usage["prompt_tokens"] += num_tokens_from_string(message["content"])
self.token_usage["prompt_tokens"] += num_tokens_from_string(
message["content"]
)
result = func(self, model, messages, stream, tools, **kwargs)
# check if result is a string
if isinstance(result, str):
self.token_usage["generated_tokens"] += num_tokens_from_string(result)
else:
self.token_usage["generated_tokens"] += num_tokens_from_object_or_list(result)
update_token_usage(self.user_api_key, self.token_usage)
self.token_usage["generated_tokens"] += num_tokens_from_object_or_list(
result
)
update_token_usage(self.decoded_token, self.user_api_key, self.token_usage)
return result
return wrapper
@@ -40,7 +50,9 @@ def gen_token_usage(func):
def stream_token_usage(func):
def wrapper(self, model, messages, stream, tools, **kwargs):
for message in messages:
self.token_usage["prompt_tokens"] += num_tokens_from_string(message["content"])
self.token_usage["prompt_tokens"] += num_tokens_from_string(
message["content"]
)
batch = []
result = func(self, model, messages, stream, tools, **kwargs)
for r in result:
@@ -48,6 +60,6 @@ def stream_token_usage(func):
yield r
for line in batch:
self.token_usage["generated_tokens"] += num_tokens_from_string(line)
update_token_usage(self.user_api_key, self.token_usage)
update_token_usage(self.decoded_token, self.user_api_key, self.token_usage)
return wrapper

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