Compare commits

..

1539 Commits

Author SHA1 Message Date
Timothy Jaeryang Baek
9bcd4ce5c0 Merge pull request #3559 from open-webui/dev
0.3.8
2024-07-09 14:25:16 -07:00
Timothy J. Baek
b38abf23bb chore: bump 2024-07-09 14:24:46 -07:00
Timothy J. Baek
7f2f45e176 doc: changelog 2024-07-09 14:21:06 -07:00
Timothy Jaeryang Baek
0444497eea Merge pull request #3734 from michaelpoluektov/refactor-main
refactor: Refactor backend/main.py
2024-07-09 12:48:33 -07:00
Michael Poluektov
1d20c27553 refac: use get_task_model_id() 2024-07-09 16:08:54 +01:00
Michael Poluektov
8f23df5749 fix: outlet __event_emitter__ 2024-07-09 15:57:24 +01:00
Michael Poluektov
144581a7df refac: get_sorted_pipelines() 2024-07-09 12:51:13 +01:00
Michael Poluektov
7ffd75b991 refac: black 2024-07-09 12:32:43 +01:00
Michael Poluektov
ff474936f8 refac: remove model param 2024-07-09 12:20:28 +01:00
Michael Poluektov
d7dd901f01 refac: remove nesting 2024-07-09 12:15:09 +01:00
Michael Poluektov
e3e02e04e8 refac: backend/main.py 2024-07-09 11:51:43 +01:00
Michael Poluektov
f9e3c47d4a rebase 2024-07-09 11:13:42 +01:00
Timothy J. Baek
24ef5af2a1 fix: code block 2024-07-09 00:41:56 -07:00
Timothy J. Baek
f8e2b31756 refac: styling 2024-07-09 00:26:02 -07:00
Timothy J. Baek
a11887cee1 enh: i said darker 2024-07-08 23:30:59 -07:00
Timothy J. Baek
78a5748727 refac 2024-07-08 23:21:17 -07:00
Timothy J. Baek
666a8ea5c0 refac 2024-07-08 23:18:35 -07:00
Timothy J. Baek
0bc46b19b3 refac 2024-07-08 23:17:57 -07:00
Timothy J. Baek
3f8af02aaa fix: styling 2024-07-08 23:16:54 -07:00
Timothy J. Baek
c9f5029a9d refac 2024-07-08 23:07:23 -07:00
Timothy Jaeryang Baek
aa896ba702 Merge pull request #3725 from aleixdorca/dev
i18n: Updated Catalan Translation
2024-07-08 22:25:28 -07:00
Timothy J. Baek
c2f4eab8ed refac: splash screen 2024-07-08 22:20:00 -07:00
Timothy J. Baek
47c76ab5fe refac: styling 2024-07-08 22:02:28 -07:00
aleix
2396010d6c i18n: Updated Catalan Translation 2024-07-09 07:01:34 +02:00
Timothy J. Baek
37285b8749 fix: do not include reserved params in specs 2024-07-08 21:52:23 -07:00
Timothy J. Baek
0a08a4d2fb refac 2024-07-08 21:40:22 -07:00
Timothy J. Baek
1b7ff1c5df feat: __event_call__ support 2024-07-08 21:39:06 -07:00
Timothy J. Baek
1d979d9b75 refac 2024-07-08 21:09:43 -07:00
Timothy J. Baek
d34e9ef935 refac: styling 2024-07-08 21:09:32 -07:00
Timothy J. Baek
d752002836 refac: styling 2024-07-08 20:17:09 -07:00
Timothy J. Baek
7df35d978f refac: styling 2024-07-08 20:15:13 -07:00
Timothy J. Baek
18ea8df685 fix: theme 2024-07-08 20:13:16 -07:00
Timothy J. Baek
a23146ebd1 refac: styling 2024-07-08 20:10:00 -07:00
Timothy J. Baek
446d85474e feat: darker oled 2024-07-08 19:55:00 -07:00
Timothy J. Baek
8942d0d0e2 refac: styling 2024-07-08 19:47:30 -07:00
Timothy J. Baek
eb1e176f14 chore: format 2024-07-08 19:40:18 -07:00
Timothy J. Baek
30e2ec7544 feat: chat controls integration 2024-07-08 19:26:31 -07:00
Timothy J. Baek
9cea5f75bb refac: styling 2024-07-08 18:31:58 -07:00
Timothy J. Baek
b8d153ebb2 feat: chat controls ui 2024-07-08 16:55:12 -07:00
Timothy J. Baek
781ad70598 refac: tooltip 2024-07-08 15:26:43 -07:00
Timothy J. Baek
3598aee71e refac: styling 2024-07-08 14:32:51 -07:00
Timothy J. Baek
e8fbb8f181 refac: styling 2024-07-08 14:20:11 -07:00
Timothy J. Baek
088e7b02a9 refac: styling 2024-07-08 13:35:25 -07:00
Timothy J. Baek
4b6ee584c2 fix: alembic 2024-07-08 12:55:27 -07:00
Timothy J. Baek
d3ef3a7494 refac 2024-07-08 12:42:52 -07:00
Timothy J. Baek
68d775e1ab chore: rm print 2024-07-08 12:08:27 -07:00
Timothy J. Baek
40abddff9a fix: font 2024-07-08 12:05:16 -07:00
Timothy J. Baek
68d6e738ac fix 2024-07-08 12:01:55 -07:00
Timothy J. Baek
3ddd88dad7 chore: rm print 2024-07-08 12:00:09 -07:00
Timothy J. Baek
42742d03d7 fix: model update 2024-07-08 11:58:36 -07:00
Timothy J. Baek
87f656b029 fix: tools update 2024-07-08 11:46:37 -07:00
Timothy Jaeryang Baek
489ef9b731 Merge pull request #3705 from open-webui/dependabot/pip/backend/dev/langfuse-2.38.0
chore(deps): bump langfuse from 2.36.2 to 2.38.0 in /backend
2024-07-08 11:36:05 -07:00
Timothy Jaeryang Baek
51ae387d59 Merge pull request #3708 from open-webui/dependabot/pip/backend/dev/peewee-3.17.6
chore(deps): bump peewee from 3.17.5 to 3.17.6 in /backend
2024-07-08 11:35:55 -07:00
Timothy Jaeryang Baek
642c5e035d Merge branch 'dev' into dependabot/pip/backend/dev/peewee-3.17.6 2024-07-08 11:35:48 -07:00
Timothy Jaeryang Baek
18962104a2 Merge pull request #3707 from open-webui/dependabot/pip/backend/dev/opencv-python-headless-4.10.0.84
chore(deps): bump opencv-python-headless from 4.9.0.80 to 4.10.0.84 in /backend
2024-07-08 11:35:21 -07:00
Timothy Jaeryang Baek
9eb0f89db8 Merge pull request #3706 from open-webui/dependabot/pip/backend/dev/alembic-1.13.2
chore(deps): bump alembic from 1.13.1 to 1.13.2 in /backend
2024-07-08 11:35:07 -07:00
Timothy Jaeryang Baek
1b660453f0 Merge pull request #3704 from open-webui/dependabot/pip/backend/dev/pytest-approx-eq-8.2.2
chore(deps): update pytest requirement from ~=8.2.1 to ~=8.2.2 in /backend
2024-07-08 11:34:51 -07:00
Timothy J. Baek
3b27acc77e fix 2024-07-08 11:34:24 -07:00
Timothy J. Baek
95426fc6c9 refac: do not use subprocess 2024-07-08 11:27:10 -07:00
Timothy J. Baek
39d3dcd032 fix: db 2024-07-08 10:46:35 -07:00
Timothy Jaeryang Baek
36c0f7f273 Merge pull request #3713 from jonathan-rohde/fix/missing-commits
fix: commit document delete
2024-07-08 10:22:40 -07:00
Jonathan Rohde
3b112375ee feat(documents): commit document delete 2024-07-08 09:14:45 +02:00
Timothy Jaeryang Baek
47ec2425ee Merge pull request #3692 from cheahjs/fix/integration-test
fix: delete docker build cache in integration test
2024-07-07 23:05:15 -07:00
Timothy J. Baek
d157af2fc7 chore: format 2024-07-07 23:04:50 -07:00
Timothy J. Baek
c1d706dc5a fix: db issues 2024-07-07 23:01:15 -07:00
Timothy Jaeryang Baek
a7fb560e84 Merge pull request #3699 from Peter-De-Ath/fix-update-user-email-auth
fix auth update_email_by_id - add db.commit
2024-07-07 22:58:20 -07:00
dependabot[bot]
44bbb40551 chore(deps): bump peewee from 3.17.5 to 3.17.6 in /backend
Bumps [peewee](https://github.com/coleifer/peewee) from 3.17.5 to 3.17.6.
- [Release notes](https://github.com/coleifer/peewee/releases)
- [Changelog](https://github.com/coleifer/peewee/blob/master/CHANGELOG.md)
- [Commits](https://github.com/coleifer/peewee/compare/3.17.5...3.17.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-08 03:11:22 +00:00
dependabot[bot]
d9a393eae1 chore(deps): bump opencv-python-headless in /backend
Bumps [opencv-python-headless](https://github.com/opencv/opencv-python) from 4.9.0.80 to 4.10.0.84.
- [Release notes](https://github.com/opencv/opencv-python/releases)
- [Commits](https://github.com/opencv/opencv-python/commits)

---
updated-dependencies:
- dependency-name: opencv-python-headless
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-08 03:11:12 +00:00
dependabot[bot]
842a6a6ee0 chore(deps): bump alembic from 1.13.1 to 1.13.2 in /backend
Bumps [alembic](https://github.com/sqlalchemy/alembic) from 1.13.1 to 1.13.2.
- [Release notes](https://github.com/sqlalchemy/alembic/releases)
- [Changelog](https://github.com/sqlalchemy/alembic/blob/main/CHANGES)
- [Commits](https://github.com/sqlalchemy/alembic/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-08 03:11:10 +00:00
dependabot[bot]
404b47274e chore(deps): bump langfuse from 2.36.2 to 2.38.0 in /backend
Bumps [langfuse](https://github.com/langfuse/langfuse) from 2.36.2 to 2.38.0.
- [Release notes](https://github.com/langfuse/langfuse/releases)
- [Commits](https://github.com/langfuse/langfuse/commits/v2.38.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-08 03:11:06 +00:00
dependabot[bot]
8eb03c02da chore(deps): update pytest requirement in /backend
Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.2.1...8.2.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-08 03:11:04 +00:00
Peter De-Ath
1bb2724282 fix password update in AuthsTable model 2024-07-07 22:27:26 +01:00
Peter De-Ath
f971ba0c0b fix email update in AuthsTable model 2024-07-07 22:07:12 +01:00
Timothy J. Baek
d51e866505 fix: db 2024-07-07 09:09:09 -07:00
Timothy Jaeryang Baek
068dff6aff Merge pull request #3691 from robertpiosik/replace-deafult-font-to-inter
feat: Replace default UI font, Arimo to Inter
2024-07-07 09:01:22 -07:00
Timothy Jaeryang Baek
7e6c5193d6 Merge pull request #3688 from leobenkel/no-trace-when-success
fix: Remove the tracestack when the collection already exists
2024-07-07 09:00:23 -07:00
Timothy Jaeryang Baek
a6dd62082f Merge pull request #3679 from que-nguyen/dev
Updated the missing Vietnamese translation.
2024-07-07 08:59:57 -07:00
Jun Siang Cheah
4ae0a1f3f8 fix: delete docker build cache in integration test 2024-07-07 12:06:42 +01:00
Robert Piosik
21b8072f35 Replace default UI font, Arimo to Inter 2024-07-07 11:27:04 +02:00
Leo Benkel
a73a9c7310 Remove the tracestack when the collection already exists 2024-07-06 23:20:41 +02:00
Que Nguyen
08f0a19812 Updated the missing Vietnamese translation. 2024-07-06 22:12:50 +07:00
Timothy J. Baek
c9b85bd4a2 fix: delete 2024-07-06 08:10:58 -07:00
Timothy J. Baek
0d70d7c9ac chore: allow more headroom for migration tests 2024-07-05 23:58:13 -07:00
Timothy J. Baek
e8ed48bd7a chore: disable backend tests 2024-07-05 23:54:03 -07:00
Timothy J. Baek
d5716ae751 chore: format 2024-07-05 23:48:53 -07:00
Timothy Jaeryang Baek
8f6f76682b Merge pull request #3595 from open-webui/dev-migration
feat: db migration
2024-07-05 23:47:01 -07:00
Timothy Jaeryang Baek
4e75150174 Merge pull request #3669 from open-webui/dev-migration-session
dev
2024-07-05 23:39:29 -07:00
Timothy J. Baek
1436bb7c61 enh: handle peewee migration 2024-07-05 23:38:53 -07:00
Timothy Jaeryang Baek
d60f06608e Merge pull request #3668 from open-webui/dev
dev
2024-07-05 22:29:13 -07:00
Timothy Jaeryang Baek
97a8491866 Merge pull request #3631 from ricky-davis/CustomCollapsible
feat: Custom Collapsible component
2024-07-05 21:42:54 -07:00
Timothy J. Baek
73899e1c0d refac 2024-07-05 21:37:29 -07:00
Timothy Jaeryang Baek
70efbef0c7 Merge pull request #3636 from Peter-De-Ath/add-scroll-to-settings-tabs
fix: add scroll to admin/user settings tabs
2024-07-05 21:28:17 -07:00
Timothy J. Baek
c3c15cbb7e refac 2024-07-05 21:27:59 -07:00
Timothy Jaeryang Baek
89e6044bdb Merge pull request #3657 from aleixdorca/dev
i18n: Update Catalan Translation
2024-07-05 21:21:21 -07:00
Timothy Jaeryang Baek
3928ac1905 Merge pull request #3615 from michaelpoluektov/citations-event
feat: Citations event via __event_emitter__
2024-07-05 21:20:40 -07:00
Timothy J. Baek
983fe4b2bc refac 2024-07-05 21:20:27 -07:00
aleix
8fd128ef1c i18n: Update Catalan Translation 2024-07-05 21:02:38 +02:00
Timothy Jaeryang Baek
45fae65ec1 Merge pull request #3630 from KarlLee830/translate
i18n: Update Chinese translation
2024-07-04 23:14:24 -07:00
Peter De-Ath
8381346378 enh: add sideways scrolling to settings tabs container 2024-07-05 02:05:59 +01:00
Timothy J. Baek
67c2ab006d fix: pipe custom model 2024-07-04 13:41:18 -07:00
Michael Poluektov
55b7c30028 simplify citation API 2024-07-04 18:50:09 +01:00
rdavis
ca3f8e6cb5 chore: format 2024-07-04 15:18:21 +00:00
Karl Lee
f611533764 i18n: Update Chinese translation 2024-07-04 22:57:32 +08:00
rdavis
78ba18a680 refactor: Update Collapsible component to include dynamic margin for open state 2024-07-04 14:55:48 +00:00
rdavis
db58bb5f0f refactor: Removed dependency 2024-07-04 14:15:16 +00:00
rdavis
d5c0876a0b refactor: fixed new Collapsible Component to allow passed in classes
chore: format
2024-07-04 14:02:26 +00:00
rdavis
2389c36a70 refactor: Update WebSearchResults.svelte to use new CollapsibleComponent 2024-07-04 13:55:37 +00:00
rdavis
d20601dc47 feat: Add custom Collapsible component for collapsible content 2024-07-04 13:53:28 +00:00
Michael Poluektov
0527755600 use data field 2024-07-04 12:21:09 +01:00
Timothy J. Baek
740b6f5c17 fix: pull model 2024-07-04 00:42:18 -07:00
Timothy J. Baek
9a6cbafdef fix: user valves 2024-07-04 00:37:05 -07:00
Timothy J. Baek
8b13755d56 Update auths.py 2024-07-04 00:25:45 -07:00
Timothy J. Baek
8fe2a7bb75 fix 2024-07-03 23:39:16 -07:00
Timothy J. Baek
37a5d2c06b Update db.py 2024-07-03 23:32:46 -07:00
Timothy J. Baek
864646094e refac 2024-07-03 23:32:39 -07:00
Timothy J. Baek
1b65df3acc revert 2024-07-03 21:28:51 -07:00
Timothy J. Baek
bfc53b49fd revert 2024-07-03 21:28:14 -07:00
Timothy J. Baek
f6dcffab13 fix: pinned chat delete issue 2024-07-03 21:18:40 -07:00
Timothy J. Baek
15f6f7bd15 revert: peewee migrations 2024-07-03 21:12:16 -07:00
Timothy Jaeryang Baek
0d78b63805 Merge pull request #3621 from open-webui/dev
dev
2024-07-03 20:59:14 -07:00
Timothy Jaeryang Baek
3efb8ab6ba Merge pull request #3617 from moblangeois/improve-fr-ca-translations
i18n: Improve French (FR) and French Canadian (CA) translations
2024-07-03 20:40:44 -07:00
Morgan Blangeois
f5a33ed3f9 Merge remote-tracking branch 'upstream/dev' into improve-fr-ca-translations 2024-07-04 00:56:40 +02:00
Timothy Jaeryang Baek
08c024d752 Merge pull request #3569 from Semihal/custom-openid-claims
feat: Custom claims for OAuth
2024-07-03 15:56:37 -07:00
Timothy J. Baek
c83704d6ca refac: task flag
Co-Authored-By: Michael Poluektov <78477503+michaelpoluektov@users.noreply.github.com>
2024-07-03 15:46:56 -07:00
Morgan Blangeois
f548762892 Resolve merge conflicts in French translations 2024-07-04 00:40:56 +02:00
Timothy Jaeryang Baek
d0e0aba593 Merge pull request #3611 from bannert1337/i18n-de_DE
i18n(de_DE): added translations for new entries, updated old entries
2024-07-03 15:39:37 -07:00
Michael Poluektov
4e433d9015 wip: citations via __event_emitter__ 2024-07-03 18:18:33 +01:00
Morgan Blangeois
2fedd91ed9 feat: Improve French Canadian (fr-ca) translations 2024-07-03 17:20:57 +02:00
Morgan Blangeois
3cd0b1077d i18n: Improve French translations 2024-07-03 17:09:53 +02:00
bannert
1f026a1811 i18n(de_DE): added translations for new entries, updated old entries 2024-07-03 15:16:54 +02:00
Michael Poluektov
49b4211c06 Merge branch 'dev' of https://github.com/open-webui/open-webui into dev 2024-07-03 10:07:46 +01:00
Michael Poluektov
24c7990fd4 revert not delete on pipe 2024-07-03 09:12:00 +01:00
Timothy J. Baek
4d23957035 revert: model_validate 2024-07-02 21:56:32 -07:00
Timothy J. Baek
aa88022624 fix: functions 2024-07-02 21:50:53 -07:00
Timothy J. Baek
44a9b86eec fix: functions 2024-07-02 21:46:56 -07:00
Timothy J. Baek
647aa1966f chore: format 2024-07-02 16:51:30 -07:00
Timothy Jaeryang Baek
d0e89a0318 Merge pull request #3327 from jonathan-rohde/feat/sqlalchemy-instead-of-peewee
BREAKING CHANGE/sqlalchemy instead of peewee
2024-07-02 16:40:13 -07:00
Timothy Jaeryang Baek
2c061777ca Merge pull request #3591 from michaelpoluektov/fix-banners-env
fix: fix WEBUI_BANNERS environment variable not working
2024-07-02 16:22:06 -07:00
Timothy J. Baek
f57a435576 chore: bump 2024-07-02 16:08:03 -07:00
Timothy J. Baek
cf317f8fdf fix 2024-07-02 15:20:50 -07:00
Michael Poluektov
655238dcd7 banners: generic exception 2024-07-02 14:41:59 +01:00
Michael Poluektov
16fa454558 fix banners env 2024-07-02 14:17:36 +01:00
Michael Poluektov
02f242e9e8 keep title, task, function tags for pipelines 2024-07-02 11:52:46 +01:00
Michael Poluektov
09514751b5 Update main.py 2024-07-02 10:57:56 +01:00
Michael Poluektov
afd74213cc Update main.py 2024-07-02 10:20:50 +01:00
Timothy J. Baek
e4c85921b2 fix: styling 2024-07-02 01:16:57 -07:00
Timothy J. Baek
7725a877d6 enh: get_last_user_message_item 2024-07-02 00:37:21 -07:00
Timothy J. Baek
fefa8a81d0 chore: format 2024-07-01 23:12:38 -07:00
Timothy J. Baek
05ec71beb9 enh: pinned chats support 2024-07-01 23:08:01 -07:00
Timothy J. Baek
439ab7a335 refac 2024-07-01 22:18:45 -07:00
Timothy J. Baek
044a0fbbbe enh: bool, literal valves 2024-07-01 22:14:09 -07:00
Timothy J. Baek
d97a4d687e refac 2024-07-01 21:41:44 -07:00
Timothy J. Baek
52cae406b4 feat: chat event handler 2024-07-01 20:53:24 -07:00
Timothy J. Baek
d6dbd73ec9 fix 2024-07-01 20:15:27 -07:00
Timothy J. Baek
a07051f51b feat: __event_emitter__ 2024-07-01 20:05:02 -07:00
Timothy J. Baek
e5895af7a0 refac 2024-07-01 19:37:54 -07:00
Timothy J. Baek
c7a9b5ccfa refac: chat completion middleware 2024-07-01 19:33:58 -07:00
Timothy J. Baek
b62d2a9b28 refac 2024-07-01 17:15:10 -07:00
Timothy J. Baek
a392865615 refac 2024-07-01 17:11:09 -07:00
Timothy Jaeryang Baek
3c1ea24374 Merge pull request #3582 from nickovs/tika-document-text
feat: Support Tika for document text extraction
2024-07-01 17:07:40 -07:00
Timothy J. Baek
62ba6a2413 chore: format 2024-07-01 16:52:46 -07:00
Timothy J. Baek
b875efab1f enh: validate required open webui version 2024-07-01 16:50:35 -07:00
Timothy J. Baek
bd45b7a04b fix 2024-07-01 16:11:44 -07:00
Timothy J. Baek
9c01297191 fix 2024-07-01 16:11:24 -07:00
Timothy J. Baek
6d350fb8bc revert: text split 2024-07-01 16:04:24 -07:00
Timothy J. Baek
7212b7c8aa chore: bump 2024-07-01 15:49:48 -07:00
Timothy J. Baek
9cc46629c2 refac 2024-07-01 15:43:19 -07:00
Timothy Jaeryang Baek
a03629178e Merge pull request #3568 from fishhf/dev
fix: use AIOHTTP_CLIENT_TIMEOUT timeout setting for openai streaming response
2024-07-01 14:02:13 -07:00
Timothy Jaeryang Baek
46a084d457 Merge pull request #3580 from bannert1337/i18n--optimize-de_DE
i18n: rework de_DE locale
2024-07-01 13:29:06 -07:00
Nicko van Someren
7aa35a3757 Added HTML and Typescript UI components to support configration of text extraction engine.
Updated RAG /config and /config/update endpoints to support UI updates.

Fixed .dockerignore to prevent Python venv from being copied into Docker image.
2024-07-01 12:10:59 -06:00
bannert
7f937af029 i18n(de_DE): Update German translation strings for better grammar and clarity 2024-07-01 19:01:22 +02:00
bannert
4041484a19 i18n(de-DE): Improve translation consistency and accuracy 2024-07-01 18:55:28 +02:00
bannert
8a38b0a286 feat(config): exclude .vscode/settings.json from version control 2024-07-01 18:52:19 +02:00
bannert
19d4c7f6cd Update languages.json 2024-07-01 18:42:56 +02:00
bannert
4ead3dabd8 Delete src/lib/i18n/locales/de-DE/translation-common.json 2024-07-01 18:41:56 +02:00
bannert
77f5d90be3 Delete src/lib/i18n/locales/de-DE/translation-informal.json 2024-07-01 18:41:49 +02:00
bannert
db495a1df0 refactor: Revert changes to i18n/locales/languages.json 2024-07-01 18:40:57 +02:00
bannert
8634140306 refactor: Remove changes to i18n/index.ts 2024-07-01 18:38:52 +02:00
bannert
586de1f5ba feat(i18n): unify German translations and remove formal/informal versions 2024-07-01 18:31:13 +02:00
Jonathan Rohde
2aecd7d0b9 Merge branch 'refs/heads/dev' into feat/sqlalchemy-instead-of-peewee
# Conflicts:
#	backend/requirements.txt
2024-07-01 10:37:56 +02:00
Sergey Mihaylin
a94c7e5c09 fix lint 2024-07-01 10:36:21 +03:00
Sergey Mihaylin
e475f025b7 fix: merge request fail (remove picture_claim) 2024-07-01 10:25:25 +03:00
Sergey Mihaylin
6e934c2d17 Merge branch 'refs/heads/main' into custom-openid-claims
# Conflicts:
#	backend/main.py
2024-07-01 10:08:38 +03:00
Fish Lung
f89fa061e8 fix: use AIOHTTP_CLIENT_TIMEOUT timeout setting for openai streaming response 2024-07-01 14:29:26 +08:00
Timothy Jaeryang Baek
f3c1ff9efc Merge pull request #3564 from wanderingmeow/fix-regexp-safari-before-16.4
fix: RegExp "invalid group specifier name" exception on Safari < 16.4
2024-06-30 22:40:53 -07:00
Timothy J. Baek
4547afe0b9 chore: bump 2024-06-30 22:29:03 -07:00
Timothy J. Baek
7955c9ba3b refac 2024-06-30 22:28:43 -07:00
Timothy Jaeryang Baek
c52e16a844 Merge pull request #3562 from open-webui/dependabot/pip/backend/dev/sentence-transformers-3.0.1
chore(deps): bump sentence-transformers from 2.7.0 to 3.0.1 in /backend
2024-06-30 19:58:38 -07:00
Timothy Jaeryang Baek
c676a78cd7 Merge pull request #3543 from open-webui/dependabot/pip/backend/dev/openpyxl-3.1.5
chore(deps): bump openpyxl from 3.1.2 to 3.1.5 in /backend
2024-06-30 19:57:20 -07:00
Timothy Jaeryang Baek
61b2bcae47 Merge pull request #3563 from open-webui/dependabot/pip/backend/dev/langchain-community-0.2.6
chore(deps): bump langchain-community from 0.2.0 to 0.2.6 in /backend
2024-06-30 19:57:10 -07:00
Timothy Jaeryang Baek
ee59cbc634 Merge pull request #3561 from open-webui/dependabot/pip/backend/dev/requests-2.32.3
chore(deps): bump requests from 2.32.2 to 2.32.3 in /backend
2024-06-30 19:56:53 -07:00
Timothy Jaeryang Baek
0c552b4730 Merge pull request #3560 from open-webui/dependabot/pip/backend/dev/langfuse-2.36.2
chore(deps): bump langfuse from 2.33.0 to 2.36.2 in /backend
2024-06-30 19:56:45 -07:00
WanderingMeow
de3d49000e fix: RegExp "invalid group specifier name" exception on Safari < 16.4 (#3306, #3371) 2024-07-01 10:42:55 +08:00
dependabot[bot]
8c7fb0312c chore(deps): bump langchain-community from 0.2.0 to 0.2.6 in /backend
Bumps [langchain-community](https://github.com/langchain-ai/langchain) from 0.2.0 to 0.2.6.
- [Release notes](https://github.com/langchain-ai/langchain/releases)
- [Commits](https://github.com/langchain-ai/langchain/compare/langchain-community==0.2.0...langchain-community==0.2.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-01 02:30:30 +00:00
dependabot[bot]
0ee984b444 chore(deps): bump sentence-transformers from 2.7.0 to 3.0.1 in /backend
Bumps [sentence-transformers](https://github.com/UKPLab/sentence-transformers) from 2.7.0 to 3.0.1.
- [Release notes](https://github.com/UKPLab/sentence-transformers/releases)
- [Commits](https://github.com/UKPLab/sentence-transformers/compare/v2.7.0...v3.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-01 02:30:25 +00:00
dependabot[bot]
2fadc0c68f chore(deps): bump requests from 2.32.2 to 2.32.3 in /backend
Bumps [requests](https://github.com/psf/requests) from 2.32.2 to 2.32.3.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.32.2...v2.32.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-01 02:30:22 +00:00
dependabot[bot]
3f31bb0975 chore(deps): bump langfuse from 2.33.0 to 2.36.2 in /backend
Bumps [langfuse](https://github.com/langfuse/langfuse) from 2.33.0 to 2.36.2.
- [Release notes](https://github.com/langfuse/langfuse/releases)
- [Commits](https://github.com/langfuse/langfuse/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-01 02:30:20 +00:00
Timothy Jaeryang Baek
5d1573fd1b Merge pull request #3557 from Naufal05R/patch-1
i18n: Update Indonesian translation
2024-06-30 17:46:56 -07:00
Timothy Jaeryang Baek
5c6e30cb5d Merge pull request #3558 from cheahjs/refac/reduce-startup-mem-usage
refac: reduce startup memory usage
2024-06-30 17:46:27 -07:00
Timothy J. Baek
a55d6e6077 fix: compare message 2024-06-30 17:45:28 -07:00
Jun Siang Cheah
a48ac6a209 refac: lazily load sentence_transformers to reduce start up memory usage 2024-07-01 08:13:56 +08:00
Jun Siang Cheah
17c684369e refac: lazily load faster_whisper to reduce start up memory usage 2024-07-01 08:13:02 +08:00
Naufal Rabbani
11210fb6e4 i18n: Update Indonesian translation 2024-07-01 06:48:57 +07:00
Timothy Jaeryang Baek
824966ad4a Merge pull request #3555 from open-webui/dev
fix: default locale
2024-06-30 14:53:27 -07:00
Timothy J. Baek
f77073410e refac 2024-06-30 14:52:18 -07:00
Nicko van Someren
9cf622d981 Added support for using Apache Tika as a document loader.
Added persistent configuration options to configure use and location of Tika service.

Updated backend.apps.rag.main:get_loader() to make use of Tika document loader.
2024-06-30 15:49:15 -06:00
Timothy J. Baek
d9a229b1ec fix: default locale 2024-06-30 14:48:05 -07:00
Timothy Jaeryang Baek
7bc88eb00d Merge pull request #3549 from open-webui/dev
refac: ollama non streaming response
2024-06-30 01:40:26 -07:00
Timothy J. Baek
f6efda9e2f refac: ollama non stream response 2024-06-30 01:30:19 -07:00
Timothy Jaeryang Baek
bb53282cb0 Merge pull request #3542 from open-webui/dependabot/pip/backend/dev/duckduckgo-search-approx-eq-6.1.7
chore(deps): update duckduckgo-search requirement from ~=6.1.5 to ~=6.1.7 in /backend
2024-06-29 20:44:19 -07:00
Timothy Jaeryang Baek
c933865172 Merge pull request #3541 from open-webui/dependabot/pip/backend/dev/langchain-chroma-0.1.2
chore(deps): bump langchain-chroma from 0.1.1 to 0.1.2 in /backend
2024-06-29 20:43:56 -07:00
Timothy Jaeryang Baek
ce990d3859 Merge pull request #3539 from open-webui/dependabot/pip/backend/dev/chromadb-0.5.3
chore(deps): bump chromadb from 0.5.0 to 0.5.3 in /backend
2024-06-29 20:43:46 -07:00
Timothy Jaeryang Baek
315c8b2af3 Merge pull request #3540 from open-webui/dependabot/pip/backend/dev/python-socketio-5.11.3
chore(deps): bump python-socketio from 5.11.2 to 5.11.3 in /backend
2024-06-29 20:43:31 -07:00
Timothy Jaeryang Baek
66182bac75 Merge pull request #3546 from open-webui/dev
refac: language detection
2024-06-29 20:41:32 -07:00
Timothy J. Baek
5ee7da54a1 refac: language detection 2024-06-29 20:41:06 -07:00
Timothy J. Baek
4b0c422ec5 chore: format 2024-06-29 20:33:10 -07:00
dependabot[bot]
5dacd41278 chore(deps): bump openpyxl from 3.1.2 to 3.1.5 in /backend
Bumps [openpyxl](https://openpyxl.readthedocs.io) from 3.1.2 to 3.1.5.

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-30 03:27:46 +00:00
dependabot[bot]
052c582930 chore(deps): update duckduckgo-search requirement in /backend
Updates the requirements on [duckduckgo-search](https://github.com/deedy5/duckduckgo_search) to permit the latest version.
- [Release notes](https://github.com/deedy5/duckduckgo_search/releases)
- [Commits](https://github.com/deedy5/duckduckgo_search/compare/v6.1.5...v6.1.7)

---
updated-dependencies:
- dependency-name: duckduckgo-search
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-30 03:27:43 +00:00
dependabot[bot]
3c0ce5d1b0 chore(deps): bump langchain-chroma from 0.1.1 to 0.1.2 in /backend
Bumps [langchain-chroma](https://github.com/langchain-ai/langchain) from 0.1.1 to 0.1.2.
- [Release notes](https://github.com/langchain-ai/langchain/releases)
- [Commits](https://github.com/langchain-ai/langchain/compare/langchain-chroma==0.1.1...langchain-chroma==0.1.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-30 03:27:39 +00:00
dependabot[bot]
9d1aa4aebe chore(deps): bump python-socketio from 5.11.2 to 5.11.3 in /backend
Bumps [python-socketio](https://github.com/miguelgrinberg/python-socketio) from 5.11.2 to 5.11.3.
- [Release notes](https://github.com/miguelgrinberg/python-socketio/releases)
- [Changelog](https://github.com/miguelgrinberg/python-socketio/blob/main/CHANGES.md)
- [Commits](https://github.com/miguelgrinberg/python-socketio/compare/v5.11.2...v5.11.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-30 03:27:34 +00:00
dependabot[bot]
e540f8a4e4 chore(deps): bump chromadb from 0.5.0 to 0.5.3 in /backend
Bumps [chromadb](https://github.com/chroma-core/chroma) from 0.5.0 to 0.5.3.
- [Release notes](https://github.com/chroma-core/chroma/releases)
- [Changelog](https://github.com/chroma-core/chroma/blob/main/RELEASE_PROCESS.md)
- [Commits](https://github.com/chroma-core/chroma/compare/0.5.0...0.5.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-30 03:27:27 +00:00
Timothy Jaeryang Baek
4900ac5136 Merge pull request #3536 from open-webui/dev
0.3.7
2024-06-29 20:26:59 -07:00
Timothy J. Baek
58d8dd94fc chore: bump 2024-06-29 20:26:51 -07:00
Timothy J. Baek
df8d1dacc4 refac: browser language detection 2024-06-29 20:04:04 -07:00
Timothy Jaeryang Baek
56137acd94 Merge pull request #3507 from open-webui/dependabot/pip/authlib-1.3.1
chore(deps): bump authlib from 1.3.0 to 1.3.1
2024-06-29 19:52:43 -07:00
Timothy J. Baek
41c55fbb81 enh: boilerplate 2024-06-29 19:49:16 -07:00
Timothy J. Baek
16454c0de5 chore: format 2024-06-29 19:47:23 -07:00
Timothy J. Baek
d0567832cc fix 2024-06-29 19:40:54 -07:00
bannert
9c2313a1bd feat(i18n): improve init function with cleaner code and nullish coalescing operator
- Use object destructuring for clearer variable assignment.
   - Utilize the nullish coalescing operator to simplify ternary expressions.
   - Refactored the import statement to remove duplication.
2024-06-30 02:44:03 +02:00
Timothy Jaeryang Baek
61972ca97d Merge pull request #3519 from Naufal05R/main
i18n: Add Indonesian translate
2024-06-29 13:17:12 -07:00
Timothy Jaeryang Baek
73135b6e51 Merge pull request #3511 from Peter-De-Ath/ollama-chat-no-stream
fix: isInstance of streamingResponse but content-type is application/json
2024-06-29 13:16:32 -07:00
Timothy Jaeryang Baek
27709c835c Merge pull request #3497 from aguvener/dev
i18n: tr_TR translation updated with the latest changes
2024-06-29 13:15:59 -07:00
Timothy Jaeryang Baek
d3a67b44cd Merge pull request #3499 from Semihal/fix-oauth-openid
fix: First OIDC account is not admin
2024-06-29 13:15:42 -07:00
Timothy Jaeryang Baek
58398b6eeb Merge pull request #3514 from KarlLee830/translate
i18n: Update Chinese translation
2024-06-29 13:14:35 -07:00
Timothy Jaeryang Baek
98ae064cd0 Merge pull request #3525 from aleixdorca/dev
i18n: Updated Catalan Translations
2024-06-29 13:14:23 -07:00
Aleix Dorca
776082dc87 i18n: Updated Catalan Translation 2024-06-29 20:34:31 +02:00
aleix
6e03a1c168 i18n: Updated Catalan Translation 2024-06-29 17:25:11 +02:00
aleix
45cb1c0fcb Updated Catalan Translation 2024-06-29 17:21:03 +02:00
Naufal Rabbani
5b848dcf54 added indonesian language 2024-06-29 16:25:42 +07:00
Naufal Rabbani
2ef899220a added indonesian language 2024-06-29 16:18:45 +07:00
Karl Lee
8ab4b7ab47 i18n: Update Chinese translation 2024-06-29 04:22:04 +08:00
Peter De-Ath
269db0748a fix: isInstance of streamingResponse but content-type is application/json 2024-06-28 20:32:07 +01:00
bannert
6ffa189814 i18n: split locale for German formal and informal
translation-common.json is the base
translation-formal.json and translation-informal.json extend the base
2024-06-28 20:03:36 +02:00
bannert
24f84a6069 feat(i18n): fixed a word in German translation 2024-06-28 19:19:45 +02:00
bannert
c138445944 feat(i18n): update and optimize de_DE 2024-06-28 19:02:50 +02:00
dependabot[bot]
4d3e1ffbe6 chore(deps): bump authlib from 1.3.0 to 1.3.1
Bumps [authlib](https://github.com/lepture/authlib) from 1.3.0 to 1.3.1.
- [Release notes](https://github.com/lepture/authlib/releases)
- [Changelog](https://github.com/lepture/authlib/blob/master/docs/changelog.rst)
- [Commits](https://github.com/lepture/authlib/compare/v1.3.0...v1.3.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-28 16:53:45 +00:00
Sergey Mihaylin
9f32e9ef60 fix username claim 2024-06-28 17:08:32 +03:00
Sergey Mihaylin
0c3f9a16e3 custom env for set custom claims for openid 2024-06-28 16:31:40 +03:00
Sergey Mihaylin
7d10dacad6 Fix: set jwt_token to cookie (instead of token from OIDC) 2024-06-28 16:20:57 +03:00
Sergey Mihaylin
57c330576d Fix: admin role for OIDC signup for first user 2024-06-28 16:20:34 +03:00
aguvener
3cc5da835c Update translation.json for minor change 2024-06-28 13:18:44 +03:00
aguvener
9111970580 Update translation.json 2024-06-28 13:06:50 +03:00
Jonathan Rohde
5391f4c1f7 feat(sqlalchemy): add new column 2024-06-28 09:21:07 +02:00
Jonathan Rohde
df47c496d3 Merge branch 'refs/heads/dev' into feat/sqlalchemy-instead-of-peewee
# Conflicts:
#	backend/apps/webui/models/functions.py
#	backend/apps/webui/routers/chats.py
2024-06-28 09:19:56 +02:00
Timothy Jaeryang Baek
24b638fcde Merge pull request #3486 from open-webui/dev
fix: trusted sign in
2024-06-27 21:44:54 -07:00
Timothy J. Baek
cd9170ed24 fix: trusted sign in 2024-06-27 21:44:35 -07:00
Timothy Jaeryang Baek
f7da94ff85 Merge pull request #3485 from open-webui/dev
fix: WEBUI_AUTH
2024-06-27 21:43:51 -07:00
Timothy J. Baek
feba50f68c fix: WEBUI_AUTH 2024-06-27 21:43:19 -07:00
Timothy Jaeryang Baek
b0724811df Merge pull request #3477 from open-webui/dev
fix: changelog
2024-06-27 13:38:44 -07:00
Timothy J. Baek
1dd54ced41 fix: changelog 2024-06-27 13:38:28 -07:00
Timothy Jaeryang Baek
1eebb85f48 Merge pull request #3323 from open-webui/dev
0.3.6
2024-06-27 13:36:02 -07:00
Timothy J. Baek
b224ba0030 refac: styling 2024-06-27 13:15:54 -07:00
Timothy J. Baek
46ae25826d chore: format 2024-06-27 13:13:16 -07:00
Timothy J. Baek
c954f1b4de refac 2024-06-27 13:12:37 -07:00
Timothy J. Baek
edbd07f893 feat: global filter 2024-06-27 13:04:12 -07:00
Timothy J. Baek
c8c85ba7fc refac 2024-06-27 12:16:55 -07:00
Timothy J. Baek
c262d9ad4f doc: changelog 2024-06-27 11:30:30 -07:00
Timothy J. Baek
3f5f410453 refac 2024-06-27 11:29:59 -07:00
Timothy J. Baek
3c7f45ced4 fix 2024-06-27 11:12:07 -07:00
Timothy J. Baek
c07da8d1f3 chore: format 2024-06-27 10:56:00 -07:00
Timothy Jaeryang Baek
d3b9f03bdb Merge pull request #3470 from djismgaming/dev
**i18n** Spanish translation updates
2024-06-27 10:51:21 -07:00
Timothy Jaeryang Baek
433abe5a25 Merge pull request #3471 from KarlLee830/translate 2024-06-27 10:37:32 -07:00
Karl Lee
b59684328d i18n: Update Chinese translation 2024-06-27 20:59:43 +08:00
Ismael
850d4aac70 additional Spanish strings updated 2024-06-27 07:33:38 -04:00
Ismael
bae87ec4ae Update translation.json
Spanish translation updates
2024-06-27 07:08:03 -04:00
Timothy J. Baek
a5138b7621 doc: changelog 2024-06-27 03:24:16 -07:00
Timothy J. Baek
4e6d165d00 refac 2024-06-27 00:43:31 -07:00
Jonathan Rohde
827b1e58e9 feat(sqlalchemy): execute tests in github actions 2024-06-27 07:48:31 +02:00
Jonathan Rohde
23e4d9daff feat(sqlalchemy): formatting 2024-06-27 07:48:26 +02:00
Jonathan Rohde
d4b6b7c4e8 feat(sqlalchemy): reverted not needed api change 2024-06-27 07:48:08 +02:00
Jonathan Rohde
642c352c69 feat(sqlalchemy): rebase 2024-06-27 07:48:08 +02:00
Jonathan Rohde
d88bd51e3c feat(sqlalchemy): format backend 2024-06-27 07:48:08 +02:00
Jonathan Rohde
2fb27adbf6 feat(sqlalchemy): add missing file 2024-06-27 07:48:08 +02:00
Jonathan Rohde
8f939cf55b feat(sqlalchemy): some fixes 2024-06-27 07:48:08 +02:00
Jonathan Rohde
a9b148791d feat(sqlalchemy): fix wrong column types 2024-06-27 07:48:08 +02:00
Jonathan Rohde
da403f3e3c feat(sqlalchemy): use session factory instead of context manager 2024-06-27 07:48:08 +02:00
Jonathan Rohde
eb01e8d275 feat(sqlalchemy): use scoped session 2024-06-27 07:48:08 +02:00
Jonathan Rohde
c134eab27a feat(sqlalchemy): format backend 2024-06-27 07:48:08 +02:00
Jonathan Rohde
320e658595 feat(sqlalchemy): cleanup fixes 2024-06-27 07:48:08 +02:00
Jonathan Rohde
070d9083d5 feat(sqlalchemy): use subprocess to do migrations 2024-06-27 07:48:08 +02:00
Jonathan Rohde
bee835cb65 feat(sqlalchemy): remove session reference from router 2024-06-27 07:48:08 +02:00
Jonathan Rohde
df09d0830a feat(sqlalchemy): Replace peewee with sqlalchemy 2024-06-27 07:48:08 +02:00
Timothy J. Baek
8dac2a2140 refac 2024-06-26 10:22:31 -07:00
Timothy Jaeryang Baek
aad23af3a3 Merge pull request #3456 from SimonOriginal/dev
i18n: Update of the Ukrainian translation
2024-06-26 09:47:26 -07:00
SimonOriginal
755850e453 add translation for new keys 2024-06-26 17:17:48 +02:00
Timothy J. Baek
581e7800fa fix 2024-06-26 00:36:35 -07:00
Timothy J. Baek
0ec4d56593 fix 2024-06-26 00:23:24 -07:00
Timothy J. Baek
0e7db89117 feat: import/export community 2024-06-26 00:22:26 -07:00
Timothy J. Baek
bc5e39d31b chore: format 2024-06-25 16:31:33 -07:00
Timothy J. Baek
6cd47a3c9b refac 2024-06-25 16:30:49 -07:00
Timothy Jaeryang Baek
70c985feec Merge pull request #3447 from ben-z/stt-model-configurable
fix: Make STT model configurable
2024-06-25 15:26:57 -07:00
Ben Zhang
044bd00386 Make STT model configurable 2024-06-25 21:46:12 +00:00
Timothy Jaeryang Baek
b0ed8ded48 Merge pull request #3437 from silentoplayz/patch-1
feat: case insensitive prompt/models selection chat input
2024-06-25 12:55:08 -07:00
Timothy J. Baek
8bdbd2f9fb fix: rm conversationMode 2024-06-25 12:27:00 -07:00
silentoplayz
ef619321ce Update Models.svelte
feat: case-insensitive @model-name search
2024-06-25 18:22:38 +00:00
silentoplayz
01ad197973 Update PromptCommands.svelte
feat: case-insensitive prompt search
2024-06-25 18:19:52 +00:00
Timothy J. Baek
a92c5381fb chore: format 2024-06-25 08:51:52 -07:00
Timothy Jaeryang Baek
3750e69e09 Merge pull request #3418 from cheahjs/feat/hide-tokens-in-ui
feat: hide all API keys by default in admin settings
2024-06-25 08:40:18 -07:00
Timothy Jaeryang Baek
771ac86163 Merge pull request #3417 from ricky-davis/addDeleteConfirmationToolsFunctions
feat: Added delete confirmation dialogs for Prompts, Tools, and Functions.
2024-06-25 08:18:36 -07:00
Timothy J. Baek
36e88d479b chore: format 2024-06-25 08:01:05 -07:00
Timothy J. Baek
c147147001 chore: format 2024-06-25 08:00:32 -07:00
rdavis
4d5e161a3e Updated Functions and Tools to use existing selected* Variable
Applied formatting
2024-06-25 08:28:43 -05:00
Jun Siang Cheah
f8f6943128 refac: use new SensitiveInput component 2024-06-25 20:15:29 +08:00
Jun Siang Cheah
d5b91fb084 feat: hide all API keys by default in admin settings 2024-06-25 19:53:22 +08:00
rdavis
263d4bf496 Added delete confirmation dialogs for Prompts, Tools, and Functions. 2024-06-24 22:50:35 -05:00
Timothy Jaeryang Baek
16a8eebd8d Merge pull request #3328 from FuturePrayer/AUTOMATIC1111_api_auth_support
feat: Supports making requests to the Automatic1111 backend when running with the --api-auth parameter
2024-06-24 19:40:55 -07:00
Timothy Jaeryang Baek
d17dc59246 Merge pull request #2574 from cheahjs/feat/oauth
feat: experimental SSO support for Google, Microsoft, and OIDC
2024-06-24 19:05:58 -07:00
Timothy Jaeryang Baek
09082a070b Merge pull request #3407 from jonathan-rohde/feat/case-insensitive-tag-selection
feat/case insensitive tag selection
2024-06-24 18:52:54 -07:00
Timothy J. Baek
a40d0ec3cb chore: requirements 2024-06-24 17:53:07 -07:00
Timothy J. Baek
bf4dcc10af fix: modal esc 2024-06-24 17:39:43 -07:00
Timothy J. Baek
503d16f49c chore: 333
Co-Authored-By: Jannes Höke <3945220+jh0ker@users.noreply.github.com>
2024-06-24 16:06:15 -07:00
Timothy J. Baek
9504c9c9b4 fix 2024-06-24 15:32:49 -07:00
Timothy J. Baek
284ab648b6 fix 2024-06-24 15:21:42 -07:00
Timothy J. Baek
0f4f01cee9 fix 2024-06-24 14:00:48 -07:00
Timothy J. Baek
68b3cce0fd fix: query memory 2024-06-24 14:00:08 -07:00
Timothy J. Baek
d361404a60 refac 2024-06-24 13:33:03 -07:00
Timothy J. Baek
465c3a9987 refac: playground styling 2024-06-24 13:11:49 -07:00
Timothy J. Baek
4c9fa6cf37 enh: pipe handling 2024-06-24 12:56:41 -07:00
Timothy J. Baek
837944dcc5 fix 2024-06-24 12:26:07 -07:00
Timothy J. Baek
9fb30d4787 fix 2024-06-24 12:23:12 -07:00
Timothy J. Baek
345b3491a7 fix: i18n 2024-06-24 12:21:19 -07:00
Timothy Jaeryang Baek
b4c9e46357 Merge pull request #3408 from SimonOriginal/dev
Added i18n keys and update of Ukrainian translation
2024-06-24 11:39:04 -07:00
Timothy J. Baek
f6082579c9 fix: wakelock 2024-06-24 11:29:21 -07:00
Timothy J. Baek
1c4e7f0324 refac 2024-06-24 11:17:18 -07:00
Simon
920fc29bdd Merge branch 'dev' into dev 2024-06-24 19:50:35 +02:00
SimonOriginal
d9c2b6e890 Add translation 2024-06-24 19:49:29 +02:00
Timothy J. Baek
74a4f642fd refac: valves save 2024-06-24 10:47:40 -07:00
SimonOriginal
e0e9fcaa24 Add keys i18n 2024-06-24 19:38:26 +02:00
Timothy J. Baek
6bad71adca fix 2024-06-24 10:37:57 -07:00
SimonOriginal
fbbffc4386 Add key adn translation 2024-06-24 19:04:31 +02:00
SimonOriginal
8dc9735873 start parser i18n 2024-06-24 18:24:21 +02:00
SimonOriginal
5e9e1108a3 fix errors 2024-06-24 18:21:29 +02:00
SimonOriginal
855b5508d0 start parser 2024-06-24 18:14:19 +02:00
SimonOriginal
2ece4e7cbb Add Ukrainian translation for keys + formatin 2024-06-24 18:12:39 +02:00
SimonOriginal
665f87e84a add toasts keys i18n 2024-06-24 18:09:45 +02:00
SimonOriginal
3d1de6144e Add Ukrainian translation for keys 2024-06-24 17:25:23 +02:00
SimonOriginal
9c81d84e16 Add toasts i18n key 2024-06-24 17:18:39 +02:00
Jonathan Rohde
c7855b3b9d feat(chat): formatting 2024-06-24 16:47:02 +02:00
Jonathan Rohde
09e95b8d3c feat(chat): ignore upper/lower case to select document/tag/collection 2024-06-24 16:44:44 +02:00
SimonOriginal
85b3b81617 fix key Database 2024-06-24 16:23:08 +02:00
SimonOriginal
06a337061b Add keys i18n 2024-06-24 16:19:56 +02:00
SimonOriginal
a8198d7a7f Update of Ukrainian translation 2024-06-24 16:07:51 +02:00
Jun Siang Cheah
79f8620b34 fix: format 2024-06-24 18:50:55 +08:00
Jun Siang Cheah
ca8c098f62 refac: update migrations to match dev 2024-06-24 18:48:28 +08:00
Jun Siang Cheah
f26d80dcae Merge remote-tracking branch 'upstream/dev' into feat/oauth 2024-06-24 18:46:48 +08:00
Timothy J. Baek
f54a66b86b chore: format 2024-06-23 23:31:56 -07:00
Timothy Jaeryang Baek
655ba475cc Merge pull request #3402 from josephrocca/patch-1
fix: scrollToBottom button container obstructing clicks on buttons beneath it
2024-06-23 23:30:33 -07:00
Timothy J. Baek
a82154de3f refac 2024-06-23 23:15:24 -07:00
josephrocca
82b44740db Fix scrollToBottom button container obstructing clicks on buttons beneath it 2024-06-24 14:10:32 +08:00
Timothy J. Baek
f33ca4c9a5 refac 2024-06-23 23:10:26 -07:00
Timothy J. Baek
bc73cb1390 enh: iframe message event listener 2024-06-23 21:48:06 -07:00
Timothy J. Baek
37a052327a chore: format 2024-06-23 21:14:31 -07:00
Timothy J. Baek
51adb1c04a enh: frontmatter version 2024-06-23 21:07:22 -07:00
Timothy J. Baek
9788633ce1 refac: wording 2024-06-23 21:01:07 -07:00
Timothy J. Baek
0b8f5c2232 enh: manifest modal 2024-06-23 21:00:34 -07:00
Timothy J. Baek
be2340d4ef refac: styling 2024-06-23 20:40:12 -07:00
Timothy J. Baek
5c0015cd66 fix: frontmatter 2024-06-23 20:37:41 -07:00
Timothy J. Baek
abf212c28f enh: tools & functions frontmatter 2024-06-23 20:31:40 -07:00
Timothy J. Baek
8b99870189 enh: filter function priority valve support 2024-06-23 20:11:08 -07:00
Timothy J. Baek
f4a2ae5eac enh: list valve 2024-06-23 19:49:48 -07:00
Timothy J. Baek
0250f69da0 fix: valves 2024-06-23 19:48:16 -07:00
Jun Siang Cheah
99e7b328a4 refac: add better logging for oauth errors 2024-06-24 10:43:53 +08:00
Timothy J. Baek
5f2d37dce5 fix: valves 2024-06-23 19:37:35 -07:00
Timothy J. Baek
2eb15ea1fc feat: SAFE_MODE 2024-06-23 19:28:33 -07:00
Timothy J. Baek
ab700a16be refac: all valves input should be required 2024-06-23 19:20:09 -07:00
Timothy J. Baek
0cf936f9e8 refac 2024-06-23 19:18:13 -07:00
Timothy J. Baek
26e735618e fix: tools valves 2024-06-23 19:10:52 -07:00
Timothy J. Baek
db9e5e008f refac: styling 2024-06-23 19:09:53 -07:00
Timothy Jaeryang Baek
65dbf9ba3f Merge pull request #3400 from open-webui/valves
feat: tools & functions valves
2024-06-23 19:07:32 -07:00
Timothy J. Baek
dc25f44d31 fix: handle default 2024-06-23 19:05:56 -07:00
Timothy J. Baek
627705a347 feat: valves 2024-06-23 19:02:27 -07:00
Timothy J. Baek
3a629ffe00 feat: global filter 2024-06-23 18:39:27 -07:00
Timothy J. Baek
d8c112d8b0 feat: function toggle support 2024-06-23 18:34:42 -07:00
Timothy J. Baek
3034f3d310 refac: styling 2024-06-23 18:06:41 -07:00
Timothy J. Baek
120b1857b2 enh: valves 2024-06-23 18:05:33 -07:00
Timothy J. Baek
58e69230a9 refac: styling 2024-06-23 17:44:31 -07:00
Timothy J. Baek
fc2001da83 refac: styling 2024-06-23 17:41:59 -07:00
Timothy J. Baek
5d7de927e5 enh: valve field desc support 2024-06-23 17:34:25 -07:00
Timothy J. Baek
7cd4a3cd1a chore: requirements 2024-06-23 17:22:11 -07:00
Timothy J. Baek
e52ed258f0 refac: styling 2024-06-23 17:18:36 -07:00
Timothy J. Baek
9c57402269 chore: format 2024-06-23 16:04:33 -07:00
Timothy J. Baek
7a218d77b7 refac: workspace 2024-06-23 16:03:21 -07:00
Timothy J. Baek
ecf1d89a8b refac: prompts ui 2024-06-23 15:57:16 -07:00
Timothy J. Baek
57455f084b refac: functions ui 2024-06-23 15:51:19 -07:00
Timothy J. Baek
44e3d384d5 refac: tools ui 2024-06-23 15:45:17 -07:00
Timothy J. Baek
8e2c377a21 refac: extractSentences 2024-06-22 16:33:20 -07:00
Timothy J. Baek
6ee94c5e97 chore: format 2024-06-22 16:15:19 -07:00
Timothy J. Baek
6e084b4a73 enh: voice call skip code block & expression 2024-06-22 16:13:13 -07:00
Timothy Jaeryang Baek
fd96c9c68d Merge pull request #3380 from Yash-1511/main
feat: add jina_search as new websearch provider
2024-06-22 15:19:38 -07:00
Timothy J. Baek
60e5adc70e enh: files api allow filename 2024-06-22 14:49:00 -07:00
Timothy J. Baek
de367e488d fix 2024-06-22 14:08:23 -07:00
Timothy J. Baek
df71d7c63b fix 2024-06-22 14:07:11 -07:00
Timothy J. Baek
5b64c28f33 refac 2024-06-22 14:06:19 -07:00
Timothy J. Baek
03bb4bcda6 refac: tool specs 2024-06-22 13:47:57 -07:00
Timothy J. Baek
2d795cffe0 refac: styling 2024-06-22 13:41:27 -07:00
Timothy J. Baek
bcefa71b03 refac: styling 2024-06-22 13:39:17 -07:00
Timothy J. Baek
b441511359 enh: wake lock 2024-06-22 13:35:18 -07:00
Timothy J. Baek
3af8c5b03d refac: wording 2024-06-22 13:22:26 -07:00
Timothy J. Baek
d0f34baaa3 feat: voice interruption toggle 2024-06-22 13:21:36 -07:00
Timothy J. Baek
98c18b3032 fix: web search 2024-06-22 13:13:46 -07:00
Timothy J. Baek
c1971fd8d7 refac: show default instead of none 2024-06-22 13:01:26 -07:00
Timothy J. Baek
cf6447eb2a feat: function exception handler 2024-06-22 12:43:30 -07:00
Timothy J. Baek
9205b90af6 fix 2024-06-22 12:26:03 -07:00
Timothy J. Baek
6ce91de7e0 fix 2024-06-22 12:25:02 -07:00
Timothy J. Baek
f524238910 fix 2024-06-22 12:24:46 -07:00
Timothy J. Baek
646832ba8c refac 2024-06-22 12:23:37 -07:00
Timothy J. Baek
6ccb5e8f67 feat: user valves support 2024-06-22 12:14:12 -07:00
Timothy J. Baek
d362fd027e feat: user valves integration 2024-06-22 12:08:32 -07:00
Timothy J. Baek
15fc23df87 feat: user valves endpoints 2024-06-22 11:26:33 -07:00
Yash-1511
7c9fb9199e feat: add jina_search as new websearch provider 2024-06-22 20:06:15 +05:30
Timothy J. Baek
8345bb55d4 refac: styling 2024-06-22 02:29:33 -07:00
Timothy J. Baek
e4af3852f7 refac: allow class in tools 2024-06-22 02:29:22 -07:00
Timothy J. Baek
0ac8e97447 refac: styling 2024-06-22 01:43:40 -07:00
Timothy J. Baek
a8a451344c refac 2024-06-22 01:42:28 -07:00
Timothy J. Baek
ae567796ee refac 2024-06-22 01:39:53 -07:00
Timothy Jaeryang Baek
9220fdc4a4 Merge pull request #3367 from aleixdorca/dev
i18n: Updated Catalan Translation
2024-06-21 23:26:45 -07:00
aleix
b06d7dd56a i18n: Update Catalan Translation 2024-06-21 20:52:25 +02:00
Timothy J. Baek
14fd3a8aca refac 2024-06-21 11:05:55 -07:00
Timothy Jaeryang Baek
74ba2d6548 Merge pull request #3346 from KarlLee830/translate
i18n: Update Chinese translation
2024-06-21 11:02:04 -07:00
Timothy Jaeryang Baek
372bbcdf34 Merge pull request #3364 from PeterDaveHello/Improve-zh-TW
i18n: Improve Traditional Chinese(zh-TW) locale
2024-06-21 11:01:12 -07:00
Peter Dave Hello
0c7df2cde2 Improve Traditional Chinese(zh-TW) locale 2024-06-22 01:44:34 +08:00
Jun Siang Cheah
981f384154 refac: modify oauth login logic for unique email addresses 2024-06-21 18:25:19 +01:00
Jun Siang Cheah
e011e7b695 fix: set auth cookie during oauth login 2024-06-21 14:35:57 +01:00
Jun Siang Cheah
416e8d1ef9 fix: db migration sync with dev 2024-06-21 14:35:57 +01:00
Jun Siang Cheah
49a00d61ac feat: show oauth sub in admin panel 2024-06-21 14:35:57 +01:00
Jun Siang Cheah
983112d17c feat: fetch and store oauth profile pictures as data URLs 2024-06-21 14:35:54 +01:00
Jun Siang Cheah
922dfae51c fix: broken tuple expansion 2024-06-21 13:44:10 +01:00
Jun Siang Cheah
4aab460905 Merge remote-tracking branch 'upstream/dev' into feat/oauth 2024-06-21 13:43:19 +01:00
Karl Lee
f0b5008fc8 i18n: Update Chinese translation 2024-06-21 15:44:06 +08:00
Karl Lee
4d66e39402 i18n: Update Chinese translation 2024-06-21 11:45:57 +08:00
Timothy J. Baek
a2ea6b1b5b enh: tool async support 2024-06-20 20:40:03 -07:00
Timothy J. Baek
4370f233a1 feat: pipe async support 2024-06-20 20:37:04 -07:00
Timothy J. Baek
5621025c12 feat: async filter support 2024-06-20 20:26:28 -07:00
Timothy J. Baek
6bb2f41812 feat: tool citation 2024-06-20 14:14:12 -07:00
Timothy J. Baek
58ae91369e refac 2024-06-20 13:49:04 -07:00
Timothy Jaeryang Baek
c72e911f6a Merge pull request #3337 from SimonOriginal/dev
Added {$i18n.t('')} keys, updated Ukrainian translation
2024-06-20 13:19:37 -07:00
Timothy J. Baek
f1de635988 refac: cookie 2024-06-20 13:14:58 -07:00
SimonOriginal
f62d033e6b Fixed indentation in About.svelte 2024-06-20 22:05:48 +02:00
SimonOriginal
7c304d225b fix key {.t('Created by')} 2024-06-20 21:57:33 +02:00
SimonOriginal
89b259acb8 add key i18n + update translation 2024-06-20 21:53:34 +02:00
SimonOriginal
571dfd8a69 ukrainian translation update 2024-06-20 21:51:29 +02:00
SimonOriginal
e29e54a8d2 add key i18n 2024-06-20 21:49:52 +02:00
SimonOriginal
05e251da0e fix translation 2024-06-20 21:44:01 +02:00
Timothy J. Baek
f342f8adc7 refac: code highlight optimisation 2024-06-20 12:27:34 -07:00
Timothy Jaeryang Baek
deb4e36095 Merge pull request #3331 from SimonOriginal/dev 2024-06-20 11:25:30 -07:00
Timothy Jaeryang Baek
619ee152b7 Merge pull request #3332 from KarlLee830/translate 2024-06-20 11:22:19 -07:00
Timothy Jaeryang Baek
6e04ca85cb Merge pull request #3333 from perfectra1n/dev 2024-06-20 11:21:51 -07:00
perf3ct
9f39bcb60e Fix /tags endpoint in postgres integration test
/api/health it is

/health it is
2024-06-20 09:30:30 -07:00
Karl Lee
64814098a3 i18n: Update Chinese translation 2024-06-20 23:58:35 +08:00
SimonOriginal
1bd6626ca5 Ukrainian translation update to v0.3.5 2024-06-20 17:36:17 +02:00
sihuangwlp
36de6576b8 fix issue with i18n 2024-06-20 21:21:37 +08:00
Timothy J. Baek
71a7750c9d refac: styling 2024-06-20 05:05:16 -07:00
Timothy J. Baek
5a2c2770a4 chore: format 2024-06-20 04:53:23 -07:00
Timothy Jaeryang Baek
09a81eb225 Merge pull request #3321 from open-webui/functions
feat: functions
2024-06-20 04:52:32 -07:00
Timothy J. Baek
9ebd308d28 refac 2024-06-20 04:51:51 -07:00
Timothy J. Baek
59fa2f8f26 refac: pipe function support 2024-06-20 04:47:40 -07:00
Timothy J. Baek
d6e4aef607 feat: pipe function 2024-06-20 04:38:59 -07:00
Timothy J. Baek
de26a78a16 refac 2024-06-20 04:21:55 -07:00
Timothy J. Baek
c689356b31 refac 2024-06-20 03:57:36 -07:00
Timothy J. Baek
015772ef9a refac 2024-06-20 03:45:13 -07:00
Timothy J. Baek
e20baad601 refac 2024-06-20 03:35:21 -07:00
Timothy J. Baek
a3f09949c0 refac 2024-06-20 03:29:50 -07:00
Timothy J. Baek
afd270523c feat: filter func outlet 2024-06-20 03:23:50 -07:00
Timothy J. Baek
3101ff143b refac: disable continuing with error message 2024-06-20 02:47:27 -07:00
Timothy J. Baek
f14ca48334 refac 2024-06-20 02:42:56 -07:00
Timothy J. Baek
96d7c3e99f fix: raise error 2024-06-20 02:37:36 -07:00
Timothy J. Baek
c4bd60114e feat: filter inlet support 2024-06-20 02:30:00 -07:00
Timothy J. Baek
6b8a7b9939 refac: chat completion middleware 2024-06-20 02:06:10 -07:00
Timothy J. Baek
448ca9d836 refac 2024-06-20 01:51:39 -07:00
Timothy J. Baek
08cc20cb93 feat: filter selector model 2024-06-20 01:44:52 -07:00
Timothy J. Baek
bf5775e07a refac 2024-06-20 01:16:31 -07:00
Timothy J. Baek
9108df177c refac: comments 2024-06-20 01:12:09 -07:00
Timothy J. Baek
40cde07e5c feat: function filter example boilerplate 2024-06-20 01:07:55 -07:00
Timothy J. Baek
43e08c6afa refac 2024-06-20 00:54:58 -07:00
Timothy J. Baek
27f8afebab feat: function db migration 2024-06-20 00:49:11 -07:00
Timothy J. Baek
f68aba687e feat: functions router 2024-06-20 00:37:02 -07:00
sihuangwlp
e16ae92edc complate missing field AUTOMATIC1111_API_AUTH 2024-06-20 14:53:38 +08:00
sihuangwlp
d3160166e9 Complete missing commas 2024-06-20 14:38:13 +08:00
sihuangwlp
bec04279aa Add AUTOMATIC1111_API_AUTH support 2024-06-20 14:15:49 +08:00
Timothy J. Baek
f9283bc311 enh: pipeline user email support 2024-06-19 17:19:35 -07:00
Timothy J. Baek
d5b76b5ed2 enh: iframe support 2024-06-19 16:51:29 -07:00
Timothy J. Baek
7b64b40270 refac 2024-06-19 15:26:35 -07:00
Timothy J. Baek
e3668a2f1c refac 2024-06-19 14:56:51 -07:00
Timothy J. Baek
ab270c1682 fix 2024-06-19 14:49:35 -07:00
Timothy Jaeryang Baek
1e0453221d Merge pull request #3221 from perfectra1n/feature-external-db-reconnect
feat: external db reconnect
2024-06-19 14:40:03 -07:00
Timothy J. Baek
b36c525ebc enh: cookie auth 2024-06-19 14:38:09 -07:00
Timothy J. Baek
1b100660af fix: image 2024-06-19 13:31:01 -07:00
Timothy Jaeryang Baek
1007f67ab1 Merge pull request #3301 from KarlLee830/translate
i18n: Update Chinese translation
2024-06-19 12:24:44 -07:00
Karl Lee
ac37b50be2 Update Chinese translation 2024-06-20 01:17:26 +08:00
Timothy Jaeryang Baek
ece48d640a Merge pull request #3293 from que-nguyen/patch-1
Refine and update Vietnamese translations in translation.json
2024-06-19 08:49:39 -07:00
Que Nguyen
8125fd3554 Update: Refine and update Vietnamese translations 2024-06-19 18:23:47 +07:00
Que Nguyen
9e87012489 Fix: Rename 'whitelist' to 'filter_list' in function 2024-06-19 18:22:29 +07:00
Timothy J. Baek
b6ad539379 refac 2024-06-18 19:44:32 -07:00
Timothy J. Baek
ce659b9e1c chore: format 2024-06-18 19:20:20 -07:00
Timothy J. Baek
dad7af6de1 enh: tool __model__ param support 2024-06-18 18:50:36 -07:00
Timothy J. Baek
dcac1a3cb7 enh: tool __id__ param support for cache dir 2024-06-18 18:14:18 -07:00
Timothy J. Baek
6f9a31eba5 feat: tool cache dir 2024-06-18 18:07:51 -07:00
Timothy J. Baek
eef125d085 refac: settings order 2024-06-18 17:48:56 -07:00
Timothy J. Baek
ff9b94bab7 refac: playground 2024-06-18 17:41:47 -07:00
Timothy J. Baek
d1eb2c2207 fix: model selector focus 2024-06-18 17:16:33 -07:00
Timothy J. Baek
807cfdecfb fix 2024-06-18 17:09:29 -07:00
Timothy J. Baek
31f768bf8a fix 2024-06-18 17:07:13 -07:00
Timothy J. Baek
a2e1ea103c feat: tools file handler support 2024-06-18 16:45:03 -07:00
Timothy J. Baek
d6ab954f81 refac: styling 2024-06-18 16:11:10 -07:00
Timothy J. Baek
514c7f1520 fix: rag 2024-06-18 16:08:42 -07:00
Timothy J. Baek
bcc27e3852 fix 2024-06-18 15:48:25 -07:00
Timothy J. Baek
35026849df refac: styling 2024-06-18 15:39:50 -07:00
Timothy J. Baek
d5a1030000 refac: uploads delete 2024-06-18 15:20:04 -07:00
Timothy J. Baek
9fa8633dcb fix: styling 2024-06-18 15:11:59 -07:00
Timothy J. Baek
1a22ae54a2 fix: styling 2024-06-18 14:58:19 -07:00
Timothy J. Baek
20e4f6cc16 refac 2024-06-18 14:55:18 -07:00
Timothy J. Baek
eb21750466 fix: files 2024-06-18 14:38:23 -07:00
Timothy J. Baek
d93160799f refac: files 2024-06-18 14:37:12 -07:00
Timothy J. Baek
b4bdea6d85 fix: files 2024-06-18 14:33:44 -07:00
Timothy J. Baek
83986620ee refac 2024-06-18 14:15:08 -07:00
Timothy J. Baek
9e7b7a895e refac: file upload 2024-06-18 13:50:18 -07:00
Timothy Jaeryang Baek
1000bcaeb7 Merge pull request #3274 from KarlLee830/translate
i18n: Update Chinese translation
2024-06-18 13:07:16 -07:00
perfectra1n
7e061d19ca Merge branch 'open-webui:main' into feature-external-db-reconnect 2024-06-18 12:11:28 -07:00
Timothy J. Baek
7c2a198370 feat: file db migration 2024-06-18 11:38:50 -07:00
Timothy J. Baek
146e550239 feat: files endpoint 2024-06-18 11:36:55 -07:00
Karl Lee
4d9220c2c8 i18n: Update Chinese translation 2024-06-19 02:21:34 +08:00
Karl Lee
3c0678fb84 i18n: Update Chinese translation 2024-06-19 02:17:18 +08:00
Karl Lee
6985567758 Update Chinese translation 2024-06-19 02:10:01 +08:00
Karl Lee
5a5bf20d42 chore: format 2024-06-19 02:03:33 +08:00
Timothy J. Baek
ba7091c25b refac 2024-06-18 10:36:06 -07:00
Timothy Jaeryang Baek
e076803f98 Merge pull request #3256 from shing100/dev
i18n: Update Korean Localization for Open-WebUI
2024-06-18 10:34:33 -07:00
Timothy J. Baek
453f9be16c refac 2024-06-18 10:26:53 -07:00
Timothy J. Baek
19b67f4975 feat: functions scaffold 2024-06-18 10:23:04 -07:00
Geun, Lim
0a3390947c i18n: Korean Localization for Open-WebUI
In the updated translation, we changed the theme to own language and changed the other language a little more naturally.
2024-06-18 14:56:37 +09:00
Timothy J. Baek
47d9c9fc74 refac 2024-06-17 20:46:42 -07:00
perf3ct
81b2416923 format 2024-06-17 16:47:09 -07:00
perf3ct
59c6ff727a borrow some of the previous PRs reconnection code 2024-06-17 16:44:20 -07:00
Timothy J. Baek
493fe562ac refac 2024-06-17 14:39:21 -07:00
Timothy J. Baek
b1d83fc42c chore: format 2024-06-17 14:32:23 -07:00
Timothy Jaeryang Baek
20f052eb37 Merge pull request #3112 from que-nguyen/searxng
Domain whitelisting for web search results
2024-06-17 14:30:17 -07:00
Timothy J. Baek
edfe20b2e1 fix 2024-06-17 14:28:56 -07:00
Timothy J. Baek
a4748af822 chore: bump pyodide 2024-06-17 14:26:10 -07:00
Timothy J. Baek
46e570bd04 fix: styling 2024-06-17 13:52:17 -07:00
Timothy J. Baek
686c5081e6 fix: model system prompt variable support 2024-06-17 13:47:48 -07:00
Timothy J. Baek
84bd4994cd refac: toolkit editor 2024-06-17 13:38:07 -07:00
Timothy J. Baek
55dfc2013a enh: __messages__ support for tools 2024-06-17 13:28:29 -07:00
Timothy J. Baek
4a3362f889 fix: help 2024-06-17 11:46:23 -07:00
Timothy J. Baek
44e145c6e9 fix: styling 2024-06-17 11:32:10 -07:00
Timothy J. Baek
eb98631ff6 refac 2024-06-17 11:11:54 -07:00
perf3ct
48e1356ed9 add logging for user upon db connection 2024-06-17 10:34:19 -07:00
perf3ct
5c655f298b stop even using pooled DBs in peewee 2024-06-17 09:56:31 -07:00
Timothy Jaeryang Baek
cc4e192a51 Merge pull request #3235 from seokho-son/dev 2024-06-17 08:53:42 -07:00
perf3ct
981866eb93 use autoconnect and stop using the mixin 2024-06-17 07:50:47 -07:00
Seokho Son
cc98e89baa Complete Korean Localization for Open-WebUI
Signed-off-by: Seokho Son <shsongist@gmail.com>
2024-06-17 22:00:53 +09:00
Timothy J. Baek
d8a1d7eb36 feat: character utils 2024-06-17 03:05:09 -07:00
Timothy J. Baek
2f7120a73a refac 2024-06-17 02:54:56 -07:00
Timothy J. Baek
a60d6c316e fix 2024-06-17 02:28:12 -07:00
Timothy J. Baek
ab62228877 feat: character card support 2024-06-17 02:26:07 -07:00
Timothy J. Baek
f1b350cbe6 enh: use model profile image in call 2024-06-17 01:55:06 -07:00
Timothy J. Baek
9023a60d0d refac 2024-06-17 01:42:04 -07:00
Timothy J. Baek
49fdb68908 chore: format 2024-06-17 01:36:43 -07:00
Timothy Jaeryang Baek
db7d63d11a Merge pull request #3227 from KarlLee830/translate
Add some missing i18n keys and update Chinese translation
2024-06-17 01:35:03 -07:00
Karl Lee
720ff35edf Add some missing i18n keys and update Chinese translation
Add some missing i18n keys and update Chinese translation
2024-06-17 16:32:36 +08:00
Timothy J. Baek
5fa355e1ae feat: background image 2024-06-17 01:31:22 -07:00
Que Nguyen
c487385980 Set filter_list as optional param in serpstack.py 2024-06-17 14:38:11 +07:00
Que Nguyen
bcb84235b1 Set filter_list as optional param in serply.py 2024-06-17 14:37:52 +07:00
Que Nguyen
6b8290fa6d Set filter_list as optional param in serper.py 2024-06-17 14:37:26 +07:00
Que Nguyen
9c446d9fb4 Set filter_list as optional param in searxng.py 2024-06-17 14:36:56 +07:00
Que Nguyen
3cc0e3ecb6 Refactor rag/main.py
Renamed function get_filtered_results
2024-06-17 14:36:26 +07:00
Que Nguyen
d8beed13b4 Set filter_list as optional param in google_pse.py 2024-06-17 14:35:27 +07:00
Que Nguyen
7d2ad8c4bf Set filter_list as optional param in duckduckgo.py 2024-06-17 14:34:59 +07:00
Que Nguyen
a02139ba9d Set filter_list as optional param in brave.py 2024-06-17 14:34:17 +07:00
Que Nguyen
b3d136b3b3 Refactored config.py
Renamed RAG_WEB_SEARCH_WHITE_LIST_DOMAINS to RAG_WEB_SEARCH_DOMAIN_FILTER_LIST
2024-06-17 14:33:23 +07:00
Que Nguyen
a3ac9ee774 Refactor main.py
Rename RAG_WEB_SEARCH_WHITE_LIST_DOMAINS to RAG_WEB_SEARCH_DOMAIN_FILTER_LIST
2024-06-17 14:31:44 +07:00
Que Nguyen
75e51ecf6d Merge branch 'open-webui:main' into searxng 2024-06-17 14:28:02 +07:00
Timothy J. Baek
a28ad06bf0 fix 2024-06-16 23:36:21 -07:00
Timothy Jaeryang Baek
9e4dd4b86f Merge pull request #3159 from open-webui/dev
0.3.5
2024-06-16 22:52:44 -07:00
Timothy J. Baek
4559c8af74 doc: changelog 2024-06-16 22:52:27 -07:00
Timothy J. Baek
4a67ae1195 fix: message delete issue 2024-06-16 22:28:26 -07:00
Timothy J. Baek
1efa25eed5 chore: format 2024-06-16 21:55:08 -07:00
Timothy J. Baek
54b65a89fd refac: call overlay 2024-06-16 21:52:50 -07:00
Timothy Jaeryang Baek
b4008b0b30 Merge pull request #3140 from ricky-davis/sortable-users
feat: Sortable users in admin panel
2024-06-16 21:49:08 -07:00
Timothy Jaeryang Baek
16c95991d3 Merge pull request #3145 from ricky-davis/sortable-chats
feat: Sortable chats in admin panel
2024-06-16 21:48:13 -07:00
Timothy Jaeryang Baek
f3bd81073d Merge pull request #3202 from JohnTheNerd/main
feat: added ability to set user name for federated auth
2024-06-16 17:56:29 -07:00
Timothy J. Baek
6d9ed32929 refac 2024-06-16 17:19:18 -07:00
Timothy J. Baek
ad2ffc33d8 refac: voice call tap to interrupt 2024-06-16 16:50:57 -07:00
Timothy J. Baek
e183b0e5ff chore: format 2024-06-16 15:34:15 -07:00
Timothy J. Baek
4b6b33b08b feat: user_location 2024-06-16 15:32:26 -07:00
perf3ct
10fa887eab fix peewee and playhouse connections to retry 2024-06-16 15:25:48 -07:00
John Karabudak
c00a6fa02a added ability to set user name for federated auth
this commit adds an optional environment variable named `WEBUI_AUTH_TRUSTED_NAME_HEADER`, which sets the user's name to the contents of that header. this only happens if the user is just being created, just like how the trusted e-mail header works.

if the environment variable or header is not present, we fall back to the original behavior which is to re-use the user e-mail address.

Co-Authored-By: Nikita Borzykh <sample@fastmail.com>
2024-06-16 18:44:10 -02:30
Timothy J. Baek
8e62c36148 enh: AIOHTTP_CLIENT_TIMEOUT None support 2024-06-16 13:56:49 -07:00
Timothy Jaeryang Baek
f76a569527 Merge pull request #3219 from JohnTheNerd/comfyui-configuration
feat: added support for Stable Diffusion 3 alongside more extensive ComfyUI configuration
2024-06-16 13:50:38 -07:00
Timothy Jaeryang Baek
9a6c2fafe3 Merge pull request #3204 from JohnTheNerd/fix-github-actions
build: fixed GitHub actions on usernames with uppercase characters
2024-06-16 13:50:00 -07:00
Timothy J. Baek
1c355929fc refac 2024-06-16 12:09:55 -06:00
John Karabudak
ea074fa9bf added Stable Diffusion 3 support alongside ComfyUI configuration
this commit adds four environment variables:

- COMFYUI_CFG_SCALE
- COMFYUI_SAMPLER
- COMFYUI_SCHEDULER
- COMFYUI_SD3 (merely setting this at all will enable SD3 mode)
2024-06-16 15:30:52 -02:30
John Karabudak
60ee08a8ac made the environment variable name IMAGE_NAME as requested 2024-06-16 15:27:13 -02:30
Timothy J. Baek
9f7ef209fa fix: chatlist 2024-06-16 11:36:15 -06:00
Timothy J. Baek
fc1a66ea76 feat: current_time, current_datetime 2024-06-16 10:39:48 -06:00
Timothy J. Baek
5d0b77e64a fix: shift delete 2024-06-16 10:27:34 -06:00
Timothy J. Baek
a4810a5e42 refac 2024-06-16 10:24:16 -06:00
Timothy Jaeryang Baek
39c466d542 Merge pull request #3212 from robbie-cahill/patch-1
Add host.docker.internal overide for docker dev
2024-06-16 09:07:59 -07:00
Timothy Jaeryang Baek
c8418da8c1 Merge pull request #3217 from theasp/better-body-logging
chore: Log API request bodies at debug level
2024-06-16 09:07:22 -07:00
perf3ct
75d713057c Merge remote-tracking branch 'upstream/main' into feature-external-db-reconnect 2024-06-16 09:03:57 -07:00
Andrew Phillips
c0c875eae2 Use log.debug() for logging request bodies for the backend API 2024-06-16 12:40:16 -03:00
Andrew Phillips
3eba963d03 Remove redundant logging 2024-06-16 12:38:20 -03:00
Robbie
78a145f181 Add host.docker.internal overide 2024-06-16 21:16:07 +10:00
Jun Siang Cheah
4ff17acc1b Merge remote-tracking branch 'upstream/dev' into feat/oauth 2024-06-16 08:31:05 +01:00
Timothy Jaeryang Baek
9928114ca8 Merge pull request #3205 from Peter-De-Ath/chat-selectors-dark-mode
style: update chat selectors dark mode colors
2024-06-15 22:33:22 -07:00
Timothy J. Baek
e5f4f64102 enh: remove trailing slash 2024-06-15 23:32:12 -06:00
Peter De-Ath
7a98c1750f enh: update dark mode colors on chat selectors - models 2024-06-16 03:18:15 +01:00
Peter De-Ath
ee279b7976 enh: update dark mode colors on chat selectors 2024-06-16 02:55:27 +01:00
John Karabudak
dbfb6d5993 fixed GitHub actions on usernames with uppercase characters 2024-06-15 22:42:29 -02:30
Que Nguyen
a02ba52de8 Merge branch 'dev' into searxng 2024-06-15 23:44:31 +07:00
Timothy J. Baek
1275371e10 fix: md model desc 2024-06-15 10:43:10 -06:00
Timothy J. Baek
f56da1a39f refac 2024-06-15 04:41:48 -06:00
Timothy J. Baek
fe9685867e enh: model desc md support 2024-06-15 04:32:18 -06:00
Timothy J. Baek
dfa2cf9e6d refac: AIOHTTP_CLIENT_TIMEOUT default value should match 2024-06-15 04:25:21 -06:00
Timothy J. Baek
262501304c enh: model preset delete confirmation 2024-06-15 04:13:30 -06:00
Timothy J. Baek
a0dbc970a9 enh: model delete confirmation 2024-06-15 04:07:40 -06:00
Timothy J. Baek
2abb6788fe enh: user delete confirmation 2024-06-15 04:04:29 -06:00
Timothy J. Baek
91cec11500 refac 2024-06-15 04:02:20 -06:00
Timothy J. Baek
68aca76805 fix 2024-06-15 03:39:21 -06:00
Timothy J. Baek
2f501aee14 chore: format 2024-06-15 03:36:17 -06:00
Timothy J. Baek
3c599e24e5 refac 2024-06-15 03:35:44 -06:00
Timothy Jaeryang Baek
a6ee7415d8 Merge pull request #3116 from Peter-De-Ath/memories-edit
feat: add abilty to edit memories
2024-06-15 01:58:57 -07:00
Timothy J. Baek
d3a3d27f70 chore: format 2024-06-15 02:55:02 -06:00
Timothy Jaeryang Baek
16f9dcf17a Merge pull request #3184 from PierreMesure/better-swedish
i18n: Better Swedish translation 🇸🇪
2024-06-15 01:53:27 -07:00
Jun Siang Cheah
f49d814dc0 Merge remote-tracking branch 'upstream/main' into feat/oauth 2024-06-15 09:51:18 +01:00
Pierre Mesure
cb988e58ba Better Swedish translation 2024-06-15 09:13:27 +02:00
Peter De-Ath
bec00e7e64 fix: change update_memory to correct naming convention
fix: update update_memory to POST
2024-06-14 21:23:34 +01:00
Timothy J. Baek
7ba98ad498 feat: chat item shift shorcut 2024-06-14 13:02:07 -07:00
Timothy J. Baek
aba2ca39d4 enh: delete confirm dialog 2024-06-14 11:24:15 -07:00
Timothy Jaeryang Baek
8db439a0d1 Merge pull request #3177 from Yash-1511/main
feat: add tavily web search in web search provider
2024-06-14 11:09:59 -07:00
Timothy Jaeryang Baek
aec8e47fbb Merge pull request #3174 from KarlLee830/translate
Update Chinese translation
2024-06-14 11:09:22 -07:00
Timothy J. Baek
d03a00758f refac: sidebar 2024-06-14 11:06:19 -07:00
Yash-1511
717bbe62a6 doc: update readme 2024-06-14 20:51:50 +05:30
Yash-1511
b9da72560a feat: add tavily web search in web search provider 2024-06-14 20:44:11 +05:30
Karl Lee
460bc43846 Update Chinese translation
Update Chinese translation
2024-06-14 22:18:56 +08:00
Timothy Jaeryang Baek
26505079b8 Merge pull request #3107 from TheTerrasque/defect/ollama-long-response-timeout
Fix: ollama long response timeout
2024-06-14 02:45:55 -07:00
Timothy J. Baek
3de4c6189b fix: voice input sensitivity 2024-06-14 02:41:27 -07:00
Timothy J. Baek
2e4f060ebb feat: emoji call 2024-06-14 02:30:36 -07:00
Timothy Jaeryang Baek
53858c9b0e Merge pull request #3161 from sammcj/num_batch
feat: add num_keep, num_batch
2024-06-14 00:14:46 -07:00
Timothy J. Baek
454a386612 refac 2024-06-14 00:10:52 -07:00
Timothy J. Baek
346caaa1db chore: format 2024-06-14 00:07:46 -07:00
Timothy J. Baek
42ba43fc81 fix 2024-06-14 00:05:01 -07:00
Sam McLeod
3f5e36271f feat: add num_keep, num_batch 2024-06-14 16:21:20 +10:00
Timothy J. Baek
18ae5860bc refac 2024-06-13 20:16:23 -07:00
Timothy J. Baek
7f70de99d3 refac: voice call 2024-06-13 20:15:23 -07:00
rdavis
385fcfe8d0 Swapped from inline style to using tailwind class. 2024-06-14 00:29:31 +00:00
rdavis
09a6206d28 Merge branch 'sortable-users' of github.com:ricky-davis/open-webui into sortable-users 2024-06-13 19:27:23 -05:00
rdavis
b0d9aa38d2 Swapped from inline style to using tailwind class. 2024-06-14 00:07:21 +00:00
rdavis
26575c5086 Changed column header text to match property.
Removed debugging code.
2024-06-13 23:59:15 +00:00
rdavis
91d53530e6 Added the ability to sort chats in the admin panel chats modal
Added "Updated at" column to the admin panel chats modal.
2024-06-13 23:24:52 +00:00
rdavis
c07e7221e5 Added the ability to sort users in the admin panel 2024-06-13 15:00:02 -05:00
rdavis
5844d0525a Added the ability to sort users in the admin panel 2024-06-13 19:01:13 +00:00
Timothy Jaeryang Baek
7ea572fdca Merge pull request #3138 from open-webui/main
dev
2024-06-13 11:15:21 -07:00
Timothy J. Baek
162643a4b1 fix 2024-06-13 11:14:33 -07:00
Timothy Jaeryang Baek
4727e5cbb1 Merge pull request #3128 from open-webui/voice-enh
enh: voice
2024-06-13 02:32:23 -07:00
Timothy J. Baek
b8136951e4 refac 2024-06-13 02:29:56 -07:00
Timothy J. Baek
5300d2c531 refac 2024-06-13 01:28:15 -07:00
Timothy J. Baek
d6fd2a8228 refac 2024-06-12 21:18:53 -07:00
Peter De-Ath
493e3068d8 enh: ability to edit memories 2024-06-13 02:42:07 +01:00
Que Nguyen
7b5f434a07 Implement domain whitelisting for web search results 2024-06-13 07:14:48 +07:00
Mikael Turøy
e130ad74d1 Added timeout setting for ollama streaming response 2024-06-12 22:50:15 +02:00
Timothy Jaeryang Baek
a382e82dec Merge pull request #3105 from open-webui/dev
fix/refac: docs
2024-06-12 13:45:43 -07:00
Timothy J. Baek
bdd2ac0015 fix/refac: docs 2024-06-12 13:45:13 -07:00
Timothy Jaeryang Baek
c0a06f7db4 Merge pull request #3103 from open-webui/dev
fix: filter pipeline
2024-06-12 13:35:43 -07:00
Timothy J. Baek
e82027310d fix 2024-06-12 13:34:34 -07:00
Timothy J. Baek
c6c0bc19d8 fix: filter pipeline 2024-06-12 13:31:05 -07:00
Timothy Jaeryang Baek
33bb787b15 Merge pull request #3095 from open-webui/dev
refac
2024-06-12 11:47:47 -07:00
Timothy J. Baek
c794d59fd5 revert: do not change the default 2024-06-12 11:47:19 -07:00
Timothy J. Baek
1fefafb254 refac 2024-06-12 11:44:05 -07:00
Timothy Jaeryang Baek
90dadf0bec Merge pull request #3073 from que-nguyen/searxng
Set searxng language to auto and enable safesearch (moderate).
2024-06-12 11:26:10 -07:00
Timothy Jaeryang Baek
95ff355737 Merge pull request #3094 from open-webui/dev
fix: chat deletion
2024-06-12 11:23:31 -07:00
Timothy J. Baek
c2d41b0376 fix: chat deletion 2024-06-12 11:23:11 -07:00
Timothy Jaeryang Baek
8f3c9b391b Merge pull request #3093 from open-webui/dev
fix
2024-06-12 11:21:44 -07:00
Timothy J. Baek
5a9d883a93 fix 2024-06-12 11:21:23 -07:00
Timothy Jaeryang Baek
c2e8af5ed1 Merge pull request #3090 from open-webui/dev
0.3.4
2024-06-12 11:13:41 -07:00
Timothy J. Baek
70a85d6111 doc: changelog 2024-06-12 11:13:24 -07:00
Timothy J. Baek
1163745a03 revert 2024-06-12 11:08:05 -07:00
Timothy J. Baek
529fcaa5c9 fix: document query save 2024-06-12 11:07:04 -07:00
Timothy J. Baek
681fd4ff3b fix: tools api 2024-06-12 10:55:26 -07:00
Que Nguyen
305ec59d76 Set searxng language as 'auto' and enable safesearch (moderate).
Configure searxng with language param set to auto and add "safesearch": 1 (moderate) for safer web results.
2024-06-12 21:33:33 +07:00
Timothy Jaeryang Baek
0917fa6f4a Merge pull request #3056 from open-webui/dev
refac
2024-06-12 03:15:25 -07:00
Timothy J. Baek
c7d3969a53 refac 2024-06-12 03:13:59 -07:00
Timothy Jaeryang Baek
c41b33c9c0 Merge pull request #3013 from open-webui/dev
0.3.3
2024-06-12 02:14:37 -07:00
Timothy J. Baek
7131ac24a7 chore: format 2024-06-12 02:12:39 -07:00
Timothy J. Baek
501cd2071c fix: fr translation 2024-06-12 02:10:38 -07:00
Timothy J. Baek
85a1233733 refac: styling 2024-06-12 02:09:00 -07:00
Timothy J. Baek
670fa83e2c doc: changelog 2024-06-12 02:05:12 -07:00
Timothy J. Baek
d03a53e755 doc: changelog 2024-06-12 01:58:38 -07:00
Timothy J. Baek
2ec416bd6b doc: readme 2024-06-12 01:49:13 -07:00
Timothy J. Baek
b1a391ca5d doc: readme 2024-06-12 01:46:28 -07:00
Timothy J. Baek
c0ca447041 chore: format 2024-06-12 01:37:53 -07:00
Timothy Jaeryang Baek
5d3db15eca Merge pull request #3049 from que-nguyen/dev
Refactor URL validation function
2024-06-12 01:36:34 -07:00
Timothy J. Baek
eead69068c fix: type casting 2024-06-12 01:35:42 -07:00
Timothy J. Baek
edae1ca4c7 chore: format 2024-06-12 01:32:22 -07:00
Timothy J. Baek
0c9cc2baa3 refac: message 2024-06-12 01:28:30 -07:00
Timothy J. Baek
326ac41422 refac: bypass confirm dialog for edit 2024-06-12 01:22:52 -07:00
Timothy J. Baek
dd2941bb43 feat: import confim dialog 2024-06-12 01:20:12 -07:00
Timothy J. Baek
a691ee08d8 feat: confirm dialog 2024-06-12 01:17:46 -07:00
Timothy J. Baek
e8fc522eba chore: format 2024-06-12 00:18:22 -07:00
Timothy J. Baek
482a41e49b fix 2024-06-12 00:16:54 -07:00
Timothy J. Baek
ae3d8ce0c0 refac 2024-06-12 00:15:45 -07:00
Timothy J. Baek
a2e30ace02 refac: her 2024-06-12 00:13:35 -07:00
Timothy J. Baek
cde9672a09 enh: her 2024-06-12 00:05:20 -07:00
Timothy J. Baek
db6ca856de Update requirements-dev.lock 2024-06-11 19:16:06 -07:00
Timothy J. Baek
bb5e615841 chore: python requirements 2024-06-11 19:14:47 -07:00
Timothy J. Baek
48ac4d9f2a fix 2024-06-11 18:23:42 -07:00
Que Nguyen
eb7bba81fe Refactor URL validation function
- The check for private IP addresses often did not yield the expected results, especially with errors like: `[Errno -2] Name or service not known`.
- Removed the check for private IP addresses in the URL validation process.
- Simplified the `validate_url` function to focus on validating the URL format and checking the existence of the URL using a HEAD request.
2024-06-12 08:15:04 +07:00
Timothy J. Baek
a43cb1eb25 refac: styling 2024-06-11 17:09:10 -07:00
Timothy J. Baek
e2d8872060 fix: e2e 2024-06-11 17:03:49 -07:00
Timothy J. Baek
ede584320f refac: tools edit 2024-06-11 16:54:21 -07:00
Timothy J. Baek
bc458d3ee0 refac 2024-06-11 16:15:54 -07:00
Timothy J. Baek
0d1f7265c4 refac 2024-06-11 16:15:19 -07:00
Timothy J. Baek
539201a032 chore: format 2024-06-11 15:44:36 -07:00
Timothy J. Baek
50ad2aa215 chore: format 2024-06-11 15:44:27 -07:00
Timothy J. Baek
5cd28c04b8 feat: model tools assignment 2024-06-11 15:29:46 -07:00
Timothy J. Baek
c3ce4d7f6a refac 2024-06-11 14:36:46 -07:00
Timothy J. Baek
9bd054490f refac 2024-06-11 14:32:01 -07:00
Timothy J. Baek
2be14d57bf fix 2024-06-11 14:30:18 -07:00
Timothy J. Baek
61e0a85aed feat: export tool 2024-06-11 12:24:36 -07:00
Timothy J. Baek
e483c6b598 refac: styling 2024-06-11 12:18:30 -07:00
Timothy J. Baek
db3b075796 refac: input menu tools 2024-06-11 12:16:11 -07:00
Timothy J. Baek
0bb26ae504 refac: tools 2024-06-11 11:31:14 -07:00
Timothy J. Baek
9d16dd997a refac 2024-06-11 11:15:43 -07:00
Timothy J. Baek
e4fe1fff97 feat: tool __user__ email 2024-06-11 10:54:11 -07:00
Timothy J. Baek
37ed31b0e2 feat: tools warning 2024-06-11 10:47:46 -07:00
Timothy J. Baek
dc82e681dd refac 2024-06-11 10:43:36 -07:00
Timothy J. Baek
4944824544 enh: __user__ example 2024-06-11 10:36:35 -07:00
Timothy J. Baek
67cd9b76ae refac: __user__ param 2024-06-11 10:35:13 -07:00
Timothy J. Baek
8a86f32700 feat: user hook 2024-06-11 10:19:59 -07:00
Timothy Jaeryang Baek
f62b15d8da Merge pull request #3042 from sime2408/fit/translations-croatian
Updated Croatian translation for user interface text
2024-06-11 09:55:17 -07:00
Timothy Jaeryang Baek
d709038b5b Merge pull request #3029 from Yash-1511/main
feat: add DuckDuckGo search functionality using duckduckgo_search library
2024-06-11 09:53:26 -07:00
Timothy Jaeryang Baek
a2631ed38e Merge pull request #3041 from cocktailpeanut/main
feat: custom HOST env variable on windows
2024-06-11 09:52:45 -07:00
Timothy Jaeryang Baek
8ffd8c3505 Merge pull request #3035 from ClassicOldSong/main
Fix frontend stops responding when backend throws error
2024-06-11 09:52:26 -07:00
sime2408
f74df0433b Updated Croatian translation for user interface text
Refined Croatian locale translations to improve understanding and clarity for Croatian users. Adjustments made include improving command descriptions, correcting grammar, and adapting the terminology to better fit the software context.
2024-06-11 18:52:08 +02:00
Timothy Jaeryang Baek
93fa98245b Merge pull request #3033 from que-nguyen/dev
Fixed the issue where a single URL error disrupts the Web Search
2024-06-11 09:52:07 -07:00
Timothy Jaeryang Baek
e7727aea9e Merge pull request #3024 from KarlLee830/translate
Update Chinese Translation
2024-06-11 09:51:34 -07:00
cocktailpeanut
884eb9f7ee update 2024-06-11 12:32:29 -04:00
Yukino Song
912b75f8ac Fix frontend stops responding when backend throw error 2024-06-11 23:25:07 +08:00
Que Nguyen
3bec60b80c Fixed the issue where a single URL error disrupts the data loading process in Web Search mode
To address the unresolved issue in the LangChain library where a single URL error disrupts the data loading process, the lazy_load method in the WebBaseLoader class has been modified. The enhanced method now handles exceptions appropriately, logging errors and continuing with the remaining URLs.
2024-06-11 22:06:14 +07:00
Yash-1511
07b08ef67e add: duckduckgo-search dependencies added in requirements.txt 2024-06-11 19:53:57 +05:30
Yash-1511
83f9475584 feat: add DuckDuckGo search functionality using duckduckgo_search library 2024-06-11 19:49:08 +05:30
Karl Lee
1ad5456fe0 Update Chinese Translation 2024-06-11 20:46:15 +08:00
Timothy J. Baek
bf1936de34 refac 2024-06-11 02:12:24 -07:00
Timothy J. Baek
827dd1b044 refac 2024-06-11 01:58:26 -07:00
Timothy J. Baek
b2028d187d chore: format 2024-06-11 01:34:29 -07:00
Timothy J. Baek
1e1b372d07 fix 2024-06-11 01:18:05 -07:00
Timothy J. Baek
bd5a8567ef refac: tools & rag 2024-06-11 01:10:24 -07:00
Timothy J. Baek
fc46532955 fix 2024-06-11 00:58:52 -07:00
Timothy Jaeryang Baek
5f29a9edf4 Merge pull request #2998 from que-nguyen/dev
Completed Vietnamese translation
2024-06-11 00:52:15 -07:00
Timothy J. Baek
91bc65c7da fix 2024-06-11 00:50:30 -07:00
Timothy J. Baek
a91db7f7a6 refac 2024-06-11 00:43:42 -07:00
Timothy Jaeryang Baek
e6fe3ada57 Merge pull request #3006 from open-webui/tools
feat: tools
2024-06-11 00:37:53 -07:00
Timothy J. Baek
354683296b refac 2024-06-11 00:37:31 -07:00
Timothy J. Baek
5237439e29 feat: tool desc 2024-06-11 00:32:16 -07:00
Timothy J. Baek
049b3136e8 refac 2024-06-11 00:24:25 -07:00
Timothy J. Baek
3d6f5f418d feat: tools full integration 2024-06-11 00:18:45 -07:00
Timothy J. Baek
a27175d672 feat: fc integration 2024-06-10 23:40:27 -07:00
Timothy J. Baek
ff1cd306d8 refac 2024-06-10 22:38:48 -07:00
Timothy J. Baek
aa7d25600f refac 2024-06-10 22:33:25 -07:00
Timothy J. Baek
0bae7ca615 feat: tool selector input menu 2024-06-10 22:29:24 -07:00
Timothy J. Baek
3578b5e337 refac: clone 2024-06-10 22:16:49 -07:00
Timothy J. Baek
8464b30485 refac 2024-06-10 22:10:53 -07:00
Timothy J. Baek
c961964647 enh: tool id validation 2024-06-10 21:59:06 -07:00
Timothy J. Baek
dd7ac4c53a refac: search 2024-06-10 21:55:34 -07:00
Timothy J. Baek
4601a0246f fix: tool edit 2024-06-10 21:53:51 -07:00
Timothy J. Baek
25c0eca414 feat: edit tool spinner 2024-06-10 21:47:25 -07:00
Timothy J. Baek
ca8be1ee4a feat: import tools 2024-06-10 21:38:29 -07:00
Timothy J. Baek
1611a3aa70 feat: export tools 2024-06-10 21:36:13 -07:00
Timothy J. Baek
b434ebf3ad feat: tools integration 2024-06-10 21:33:46 -07:00
Timothy Jaeryang Baek
d2ed99e201 Merge pull request #3008 from open-webui/main
dev
2024-06-10 21:07:35 -07:00
Timothy Jaeryang Baek
06976c4551 Merge pull request #3007 from open-webui/fix
fix
2024-06-10 21:07:13 -07:00
Timothy J. Baek
626643104e fix 2024-06-10 21:06:46 -07:00
Timothy J. Baek
c5683dd24c refac 2024-06-10 21:05:06 -07:00
Timothy J. Baek
6589464ddf refac 2024-06-10 20:58:47 -07:00
Timothy J. Baek
e27c264081 feat: tools apis 2024-06-10 20:43:11 -07:00
Timothy J. Baek
3a96e1f109 feat: tools backend 2024-06-10 20:39:55 -07:00
Timothy J. Baek
c4629c8c2d refac: tools 2024-06-10 19:30:48 -07:00
Timothy J. Baek
adea3af8d7 refac 2024-06-10 19:19:53 -07:00
Timothy Jaeryang Baek
ec516e95fa Merge pull request #3001 from open-webui/main
dev
2024-06-10 18:56:21 -07:00
Timothy Jaeryang Baek
481fd9f88a Merge pull request #3000 from open-webui/fix
fix
2024-06-10 18:56:01 -07:00
Timothy J. Baek
2bbaf1234c fix 2024-06-10 18:55:45 -07:00
Timothy Jaeryang Baek
690c25215d Merge pull request #2999 from open-webui/fix
fix
2024-06-10 18:55:18 -07:00
Timothy J. Baek
b72243c166 fix 2024-06-10 18:54:52 -07:00
Timothy J. Baek
8f2e799615 fix 2024-06-10 18:53:33 -07:00
Que Nguyen
8ecf262457 Completed Vietnamese translation 2024-06-11 08:49:06 +07:00
Timothy Jaeryang Baek
7fb785901e Merge pull request #2997 from open-webui/main
dev
2024-06-10 18:02:06 -07:00
Timothy Jaeryang Baek
5dc9b93916 Merge pull request #2996 from open-webui/fix
fix: regeneration
2024-06-10 17:58:42 -07:00
Timothy J. Baek
1241bc3e89 fix: regeneration 2024-06-10 17:58:07 -07:00
Timothy J. Baek
c5ed3452d2 refac 2024-06-10 17:52:12 -07:00
Timothy J. Baek
c2e6e44714 refac 2024-06-10 17:33:40 -07:00
Timothy J. Baek
6e7e575a18 refac 2024-06-10 17:30:07 -07:00
Timothy J. Baek
f43b545bdd refac 2024-06-10 17:24:42 -07:00
Timothy J. Baek
12a04b118f refac: styling 2024-06-10 17:22:44 -07:00
Timothy J. Baek
8b1e2ce279 feat: code format 2024-06-10 17:12:48 -07:00
Timothy J. Baek
fb0f106afe refac: styling 2024-06-10 16:37:33 -07:00
Timothy J. Baek
dd423f43de feat: default tools template 2024-06-10 16:35:42 -07:00
Timothy J. Baek
f2bd3fdf19 chore: rm console log 2024-06-10 16:16:08 -07:00
Timothy J. Baek
8ad52f0fcc feat: code editor 2024-06-10 16:02:23 -07:00
Timothy J. Baek
5a3736f1ee feat: tools page 2024-06-10 15:24:26 -07:00
Timothy J. Baek
54bbfa5e89 chore: npm 2024-06-10 14:49:03 -07:00
Timothy Jaeryang Baek
3933db2c91 Merge pull request #2992 from open-webui/dev
0.3.2
2024-06-10 13:53:56 -07:00
Timothy J. Baek
dc8f0987e1 doc: changelog 2024-06-10 13:53:47 -07:00
Timothy J. Baek
644f0fe6c3 chore: version bump 2024-06-10 13:52:35 -07:00
Timothy J. Baek
87b04b0fc3 fix: params 2024-06-10 13:41:13 -07:00
Timothy J. Baek
455403a4a4 fix 2024-06-10 13:29:28 -07:00
Timothy J. Baek
547b990041 refac: params 2024-06-10 13:29:09 -07:00
Timothy Jaeryang Baek
fcf8f2a704 Merge pull request #2952 from arkohut/tolerant-readonly-filesystem
fix: tolerant readonly filesystem for copy favicon to static dir
2024-06-10 11:59:06 -07:00
Timothy Jaeryang Baek
496e798cc7 Merge pull request #2973 from KarlLee830/translate
i18n: Add some missing i18n keys and update Chinese translation
2024-06-10 11:58:47 -07:00
Timothy Jaeryang Baek
e01b3569d5 Merge pull request #2983 from pedrosilva/ptPTtranslation
pt-PT translation
2024-06-10 11:58:34 -07:00
Timothy J. Baek
a75b68c19c refac: message status history 2024-06-10 11:40:58 -07:00
Timothy J. Baek
34f04c53fc fix: audio 2024-06-10 11:04:00 -07:00
Pedro Silva
2b87ab32ff Updated pt-PT translation 2024-06-10 15:27:06 +01:00
arkohut
d20f6cb45b fix: use Exception to handle all errors 2024-06-10 21:27:35 +08:00
Karl Lee
48c9fb0ddf Update Chinese translation 2024-06-10 18:02:52 +08:00
Timothy Jaeryang Baek
c609cc23eb Merge pull request #2964 from googio/dev
feat: adding Serply as another web search provider
2024-06-10 01:50:04 -07:00
Timothy Jaeryang Baek
3566002b0b doc 2024-06-10 03:49:23 -05:00
Timothy J. Baek
140d7ff721 refac 2024-06-10 01:07:24 -07:00
Timothy J. Baek
7349e1d8c8 refac 2024-06-10 00:56:13 -07:00
Timothy J. Baek
bf5a62298c refac 2024-06-10 00:15:46 -07:00
Timothy J. Baek
2972774d87 refac 2024-06-10 00:07:47 -07:00
Timothy Jaeryang Baek
75d455ac8f Merge pull request #2967 from open-webui/dev
0.3.1
2024-06-09 19:09:28 -07:00
Timothy J. Baek
64b6a8c5db doc: changelog 2024-06-09 19:08:57 -07:00
Timothy Jaeryang Baek
8947da57af Merge pull request #2966 from open-webui/dev
fix
2024-06-09 19:00:08 -07:00
Timothy J. Baek
16f09fff63 fix 2024-06-09 18:59:45 -07:00
teampen
14eb667fc8 add changes to web search in admin settings 2024-06-09 21:52:08 -04:00
teampen
79bf8d6dd3 undo changes to deprecated web search params 2024-06-09 21:43:13 -04:00
teampen
14d33f0fcc Merge branch 'add-serply' into dev 2024-06-09 21:40:50 -04:00
teampen
4dcec4855e adding Serply as an alternative web search 2024-06-09 21:39:46 -04:00
Timothy Jaeryang Baek
96a004d4d8 Merge pull request #2921 from open-webui/dev
0.3.0
2024-06-09 18:11:53 -07:00
Timothy J. Baek
1fa16d73f0 doc: changelog 2024-06-09 18:02:42 -07:00
Timothy J. Baek
d2cd803a57 doc: changelog 2024-06-09 18:01:40 -07:00
Timothy J. Baek
e685953df7 doc: changelog 2024-06-09 17:57:16 -07:00
Timothy J. Baek
d6520e7fd8 fix: document selector 2024-06-09 17:55:23 -07:00
Timothy J. Baek
c54e3d5bf4 doc: changelog 2024-06-09 17:52:22 -07:00
teampen
efb4a710c8 adding Serply as an alternative web search 2024-06-09 20:44:34 -04:00
Timothy J. Baek
a71b824728 chore: format 2024-06-09 17:39:33 -07:00
Timothy J. Baek
bc1a7ab567 fix: playground 2024-06-09 17:38:08 -07:00
Timothy J. Baek
b565301a47 feat: knowledge integration 2024-06-09 17:34:42 -07:00
Timothy J. Baek
78272aed8d fix 2024-06-09 17:20:23 -07:00
Timothy J. Baek
3f7913b36f feat: knowledge selector 2024-06-09 17:17:35 -07:00
Timothy J. Baek
e1edf9227d feat: model knowledge ui 2024-06-09 16:34:34 -07:00
Timothy J. Baek
a8d709102f refac: styling 2024-06-09 16:17:10 -07:00
Timothy J. Baek
9032ab8857 refac 2024-06-09 16:05:29 -07:00
Timothy J. Baek
c9137edf5a refac 2024-06-09 15:59:33 -07:00
Timothy J. Baek
d11c373cf1 fix: styling 2024-06-09 15:36:25 -07:00
Timothy J. Baek
2755ef62d9 feat: threshold setting 2024-06-09 15:29:55 -07:00
Timothy J. Baek
8debb71197 feat: search query threshold 2024-06-09 15:19:36 -07:00
Timothy J. Baek
8b4867deb5 refac: styling 2024-06-09 15:12:38 -07:00
Timothy J. Baek
a93645aee8 refac 2024-06-09 15:08:23 -07:00
Timothy J. Baek
28ca6fb678 refac 2024-06-09 15:04:33 -07:00
Timothy J. Baek
da4586f27d refac: task models 2024-06-09 15:00:38 -07:00
Timothy J. Baek
591cd993c2 refac: search query task 2024-06-09 14:53:10 -07:00
Timothy J. Baek
aa1bb4fb6d refac 2024-06-09 14:26:49 -07:00
Timothy J. Baek
5e7237b9cb refac: title generation 2024-06-09 14:25:31 -07:00
Timothy J. Baek
84defafc14 feat: unified chat completions endpoint 2024-06-09 13:17:44 -07:00
Timothy Jaeryang Baek
7b1404f490 Merge pull request #2936 from SimonOriginal/dev
Ukrainian translation update
2024-06-09 13:01:33 -07:00
Timothy Jaeryang Baek
9b38788aa2 Merge pull request #2937 from mindspawn/cuda-fix
Enable case independent environment variable testing in start.sh
2024-06-09 13:01:17 -07:00
Timothy J. Baek
c44fc82ecd refac: openai 2024-06-09 12:43:54 -07:00
Timothy J. Baek
8b6f422d45 refac: styling 2024-06-09 12:15:48 -07:00
Timothy J. Baek
2eb8c3456f refac 2024-06-09 12:13:13 -07:00
Timothy J. Baek
fa9835a7ad refac: styling 2024-06-09 12:08:16 -07:00
Timothy J. Baek
3db9d19ae3 refac 2024-06-09 11:57:33 -07:00
Timothy Jaeryang Baek
762db11bf4 Merge pull request #2939 from KarlLee830/translate
Add some missing i18n keys
2024-06-09 11:14:50 -07:00
arkohut
8e9e429a91 fix: tolerant readonly filesystem for copy favicon to static dir 2024-06-09 23:23:28 +08:00
Timothy J. Baek
f2b9a5f5bf refac: rag 2024-06-09 03:01:25 -07:00
Timothy J. Baek
277fc3feac refac: styling 2024-06-09 02:44:50 -07:00
Timothy J. Baek
9053bfdadf refac: styling 2024-06-09 02:41:52 -07:00
Timothy J. Baek
414ab53144 refac: banners 2024-06-09 02:39:18 -07:00
Timothy J. Baek
4a7d3a076c fix: styling 2024-06-09 02:29:56 -07:00
Timothy J. Baek
8198807fc9 refac: document settings > admin settings 2024-06-09 02:28:52 -07:00
Timothy J. Baek
e556c02fe9 refac: styling 2024-06-09 02:14:05 -07:00
Timothy J. Baek
f7de01cbc2 refac: styling 2024-06-09 02:05:36 -07:00
Timothy J. Baek
1bc96de620 refac: styling 2024-06-09 02:01:03 -07:00
Timothy J. Baek
23489105fc refac: styling 2024-06-09 01:59:39 -07:00
Timothy Jaeryang Baek
8c95a8be3a Merge pull request #2943 from choltha/fix/temperature-params
fix: model settings temperature not passed correctly from model settings to ollama/openai api
2024-06-09 01:56:18 -07:00
Timothy J. Baek
d00c8b00df refac 2024-06-09 01:55:56 -07:00
Timothy J. Baek
4bae43096b refac: settings 2024-06-09 01:54:50 -07:00
Timothy J. Baek
d90e138773 refac: settings 2024-06-09 01:44:08 -07:00
Timothy J. Baek
bb15eb016e fix: message input delay 2024-06-09 01:22:21 -07:00
Christoph Holthaus
97b39115a1 fix: temperature not passed correctly 2024-06-09 09:49:24 +02:00
Timothy J. Baek
cc53fd8379 fix: params 2024-06-08 20:09:59 -07:00
Timothy J. Baek
5342063e24 fix: import model 2024-06-08 19:21:37 -07:00
Timothy J. Baek
e274a0dc3e fix: base model verification 2024-06-08 17:21:23 -07:00
Karl Lee
9c3920a489 Update Chinese translation 2024-06-09 04:54:58 +08:00
Timothy J. Baek
d1da3dde65 refac: migration 2024-06-08 13:45:33 -07:00
Karl Lee
0ccd96c950 Add some missing i18n keys 2024-06-09 04:42:22 +08:00
Timothy J. Baek
3499ec3f79 fix: tts 2024-06-08 13:18:42 -07:00
Timothy J. Baek
40ea18d54d refac: auth styling 2024-06-08 13:08:03 -07:00
Timothy Jaeryang Baek
ffbc480bb4 Merge pull request #2930 from KarlLee830/translate
Update Chinese translation
2024-06-08 13:03:22 -07:00
Timothy Jaeryang Baek
9b839e5c9f Merge pull request #2927 from arkohut/tolerant-no-write-permission-for-static-dir
fix: tolerant no write permission for copy favicon to static dir
2024-06-08 13:03:08 -07:00
mindspawn
575f23cde9 Update start.sh to be case-independent when testing env vars 2024-06-08 10:59:59 -07:00
SimonOriginal
711317555c Ukrainian translation update 2024-06-08 18:08:53 +02:00
Timothy J. Baek
063e1ee46c refac: call 2024-06-08 03:27:56 -07:00
Karl Lee
0d0f8aac15 Upload Chinese tranlsation 2024-06-08 18:13:19 +08:00
Timothy J. Baek
5cbb79fa6e fix: safari audio issue 2024-06-08 02:07:19 -07:00
Timothy J. Baek
277e7aead7 refac 2024-06-08 00:52:19 -07:00
arkohut
d3558fdb33 fix: tolerant no write permission for copy favicon to static dir 2024-06-08 13:56:08 +08:00
Timothy J. Baek
1271d1dccc feat: allow image from prompt {{CLIPBOARD}} 2024-06-07 22:44:10 -07:00
Timothy J. Baek
3be1cb98d3 fix: ollama embedding list 2024-06-07 22:32:45 -07:00
Timothy J. Baek
c2a7e7f510 doc: readme 2024-06-07 22:17:11 -07:00
Timothy Jaeryang Baek
dbde628141 Merge pull request #2923 from mindspawn/outlook-msg
Support Outlook Message File Format
2024-06-07 21:50:03 -07:00
mindspawn
2412f31ed9 Update Dockerfile to revert duplicated pip install 2024-06-07 21:47:50 -07:00
Timothy J. Baek
8df0c9e0dd doc: readme 2024-06-07 21:47:31 -07:00
mindspawn
6f9148ac4c Update main.py 2024-06-07 21:41:30 -07:00
Timothy J. Baek
d7d8896e43 fix: active users 2024-06-07 21:38:09 -07:00
mindspawn
d5a2a8a880 Update Dockerfile 2024-06-07 21:20:59 -07:00
mindspawn
cff8534f33 Update constants.ts 2024-06-07 21:19:46 -07:00
mindspawn
f69bc57fed Update requirements.txt 2024-06-07 21:18:35 -07:00
mindspawn
4ecc1c06d3 Update main.py 2024-06-07 21:18:04 -07:00
Timothy J. Baek
f78e6a5fba fix: auto playback 2024-06-07 21:10:00 -07:00
Timothy J. Baek
411e6c2fe4 fix: styling 2024-06-07 21:06:26 -07:00
Timothy J. Baek
369816ace0 chore: format 2024-06-07 21:03:45 -07:00
Timothy J. Baek
06e4b87c9e refac: audio 2024-06-07 20:57:15 -07:00
Timothy J. Baek
c6b74a3cf9 refac: audio 2024-06-07 20:55:50 -07:00
Timothy J. Baek
27417dd771 refac: audio 2024-06-07 20:51:53 -07:00
Timothy J. Baek
16615010f7 refac 2024-06-07 20:35:50 -07:00
Timothy J. Baek
e516374d54 feat: external stt 2024-06-07 20:31:52 -07:00
Timothy J. Baek
55dc6c1b3b refac: audio 2024-06-07 20:18:48 -07:00
Timothy J. Baek
da47c2dfa3 refac: active users 2024-06-07 17:35:01 -07:00
Timothy J. Baek
694d1708a3 fix: styling 2024-06-07 16:58:33 -07:00
Timothy J. Baek
f9aa03bc6d feat: dark splash screen 2024-06-07 16:57:02 -07:00
Timothy J. Baek
bd3e5d7bfa refac 2024-06-07 16:20:50 -07:00
Timothy J. Baek
5743df2280 refac: styling 2024-06-07 15:55:02 -07:00
Timothy J. Baek
e3efe2c565 fix: safari video 2024-06-07 15:31:58 -07:00
Timothy J. Baek
32c4748749 refac 2024-06-07 15:18:45 -07:00
Timothy J. Baek
f4a15f9590 chore: version bump 2024-06-07 15:16:24 -07:00
Timothy J. Baek
f92ef3211b refac: permission 2024-06-07 15:12:34 -07:00
Timothy J. Baek
508e8eb4b1 refac 2024-06-07 15:08:21 -07:00
Timothy J. Baek
dfc2fb65af refac: styling 2024-06-07 15:01:55 -07:00
Timothy J. Baek
d7ce408d25 refac 2024-06-07 15:00:42 -07:00
Timothy J. Baek
374f647048 fix 2024-06-07 14:56:10 -07:00
Timothy J. Baek
00018085ed refac 2024-06-07 14:55:32 -07:00
Timothy J. Baek
d17cdc8068 feat: video devices support 2024-06-07 14:49:36 -07:00
Timothy J. Baek
e5ad76615c feat: video call 2024-06-07 14:08:04 -07:00
Jun Siang Cheah
bba4c4242f feat: add WEBUI_SESSION_COOKIE_SECURE 2024-06-07 09:13:42 +01:00
Timothy J. Baek
54420f5300 refac: styling 2024-06-07 01:04:51 -07:00
Timothy J. Baek
f076f81e0c fix: safari compatibility issue 2024-06-07 00:57:53 -07:00
Timothy J. Baek
ea80285e9c fix: disable empty string 2024-06-07 00:37:54 -07:00
Timothy J. Baek
f43d255e8f refac: wip tooltip 2024-06-07 00:28:34 -07:00
Timothy J. Baek
fc3a31e3d5 refac: optimisation 2024-06-07 00:27:05 -07:00
Timothy J. Baek
4e640daf83 feat: submit prompt integration 2024-06-07 00:04:47 -07:00
Timothy J. Baek
404bb3fd67 refac: call overlay 2024-06-06 23:42:31 -07:00
Timothy J. Baek
eacce66a82 refac 2024-06-06 23:36:47 -07:00
Timothy J. Baek
4f19a92dc5 refac 2024-06-06 23:29:08 -07:00
Timothy J. Baek
5196b65c2e feat: call ui scaffold 2024-06-06 22:30:19 -07:00
Timothy J. Baek
340b716a90 refac: voice recording web api 2024-06-06 21:56:09 -07:00
Timothy Jaeryang Baek
ff4cf16742 Merge pull request #2900 from open-webui/voice
feat: voice note
2024-06-06 21:01:41 -07:00
Timothy J. Baek
7eaf83cb7d fix: silence monitoring 2024-06-06 21:00:41 -07:00
Timothy J. Baek
4a73a01c24 refac 2024-06-06 20:44:42 -07:00
Timothy J. Baek
1a9dee50ce refac: styling 2024-06-06 20:35:18 -07:00
Timothy J. Baek
f97f6601f7 feat: voice input refac 2024-06-06 20:33:23 -07:00
Timothy Jaeryang Baek
5ab5231c73 Merge pull request #2886 from KarlLee830/translate
i18n: Added missing i18n key
2024-06-06 11:18:48 -07:00
Karl Lee
046dd02a6d Added missing i18n key 2024-06-07 01:52:03 +08:00
Timothy Jaeryang Baek
72e1615fe1 Merge pull request #2873 from KarlLee830/translate
i18n: Added missing i18n keys for the pending overlay and other pages.
2024-06-06 04:10:47 -07:00
Timothy Jaeryang Baek
b512501d9d Merge pull request #2870 from mindspawn/proxy-fix2
Fix proxy not being used for /api/version/updates
2024-06-06 04:06:36 -07:00
Karl Lee
36cc3c24e6 Added some missing i18n keys 2024-06-06 15:07:09 +08:00
Karl Lee
62bfcf35e5 Update translation.json 2024-06-06 14:19:36 +08:00
Karl Lee
9bec2cea12 Update translation 2024-06-06 13:58:25 +08:00
Karl Lee
f4a122a4bb Added missing i18n
Added missing i18n keys and update Chinese translation.
2024-06-06 13:47:40 +08:00
Karl Lee
34aea03c02 Update Chinese translation 2024-06-06 13:29:45 +08:00
Karl Lee
e7fb86190a added missing i18n keys 2024-06-06 13:12:45 +08:00
Karl Lee
194564e253 Update Chinese translation 2024-06-06 13:03:11 +08:00
Timothy J. Baek
d4b10097d2 fix: sidebar 2024-06-05 18:31:15 -07:00
mindspawn
3069fa8074 Update main.py to enable http_proxy use for checking for new versions 2024-06-05 17:24:59 -07:00
Timothy J. Baek
c2bbb37fc1 fix: sidebar 2024-06-05 14:20:26 -07:00
Timothy J. Baek
e52568e7cb feat: upload pipeline 2024-06-05 13:57:48 -07:00
Timothy Jaeryang Baek
bc4aeeeec8 Merge pull request #2866 from bartowski1182/patch-1
Update CONTRIBUTING.md to include proper first step
2024-06-05 13:26:19 -07:00
Bartowski
25ee5f6cb0 Update CONTRIBUTING.md
Update step one to be opening a discussion rather than an issue
2024-06-05 16:24:24 -04:00
Timothy Jaeryang Baek
882ad219ea Merge pull request #2862 from jannikstdl/locale-update
i18n: added missing i18n keys + german locales
2024-06-05 12:43:34 -07:00
Jannik Streidl
c108d863f6 added missing i18n keys + german locales 2024-06-05 20:42:41 +02:00
Jun Siang Cheah
ae376ec8fe Merge remote-tracking branch 'upstream/dev' into feat/oauth 2024-06-05 19:23:52 +01:00
Jun Siang Cheah
af4f8aa589 feat: add WEBUI_SESSION_COOKIE_SAME_SITE for when open webui is embedded 2024-06-05 19:21:42 +01:00
Timothy Jaeryang Baek
a8d80f936a Merge pull request #2857 from open-webui/dev
refac: styling
2024-06-05 10:12:23 -07:00
Timothy J. Baek
1bb7fc7c92 refac: styling 2024-06-05 10:12:01 -07:00
Timothy Jaeryang Baek
dbb83f9824 Merge pull request #2856 from open-webui/dev
0.2.5
2024-06-05 10:06:11 -07:00
Timothy J. Baek
e1889b0c5e doc: changelog 2024-06-05 10:05:35 -07:00
Timothy J. Baek
88758af621 chore: format 2024-06-05 09:59:40 -07:00
Timothy Jaeryang Baek
43f83673e2 Merge pull request #2855 from mikal-k/norwegian-translation
Added Norwegian Bokmål translation
2024-06-05 09:57:14 -07:00
Timothy J. Baek
950073c1de feat: create modelfile from models 2024-06-05 09:52:58 -07:00
Mikal Krogstad
651c85fcc4 Added Norwegian Bokmål translation 2024-06-05 18:21:35 +02:00
Timothy J. Baek
ec7c5a3f07 refac: set default model from interface settings 2024-06-05 09:14:15 -07:00
Timothy J. Baek
d077b3dcdb fix: ollama version request when ollama api is disabled 2024-06-05 09:08:52 -07:00
Timothy Jaeryang Baek
1d6bbdf917 Merge pull request #2836 from Jasurbek99/main
added turkmen language
2024-06-05 08:55:32 -07:00
Timothy J. Baek
f248425931 refac: styling 2024-06-05 08:48:02 -07:00
Timothy J. Baek
bdf7ed7f02 fix: mobile screen 2024-06-05 08:43:23 -07:00
Timothy Jaeryang Baek
66200cfb7d Merge pull request #2828 from KarlLee830/dev
i18n: Update Chinese translation
2024-06-04 18:09:55 -07:00
Karl Lee
9174e01d6a Update Chinese translation
Update Chinese translation to match the latest version.
2024-06-05 08:03:04 +08:00
Timothy J. Baek
f17df4f114 doc: readme 2024-06-04 12:32:20 -07:00
Timothy J. Baek
1948e78351 refac: rename fullscreen -> widescreen 2024-06-04 11:48:46 -07:00
Timothy J. Baek
73a98ff938 chore: format 2024-06-04 11:45:23 -07:00
Timothy J. Baek
92d056c982 fix: running models 2024-06-04 11:38:31 -07:00
Timothy J. Baek
2be9c25ba7 feat: show current running models 2024-06-04 11:13:43 -07:00
Timothy Jaeryang Baek
56fda46215 Merge pull request #2802 from KarlLee830/dev
Updated Chinese translation
2024-06-04 10:07:57 -07:00
Timothy J. Baek
1d1b945483 fix 2024-06-04 09:56:51 -07:00
Karl Lee
9aac6e05a9 Update Chinese translation
Fix a typo.
2024-06-05 00:56:14 +08:00
Timothy J. Baek
73e2912849 fix: incognito mode 2024-06-04 09:55:31 -07:00
Timothy J. Baek
7cf80289c3 fix: active users 2024-06-04 09:52:27 -07:00
Karl Lee
2fa594d5bd Update Chinese translation
Optimize more translations.
2024-06-05 00:41:56 +08:00
Karl Lee
45f1a4157b Update Chinese translation
Modify the time format to a format that native Chinese speakers are more comfortable with.
2024-06-05 00:19:21 +08:00
Karl Lee
b3e4ecd2f4 Update Chinese translation
Change the date format so that the dates are aligned. Fix some incorrect translations.
2024-06-05 00:14:11 +08:00
Timothy Jaeryang Baek
ab04e0dfd4 Merge pull request #2818 from Lanhild/patch-4 2024-06-04 08:25:43 -07:00
Loan J
ee625a0519 Update pull_request_template.md 2024-06-04 11:02:51 -04:00
Loan J
fea563cf89 Update pull_request_template.md 2024-06-04 11:01:15 -04:00
Jasurbek99
fcfdfa3dea added turkmen language 2024-06-04 15:54:47 +05:00
Timothy J. Baek
844923d0a4 refac: edit admin 2024-06-04 01:14:37 -07:00
Timothy J. Baek
bbfa54a6b9 feat: active user count 2024-06-04 01:10:31 -07:00
Karl Lee
e2429314f3 Update translation.json 2024-06-04 16:00:50 +08:00
Timothy J. Baek
4925a6530b fix: banner z index 2024-06-04 00:45:56 -07:00
Timothy J. Baek
85484392b2 feat: websocket 2024-06-03 23:39:52 -07:00
Karl Lee
8d9712d66e Updated Chinese translation
Updated Chinese translation for version 0.2.4.
2024-06-04 13:46:24 +08:00
Timothy J. Baek
0495f01acb feat: reset upload dir 2024-06-03 21:45:36 -07:00
Timothy Jaeryang Baek
f28877f4db Merge pull request #2801 from open-webui/dev
0.2.4
2024-06-03 21:29:13 -07:00
Timothy J. Baek
b673ae7ab2 chore: version bump 2024-06-03 21:28:12 -07:00
Timothy Jaeryang Baek
8c3a6eb262 Merge pull request #2799 from mindspawn/proxy-fix
Enable use of http_proxy environment variable in openai and ollama calls
2024-06-03 21:19:53 -07:00
Timothy J. Baek
25336f85f3 feat: admin details in account pending overlay 2024-06-03 21:17:43 -07:00
mindspawn
74ed74f1a4 Enable http_proxy use for ollama calls. 2024-06-03 20:43:25 -07:00
mindspawn
94ebf02719 Enable http_proxy use for openai calls. 2024-06-03 20:41:59 -07:00
Timothy Jaeryang Baek
11bb1a6f7d Merge pull request #2795 from open-webui/dev
fix
2024-06-03 17:07:25 -07:00
Timothy J. Baek
61867c1545 Update searxng.py 2024-06-03 17:02:50 -07:00
Timothy J. Baek
4068a421bf fix 2024-06-03 17:00:35 -07:00
Timothy Jaeryang Baek
8a94d8a226 Merge pull request #2794 from open-webui/dev
fix
2024-06-03 15:52:04 -07:00
Timothy Jaeryang Baek
5a24ce06f0 Merge pull request #2792 from aguvener/dev
i18n: incorrect translations fixed
2024-06-03 15:51:47 -07:00
Timothy J. Baek
e72170bac7 doc: readme 2024-06-03 15:51:10 -07:00
Timothy J. Baek
dd93b0dd24 feat: documentation button 2024-06-03 15:45:33 -07:00
Timothy Jaeryang Baek
0ac04c9518 Merge pull request #2791 from open-webui/dev
doc: demo.gif
2024-06-03 15:17:22 -07:00
Timothy J. Baek
d28925de05 doc: demo.gif 2024-06-03 15:15:59 -07:00
aguvener
2c4c62fb75 Update translation.json 2024-06-04 01:15:42 +03:00
Timothy Jaeryang Baek
de162a1f32 Merge pull request #2789 from open-webui/dev
fix
2024-06-03 13:52:07 -07:00
Timothy J. Baek
aa7e5dbfce chore: format 2024-06-03 13:51:56 -07:00
Timothy J. Baek
8d99bffbdc chore: format 2024-06-03 13:51:32 -07:00
Timothy Jaeryang Baek
768941bded Merge pull request #2785 from cheahjs/feat/openai-embeddings-batch
feat: add RAG_EMBEDDING_OPENAI_BATCH_SIZE to batch multiple embeddings
2024-06-03 13:50:14 -07:00
Timothy Jaeryang Baek
e0772c6807 Merge pull request #2788 from open-webui/dev
fix: import model
2024-06-03 13:41:59 -07:00
Timothy J. Baek
cbd60168ea fix: import model 2024-06-03 13:41:33 -07:00
Timothy Jaeryang Baek
c6c5c0ddb2 Merge pull request #2787 from open-webui/dev
fix
2024-06-03 13:37:13 -07:00
Timothy Jaeryang Baek
7c35447666 Merge pull request #2786 from justinh-rahb/contributing-update
docs: add line re: keeping PRs open
2024-06-03 13:36:42 -07:00
Timothy J. Baek
58b5e0bf3e fix: modelfile edit 2024-06-03 13:35:03 -07:00
Justin Hayes
0586d76b5d Add line re: keeping PRs open 2024-06-03 16:26:30 -04:00
Timothy Jaeryang Baek
eb3fcef6d7 Merge pull request #2784 from open-webui/dev
fix
2024-06-03 13:16:29 -07:00
Timothy Jaeryang Baek
59a730cb8b Merge pull request #2783 from cheahjs/fix/searxng-backwards-compat
fix: add backwards compat with older searxng urls
2024-06-03 13:16:10 -07:00
Jun Siang Cheah
7fefbb316d fix: add backwards compat with older searxng urls 2024-06-03 21:13:10 +01:00
Timothy Jaeryang Baek
14646e84ea Merge pull request #2782 from open-webui/dev
0.2.3
2024-06-03 13:06:46 -07:00
Timothy Jaeryang Baek
ba56edab19 Merge pull request #2781 from Lhemamou/dev
fix: lithuanian language doesn't appear in the languages list (#2780)
2024-06-03 13:05:20 -07:00
Timothy J. Baek
a233a8855b chore: version bump 2024-06-03 13:04:18 -07:00
Lhemamou
c11fdad82d add lithuanian 2024-06-03 21:56:07 +02:00
Timothy J. Baek
82f7b6c315 refac: modelfiles migration 2024-06-03 12:48:17 -07:00
Timothy Jaeryang Baek
92d9b38110 Merge branch 'dev' into feat/openai-embeddings-batch 2024-06-03 12:39:09 -07:00
Timothy Jaeryang Baek
36a66fcfc4 Merge pull request #2774 from cheahjs/feat/title-edit-improvements
feat: title edit improvements
2024-06-03 12:34:02 -07:00
Timothy Jaeryang Baek
ea27bc50f5 Merge pull request #2750 from KarlLee830/dev
Improve Chinese translation
2024-06-03 12:33:13 -07:00
Timothy Jaeryang Baek
8a9c18ddd5 Merge branch 'dev' into dev 2024-06-03 12:33:03 -07:00
Timothy Jaeryang Baek
3729771f33 Merge pull request #2760 from nirabo/fix/searxng
Fix/searxng
2024-06-03 12:32:03 -07:00
Timothy Jaeryang Baek
ab58947a19 Merge pull request #2775 from cheahjs/feat/allow-nonlocal-tts
feat: allow nonlocal tts voices
2024-06-03 12:31:20 -07:00
Timothy J. Baek
f9351af6a3 refac: message input 2024-06-03 12:30:37 -07:00
Jun Siang Cheah
8f03daeaf6 fix: selected local tts voice not shown in settings 2024-06-03 19:11:37 +01:00
Jun Siang Cheah
de03bf834e feat: add option to allow nonlocal TTS voices 2024-06-03 19:11:14 +01:00
Jun Siang Cheah
db3d48ee15 feat: automatically focus title input when editing 2024-06-03 18:49:34 +01:00
Jun Siang Cheah
6022f6f5df feat: double click title to edit 2024-06-03 18:48:27 +01:00
Karl Lee
2d1a890ed9 Improve more Chinese translation content
Improve more Chinese translation content
2024-06-03 22:08:58 +08:00
Lyuboslav Petrov
08443b3c55 Revert log level to debug 2024-06-03 12:48:40 +03:00
Lyuboslav Petrov
7e761a69a7 FIX searxng URL construction using params for arg passing
Accept additional parameters such as language, time_range, and categories to tailor the search results.
Raise an exception if a request error occurs during the search process.
Use params argument to construct the query string
Sort by relevance
Expand docstring
2024-06-03 12:44:46 +03:00
Timothy Jaeryang Baek
be08dbf00d Merge pull request #2753 from open-webui/dev
fix
2024-06-02 22:36:43 -07:00
Timothy J. Baek
b4fca046a8 fix 2024-06-02 22:36:23 -07:00
Timothy Jaeryang Baek
61287a16d5 Merge pull request #2752 from open-webui/dev
fix: #2751
2024-06-02 22:29:16 -07:00
Timothy J. Baek
16e72b2f67 fix: #2751 2024-06-02 22:28:56 -07:00
Karl Lee
80d173c1c3 Improve Chinese translation
Corrected the terrible errors in the Chinese translation and improved the overall translation quality.
2024-06-03 12:50:31 +08:00
Timothy Jaeryang Baek
4b459e372f Merge pull request #2749 from open-webui/dev
feat: single chat export
2024-06-02 21:39:36 -07:00
Timothy J. Baek
fddf693dad feat: single chat export 2024-06-02 21:39:09 -07:00
Timothy Jaeryang Baek
a39ab4dd24 Merge pull request #2747 from open-webui/dev
fix: render mermaid after edit
2024-06-02 19:09:19 -07:00
Timothy Jaeryang Baek
13db33e947 Merge pull request #2746 from Yanyutin753/translate
📌fix format #2744
2024-06-02 19:08:47 -07:00
Timothy J. Baek
727a837e53 fix: render mermaid after edit 2024-06-02 19:08:18 -07:00
Yanyutin753
22d7ce2b7f 📌fix format 2024-06-03 10:04:17 +08:00
Timothy Jaeryang Baek
1d4c358624 Merge pull request #2745 from open-webui/dev
fix
2024-06-02 19:03:16 -07:00
Timothy J. Baek
a3ecf62564 fix: chat completed 2024-06-02 19:02:46 -07:00
Timothy Jaeryang Baek
6199296205 Merge pull request #2744 from Yanyutin753/translate
 use Azure API 3.0 to translate almost all languages and complete those without translations in v0.2.0
2024-06-02 18:55:08 -07:00
Yanyutin753
5ea88f2e4f use Azure API 3.0 to translate almost all languages and complete those without translations in v0.2.0 2024-06-03 09:48:49 +08:00
Timothy Jaeryang Baek
5be97b81d6 Merge pull request #2743 from open-webui/dev
fix: changelog
2024-06-02 18:27:36 -07:00
Timothy J. Baek
42f335851f fix: changelog 2024-06-02 18:27:18 -07:00
Timothy Jaeryang Baek
0744806523 Merge pull request #2742 from open-webui/dev
0.2.2
2024-06-02 18:26:09 -07:00
Timothy J. Baek
a36555a25a chore: version bump 2024-06-02 18:26:00 -07:00
Timothy J. Baek
814fbb73f3 feat: RESET_CONFIG_ON_START 2024-06-02 18:14:36 -07:00
Timothy J. Baek
3d74c04f50 feat: mermaid rendering support 2024-06-02 18:03:30 -07:00
Timothy J. Baek
a9e5003c4f fix: filter outlet 2024-06-02 17:49:44 -07:00
Yanyutin753
8114fa93cf translation zh-CN 2024-06-03 08:32:39 +08:00
Timothy J. Baek
32a61ea15d fix: pipelines 2024-06-02 16:46:33 -07:00
Timothy Jaeryang Baek
d9a3e4db39 Merge pull request #2731 from cheahjs/fix/ollama-cancellation
fix: ollama and openai stream cancellation
2024-06-02 16:27:11 -07:00
Timothy Jaeryang Baek
c5ff4c24e1 Merge branch 'dev' into fix/ollama-cancellation 2024-06-02 16:27:01 -07:00
Timothy Jaeryang Baek
9b3fdb1838 Merge pull request #2741 from open-webui/dev
fix: validate lengths for openai urls and keys
2024-06-02 16:21:28 -07:00
Timothy J. Baek
27ff3ab112 fix: validate lengths for openai urls and keys 2024-06-02 16:20:22 -07:00
Timothy Jaeryang Baek
d09164b729 Merge pull request #2739 from open-webui/dev
fix
2024-06-02 15:19:59 -07:00
Timothy Jaeryang Baek
44597314c6 Merge pull request #2737 from cheahjs/fix/docker-tag-again
fix: another attempt at fixing tag docker builds
2024-06-02 14:49:18 -07:00
Jun Siang Cheah
3176fe0c2b fix: another attempt at fixing tag docker builds 2024-06-02 22:35:59 +01:00
Timothy Jaeryang Baek
cfc78dedf0 Merge pull request #2736 from open-webui/dev
fix
2024-06-02 14:08:43 -07:00
Timothy J. Baek
642079ca29 Update CHANGELOG.md 2024-06-02 14:08:23 -07:00
Timothy J. Baek
523455a46e refac: styling 2024-06-02 14:04:37 -07:00
Timothy Jaeryang Baek
8eb81c9127 Merge pull request #2735 from open-webui/dev
0.2.1
2024-06-02 14:01:54 -07:00
Timothy J. Baek
6e818e3694 doc: changelog 2024-06-02 14:01:05 -07:00
Timothy J. Baek
6de8327665 chore: version bump 2024-06-02 13:54:11 -07:00
Timothy J. Baek
0a9f41a044 feat: single model export 2024-06-02 13:53:02 -07:00
Timothy J. Baek
2e24c2d4d3 refac 2024-06-02 13:48:14 -07:00
Timothy J. Baek
41442897e3 fix: connections 2024-06-02 13:45:04 -07:00
Timothy J. Baek
e0ba585204 feat: include num_thread in advanced params 2024-06-02 13:20:38 -07:00
Timothy Jaeryang Baek
0bae97218d Merge pull request #2722 from SimonOriginal/main
Updated Ukrainian translation to v0.2.0  🇺🇦
2024-06-02 11:45:55 -07:00
Timothy Jaeryang Baek
2ad286698b Merge pull request #2718 from cheahjs/fix/tag-docker-build
fix: docker build on tag broke due to cache
2024-06-02 11:45:21 -07:00
Timothy Jaeryang Baek
5015486fc2 Merge pull request #2715 from que-nguyen/dev
Update vi-VN/translation.json
2024-06-02 11:45:04 -07:00
Jun Siang Cheah
b5b2b70f4a fix: bad payload refactor 2024-06-02 19:40:18 +01:00
Jun Siang Cheah
7f74426a22 fix: openai streaming cancellation using aiohttp 2024-06-02 18:48:45 +01:00
Jun Siang Cheah
4dd51badfe fix: ollama streaming cancellation using aiohttp 2024-06-02 18:06:12 +01:00
Jun Siang Cheah
24c35c308d fix: stream defaults to true, return request ID 2024-06-02 17:09:15 +01:00
Jun Siang Cheah
0cb8163321 feat: add RAG_EMBEDDING_OPENAI_BATCH_SIZE to batch multiple embeddings 2024-06-02 15:34:31 +01:00
SimonOriginal
80c6a39fab Updated Ukrainian translation to v0.2.0 2024-06-02 14:42:15 +02:00
Jun Siang Cheah
45bff743a2 fix: docker build on tag broke due to cache 2024-06-02 10:01:01 +01:00
Que Nguyen
afd62c3b22 Update translation.json
These changes serve to complete the Vietnamese translation.
2024-06-02 13:53:38 +07:00
Timothy Jaeryang Baek
72354e06a7 Merge pull request #2476 from open-webui/dev
0.2.0
2024-06-01 22:03:02 -07:00
Timothy J. Baek
207e25035a chore: format 2024-06-01 22:01:53 -07:00
Timothy J. Baek
e99b2ecdd6 chore: version bump
Co-Authored-By: nullptr <62841684+not-nullptr@users.noreply.github.com>
2024-06-01 21:55:44 -07:00
Timothy J. Baek
1c7dd7b859 refac: openwebui.com integration 2024-06-01 21:16:32 -07:00
Timothy J. Baek
32a8884c0c fix: model create 2024-06-01 21:12:51 -07:00
Timothy J. Baek
f1855f5208 doc: typo 2024-06-01 20:50:01 -07:00
Timothy J. Baek
6895e2439c doc: code-of-conduct 2024-06-01 20:49:45 -07:00
Timothy J. Baek
1285ec1828 doc: readme 2024-06-01 20:43:10 -07:00
Timothy J. Baek
bdcfbfeb11 refac 2024-06-01 20:26:19 -07:00
Timothy J. Baek
aa7fcca4ee fix: banner styling 2024-06-01 20:21:26 -07:00
Timothy J. Baek
fc5f74cc64 chore: version bump 2024-06-01 20:16:00 -07:00
Timothy J. Baek
86510de343 chore: format 2024-06-01 20:12:11 -07:00
Timothy J. Baek
a53796270f refac: web search config 2024-06-01 20:08:08 -07:00
Timothy J. Baek
fbdfb7e4fa refac: web search 2024-06-01 19:57:00 -07:00
Timothy J. Baek
999d2bc21b refac: web search 2024-06-01 19:52:12 -07:00
Timothy J. Baek
912a704fdc refac: web search settings 2024-06-01 19:40:48 -07:00
Timothy J. Baek
ea6b8984ab refac: web search 2024-06-01 19:03:56 -07:00
Timothy J. Baek
0b8b039aae doc: changelog 2024-06-01 18:54:05 -07:00
Timothy J. Baek
dd799ebea8 refac: styling 2024-06-01 18:44:47 -07:00
Timothy Jaeryang Baek
f2e5bebdbc Merge pull request #2709 from open-webui/main
dev
2024-06-01 18:44:36 -07:00
Timothy J. Baek
abbf622fba doc: readme 2024-06-01 18:26:30 -07:00
Timothy J. Baek
e4808db281 doc: changelog 2024-06-01 18:18:51 -07:00
Timothy J. Baek
500765a778 doc: changelog 2024-06-01 18:16:17 -07:00
Timothy J. Baek
c5c33aabf9 refac: styling 2024-06-01 18:11:54 -07:00
Timothy J. Baek
ef4af86883 refac: message new line issue 2024-06-01 18:00:01 -07:00
Timothy J. Baek
d993beb18f fix: typo 2024-06-01 17:42:55 -07:00
Timothy J. Baek
53b44b75dc doc: changelog 2024-06-01 17:41:07 -07:00
Timothy J. Baek
7f2e89a8aa enh: allow label search 2024-06-01 16:34:36 -07:00
Timothy J. Baek
c6a57b1f6f feat: export litellm config.yaml 2024-06-01 16:31:39 -07:00
Timothy J. Baek
475e09be45 fix: model params 2024-06-01 15:29:47 -07:00
Timothy J. Baek
b124cd4ac5 fix: typo 2024-06-01 13:46:14 -07:00
Timothy Jaeryang Baek
d2cbb14ebf Merge pull request #2705 from cheahjs/fix/show-both-error-and-content
fix: show both message contents and error message
2024-06-01 13:05:34 -07:00
Jun Siang Cheah
1cef48730e fix: show both message contents and error message 2024-06-01 20:59:14 +01:00
Timothy J. Baek
561778d04d feat: pipelines warning message 2024-06-01 12:17:18 -07:00
Timothy J. Baek
51d2cecd36 refac: styling 2024-06-01 12:06:12 -07:00
Timothy J. Baek
6758f3d390 refac: disable sortable on mobile 2024-06-01 12:03:36 -07:00
Timothy J. Baek
6bd7c20fbb fix 2024-06-01 10:33:49 -07:00
Timothy Jaeryang Baek
572155b40c Merge pull request #2696 from cheahjs/fix/docker-layer-caching
fix: don't use BUILD_HASH build arg until needed, busts cache otherwise
2024-06-01 10:20:17 -07:00
Timothy Jaeryang Baek
950231acf7 Merge pull request #2699 from cheahjs/feat/error-message-separate
refac: error message separate from content
2024-06-01 10:19:44 -07:00
Timothy J. Baek
78b279cabc refac 2024-06-01 10:19:27 -07:00
Jun Siang Cheah
fd31b5f8d6 refac: error message separate from content 2024-06-01 15:54:02 +01:00
Jun Siang Cheah
afbea2ecea fix: don't use BUILD_HASH build arg until needed, busts cache otherwise 2024-06-01 14:16:39 +01:00
Timothy J. Baek
3730251958 refac: selector mobile styling 2024-05-31 19:51:34 -07:00
Timothy J. Baek
2dbbd1bc6f refac: pipelines 2024-05-31 19:40:58 -07:00
Timothy J. Baek
5b01d7994a fix: openai notification issue 2024-05-31 14:21:26 -07:00
Timothy J. Baek
5ef478a154 fix: read only compare messages 2024-05-31 14:19:46 -07:00
Timothy J. Baek
92700302b9 fix: edit 2024-05-31 13:35:47 -07:00
Timothy J. Baek
cb8c45d864 fix: pipelines 2024-05-31 13:30:12 -07:00
Timothy J. Baek
fc2b314c4f feat: model ordering 2024-05-31 13:11:04 -07:00
Timothy J. Baek
9c67a94542 refac: styling 2024-05-31 12:26:49 -07:00
Timothy J. Baek
cddbeb023d feat: model selector tag search 2024-05-31 12:23:29 -07:00
Timothy J. Baek
eca20b1b2c feat: model tag support 2024-05-31 12:08:25 -07:00
Timothy J. Baek
0cf9b07ec2 refac: model list styling 2024-05-31 11:24:24 -07:00
Timothy J. Baek
9dfa334a83 feat: CURRENT_DATE, USER_NAME prompt variable support 2024-05-31 11:11:28 -07:00
Timothy J. Baek
7674229e3a feat: chat clone 2024-05-31 10:30:42 -07:00
Timothy Jaeryang Baek
36e2a5e674 Create CODE_OF_CONDUCT.md 2024-05-31 11:48:56 -05:00
Timothy Jaeryang Baek
eb12d1e111 Merge pull request #2680 from aguvener/dev
i18n: incorrect translations fixed and new translations synced
2024-05-31 09:21:24 -07:00
aguvener
ffcb8da346 fix: incorrect translations fixed and new translations synced 2024-05-31 15:12:07 +03:00
Timothy J. Baek
c999c86c5e refac 2024-05-31 02:15:41 -07:00
Timothy J. Baek
9283ed1685 feat: hide/unhide model from selector 2024-05-31 02:11:25 -07:00
Беклемишев Петр Алексеевич
ad32a2ef3c Drop mysql restarts 2024-05-31 13:26:23 +07:00
Timothy J. Baek
995f7bc51b fix: chat completed 2024-05-30 21:43:10 -07:00
Timothy J. Baek
e1d65065f5 fix 2024-05-30 16:31:55 -07:00
Timothy J. Baek
6922529b78 enh: keep originalContent 2024-05-30 16:23:10 -07:00
Беклемишев Петр Алексеевич
e59e1f5049 Fix rebase artifacts 2024-05-30 20:44:13 +07:00
Беклемишев Петр Алексеевич
e1fa453eda Test reconnection to postgres in gh actions 2024-05-30 20:41:10 +07:00
Беклемишев Петр Алексеевич
dfbc125947 Reconnect to postgresql & mysql external databases when getting disconnected 2024-05-30 20:41:06 +07:00
Timothy J. Baek
9cd150a048 chore: version bump 2024-05-30 02:40:17 -07:00
Timothy J. Baek
fec51342ca fix: filter outlet issue with many model chat 2024-05-30 02:37:43 -07:00
Timothy J. Baek
ef8d84296e feat: pipelines filter outlet 2024-05-30 02:04:29 -07:00
Timothy J. Baek
d9ceb31674 chore: format 2024-05-29 23:33:56 -07:00
Timothy J. Baek
c76330710f refac: pipeline filters 2024-05-29 23:29:45 -07:00
Timothy J. Baek
4715160b53 fix: pipelines 2024-05-29 22:41:51 -07:00
Timothy J. Baek
7cec88c776 fix 2024-05-29 22:27:10 -07:00
Timothy J. Baek
340b399fc2 refac: pipelines 2024-05-29 22:18:27 -07:00
Timothy J. Baek
12e60d8ebf feat: add/delete pipelines 2024-05-29 22:03:22 -07:00
Timothy J. Baek
c1cabf1415 feat: multiple pipelines server support 2024-05-29 21:26:57 -07:00
Timothy J. Baek
2981caa34b fix: import models 2024-05-29 14:05:46 -07:00
Timothy Jaeryang Baek
5c6373e057 Merge pull request #2645 from jasinliu/patch-1
enh: Update the style of ResponseMessage to change the space between paras
2024-05-29 12:46:13 -07:00
Timothy J. Baek
e427ef767b fix: openai proxy 2024-05-29 11:28:42 -07:00
Timothy J. Baek
37c87e3a14 fix: model import 2024-05-29 11:12:09 -07:00
Timothy J. Baek
7f8be4a709 refac 2024-05-29 01:42:42 -07:00
Timothy J. Baek
5cf44ac7da feat: display pipelines server in api settings 2024-05-29 01:36:13 -07:00
Timothy J. Baek
e8d4e03c0d refac: openai allow empty key 2024-05-29 01:12:25 -07:00
Liu Jiajun
5609b12852 Update ResponseMessage.svelte 2024-05-29 12:47:04 +08:00
Timothy J. Baek
943baad689 feat: allow valve pipelines edit 2024-05-28 18:24:39 -07:00
Timothy J. Baek
2bdfd85137 fix: pipelines setting 2024-05-28 16:12:54 -07:00
Timothy J. Baek
834bdf46be refac 2024-05-28 15:47:14 -07:00
Timothy Jaeryang Baek
cf1c8be85f Merge pull request #2637 from open-webui/pipelines
feat: pipeline valves
2024-05-28 13:06:46 -07:00
Timothy J. Baek
2d596d7307 feat: valves full integration 2024-05-28 13:05:31 -07:00
Timothy J. Baek
7c271734a1 fix 2024-05-28 12:36:22 -07:00
Timothy J. Baek
130d15a2fb feat: pipeline valves 2024-05-28 12:32:49 -07:00
Timothy J. Baek
0bef1b44c0 feat: pipeline valves 2024-05-28 12:04:19 -07:00
Timothy J. Baek
0383efa207 refac: pipelines 2024-05-28 11:43:48 -07:00
Timothy J. Baek
e231333bcd refac 2024-05-28 09:50:17 -07:00
Timothy Jaeryang Baek
eea8fb591a Merge pull request #2570 from silentoplayz/silentoplayz-update-readme
docs: Update README to display prominent features and link to a new dedicated features page
2024-05-28 09:24:28 -07:00
silentoplayz
0bef7fef29 Update README.md
- 🔍 **Web Search for RAG**: Perform web searches using providers like `SearXNG`, `Google PSE`, `Brave Search`, `serpstack`, and `serper`, and inject the results directly into your chat experience.
2024-05-28 05:51:25 +00:00
Timothy J. Baek
b870fd1118 fix: task model options 2024-05-27 22:24:48 -07:00
Timothy J. Baek
ec36493d61 feat: pipeline filter wildcard support 2024-05-27 20:26:24 -07:00
Timothy J. Baek
966f10e715 refac: pipeline valves -> filters 2024-05-27 19:34:05 -07:00
Timothy J. Baek
4fac99c5b3 feat: pipeline valves user field 2024-05-27 19:16:07 -07:00
Timothy J. Baek
cc6d9bb8c0 feat: pipeline valve support 2024-05-27 19:03:26 -07:00
silentoplayz
2070fa9e99 Update README.md
should be the last fix!
2024-05-28 01:18:08 +00:00
silentoplayz
0edb940dca Update README.md
PWA
2024-05-27 23:42:41 +00:00
silentoplayz
da4a057273 Update README.md
fix
2024-05-27 23:07:49 +00:00
silentoplayz
cc01c5004b Update README.md
fixes
2024-05-27 23:06:55 +00:00
Timothy Jaeryang Baek
abce172b9d Merge pull request #2602 from cheahjs/feat/openai-usage-stats
feat: add OpenAI generation stats
2024-05-27 15:26:00 -07:00
Timothy Jaeryang Baek
9d370c51e2 Merge pull request #2612 from arkohut/support-py-for-run-code
feat: support py Python for code execution
2024-05-27 15:15:48 -07:00
Timothy J. Baek
e2e96b5776 refac: citations styling 2024-05-27 15:10:38 -07:00
Timothy J. Baek
5de2b00064 refac: styling 2024-05-27 15:01:55 -07:00
Timothy Jaeryang Baek
ddc8d22e98 Merge pull request #2599 from open-webui/websearch
feat: websearch
2024-05-27 14:59:48 -07:00
Timothy J. Baek
66734a7080 refac: styling 2024-05-27 14:58:10 -07:00
Timothy J. Baek
4d64893661 refac: styling 2024-05-27 14:55:08 -07:00
Timothy J. Baek
74a8deb19f refac 2024-05-27 14:25:36 -07:00
Timothy J. Baek
f3527df644 refac: styling 2024-05-27 13:22:24 -07:00
Timothy J. Baek
4685f523b6 refac 2024-05-27 12:48:08 -07:00
Timothy J. Baek
73178cf519 refac 2024-05-27 11:08:01 -07:00
Jun Siang Cheah
a1f4706aa1 fix: OAUTH_MERGE_ACCOUNTS_BY_EMAIL not being correctly read 2024-05-27 18:16:36 +01:00
Jun Siang Cheah
6a36039a59 Merge remote-tracking branch 'upstream/dev' into feat/oauth 2024-05-27 18:14:43 +01:00
Jun Siang Cheah
985fdca585 refac: move things around, uplift oauth endpoints 2024-05-27 18:07:38 +01:00
arkohut
5166e92f90 Merge branch 'dev' into support-py-for-run-code 2024-05-28 01:05:15 +08:00
arkohut
b443d61c6f feat: support py Python for code execution 2024-05-28 00:57:36 +08:00
Jun Siang Cheah
06dbf59742 feat: make oauth config persist into config.json 2024-05-27 17:21:24 +01:00
Jun Siang Cheah
4a4e323a44 refac: rename OPENID -> OAUTH 2024-05-27 17:17:34 +01:00
Jun Siang Cheah
99b1661638 feat: add OpenAI generation stats 2024-05-27 10:11:53 +01:00
Timothy Jaeryang Baek
f1a7c76693 Merge pull request #2004 from cheahjs/feat/backend-web-search
feat: add user toggleable web search
2024-05-26 23:40:13 -07:00
Timothy Jaeryang Baek
bced90734b Merge branch 'websearch' into feat/backend-web-search 2024-05-26 23:40:05 -07:00
Timothy J. Baek
b6b71c08f3 fix: styling 2024-05-26 23:28:52 -07:00
Timothy J. Baek
47a9a2a190 refac: default model moved to admin settings 2024-05-26 23:04:54 -07:00
Timothy J. Baek
f2285f29b7 refac: save only to db 2024-05-26 22:52:29 -07:00
Timothy Jaeryang Baek
ee913cb6e8 Merge pull request #2597 from que-nguyen/dev
Fix errors and improve the Vietnamese translation
2024-05-26 22:49:30 -07:00
Timothy Jaeryang Baek
dd959c8f88 Merge pull request #2598 from open-webui/user-settings
feat: user settings to db
2024-05-26 22:48:13 -07:00
Timothy J. Baek
ccbafca74c feat: save user settings to db 2024-05-26 22:47:42 -07:00
silentoplayz
f98ada1776 Update README.md
Adjustment (can revert if necessary)
2024-05-27 04:54:38 +00:00
Que Nguyen
fab3f7fb1a Fix errors and improve the Vietnamese translation 2024-05-27 11:36:37 +07:00
silentoplayz
9c2bd0ef7f Update README.md 2024-05-27 02:41:57 +00:00
silentoplayz
ac0d7201e3 Update README.md
fix
2024-05-27 02:37:15 +00:00
silentoplayz
f3dd271e2c Update README.md
Fix
2024-05-27 02:34:55 +00:00
Timothy J. Baek
9d4c07b76a fix: styling 2024-05-26 18:05:49 -07:00
Timothy J. Baek
3b6acaa513 fix: styling 2024-05-26 17:47:37 -07:00
silentoplayz
839877e197 Update README.md
fix
2024-05-27 00:40:34 +00:00
silentoplayz
e39815420e Update README.md
fix
2024-05-27 00:39:31 +00:00
silentoplayz
fc59db3181 Update README.md
Revised Key Features list
2024-05-27 00:32:21 +00:00
silentoplayz
5ea47c1258 Update README.md
Revert change
2024-05-27 00:17:00 +00:00
Timothy J. Baek
225ec3d174 refac: hide banners if models > 1 2024-05-26 17:03:35 -07:00
Timothy J. Baek
b7fc37d992 fix: spinner 2024-05-26 15:21:08 -07:00
Timothy J. Baek
9fb6160010 fix: styling 2024-05-26 13:06:26 -07:00
Timothy Jaeryang Baek
c62cb43f7e Merge pull request #2579 from cheahjs/feat/disable-community-sharing
feat: toggleable community sharing
2024-05-26 10:03:42 -10:00
Timothy J. Baek
0c04b18f36 refac 2024-05-26 13:02:40 -07:00
Timothy Jaeryang Baek
78dedb3389 Merge branch 'dev' into feat/disable-community-sharing 2024-05-26 10:00:51 -10:00
Timothy J. Baek
efedd39db1 refac: styling 2024-05-26 12:55:33 -07:00
Timothy J. Baek
36f92164cd refac: styling 2024-05-26 12:53:45 -07:00
Timothy J. Baek
a8e3d80256 chore: format 2024-05-26 12:46:06 -07:00
Timothy J. Baek
e8a3fe2e07 refac: styling 2024-05-26 12:41:47 -07:00
Timothy J. Baek
d77657f0d0 refac: only show banners in landing 2024-05-26 12:39:48 -07:00
Timothy Jaeryang Baek
3a737af190 Merge pull request #2585 from open-webui/banners
feat: banners
2024-05-26 09:36:58 -10:00
Timothy J. Baek
ecfc057f25 refac: banners dismissible by default
Co-Authored-By: Jun Siang Cheah <me@jscheah.me>
2024-05-26 12:36:51 -07:00
Timothy J. Baek
fd98add6aa refac: styling
Co-Authored-By: Jun Siang Cheah <me@jscheah.me>
2024-05-26 12:32:57 -07:00
Timothy J. Baek
242d4f0c8d feat: banners
Co-Authored-By: Jun Siang Cheah <me@jscheah.me>
2024-05-26 12:18:43 -07:00
silentoplayz
2e215ca85e Update README.md
List only top 10-ish key features of Open WebUI + dedicated features page to soon be linked to README.
2024-05-26 18:21:58 +00:00
Timothy Jaeryang Baek
00f32e2651 Merge pull request #2562 from cheahjs/feat/include-git-hash-everywhere
feat: add git hash of build everywhere
2024-05-26 07:12:55 -10:00
Jun Siang Cheah
e06417f0aa feat: add admin ui for toggling community sharing 2024-05-26 17:23:24 +01:00
Jun Siang Cheah
ccff221921 refac: run admin general settings fetches in parallel 2024-05-26 17:12:52 +01:00
Jun Siang Cheah
1235714914 feat: add ENABLE_COMMUNITY_SHARING 2024-05-26 17:10:25 +01:00
Jun Siang Cheah
6c5f5fe368 refac: move ui flags into feature flags 2024-05-26 17:05:26 +01:00
Jun Siang Cheah
776bb2892c fix: bad user get 2024-05-26 13:07:41 +01:00
Jun Siang Cheah
7385016e36 fix: only get user by emails without oauth sub 2024-05-26 11:59:21 +01:00
Jun Siang Cheah
b3557d05b1 fix: bad authlib version 2024-05-26 11:32:42 +01:00
Jun Siang Cheah
276b7b90b8 Merge remote-tracking branch 'upstream/dev' into feat/backend-web-search 2024-05-26 11:31:23 +01:00
Jun Siang Cheah
aba6308825 Merge remote-tracking branch 'upstream/dev' into feat/include-git-hash-everywhere 2024-05-26 11:27:02 +01:00
Jun Siang Cheah
0210a105bf feat: experimental SSO support for Google, Microsoft, and OIDC 2024-05-26 11:26:15 +01:00
Jun Siang Cheah
a842d8d62b deps: add authlib 2024-05-26 11:25:29 +01:00
Timothy J. Baek
7b81271b9e refac: hide signup when ENABLE_SIGNUP false 2024-05-26 03:13:13 -07:00
Timothy J. Baek
83107c8ea5 feat: archive all 2024-05-26 02:54:35 -07:00
Timothy J. Baek
bf8c32f09f refac: admin panel 2024-05-26 02:43:52 -07:00
Timothy J. Baek
d9de426fcd feat: export all archived chats 2024-05-26 02:22:46 -07:00
Timothy J. Baek
e20bb23409 feat: access archived chats as admin 2024-05-26 02:00:31 -07:00
Timothy J. Baek
62d37f1f10 refac 2024-05-26 01:48:18 -07:00
Timothy J. Baek
555e4e32d5 feat: allow insecure pull 2024-05-26 01:31:17 -07:00
Timothy J. Baek
84bfebd05e fix 2024-05-26 01:17:57 -07:00
Timothy J. Baek
633607a5c2 fix 2024-05-26 01:16:58 -07:00
Timothy J. Baek
1fce466253 refac: folder rename web -> webui 2024-05-26 01:15:48 -07:00
Jun Siang Cheah
4fdb26fdc4 refac: rename build hash vars 2024-05-26 08:49:30 +01:00
Timothy Jaeryang Baek
6e89a481be Merge pull request #2180 from austenadler/dev
Always open links in chat in a new tab
2024-05-25 21:32:39 -10:00
Timothy J. Baek
27f034b216 feat: auto detect user language 2024-05-26 00:17:20 -07:00
Timothy J. Baek
f755f5e512 feat: model search support 2024-05-25 23:52:17 -07:00
silentoplayz
47e8465626 Update README.md
More adjustments
2024-05-26 06:38:40 +00:00
silentoplayz
e2fe87a4d0 Update README.md
Let me cook pls
2024-05-26 06:19:39 +00:00
silentoplayz
c2c7299650 Update README.md
Revamped features list into key features and more features categorized into dropdown lists
2024-05-26 05:12:43 +00:00
Timothy J. Baek
98ec38a8e7 fix 2024-05-25 17:43:28 -07:00
Timothy J. Baek
1ab01170b9 refac: loading 2024-05-25 16:26:25 -07:00
Timothy J. Baek
c1526b0022 fix 2024-05-25 15:56:47 -07:00
Timothy J. Baek
5e4a1ff6fb fix 2024-05-25 15:54:00 -07:00
Timothy J. Baek
74ca4189e2 fix 2024-05-25 15:46:22 -07:00
Timothy J. Baek
7f77b3addb rm litellm dependency 2024-05-25 14:52:08 -07:00
Timothy J. Baek
79f440f302 refac: byebye litellm 2024-05-25 14:43:35 -07:00
Timothy J. Baek
50951459c9 refac: styling 2024-05-25 14:25:53 -07:00
Timothy J. Baek
6c8997fcbc fix 2024-05-25 14:24:00 -07:00
Timothy J. Baek
9ad4a93876 fix: ollama settings 2024-05-25 14:08:41 -07:00
Timothy J. Baek
afcab78cab fix: models 2024-05-25 13:48:45 -07:00
Timothy J. Baek
3d0b3eb577 fix: styling 2024-05-25 13:13:25 -07:00
Timothy Jaeryang Baek
92e5e6c78e Merge pull request #2563 from KodeurKubik/dev
Completed French Translations (2nd time)
2024-05-25 09:49:36 -10:00
Jun Siang Cheah
b1265c9c34 Merge remote-tracking branch 'upstream/dev' into feat/backend-web-search 2024-05-25 14:55:49 +01:00
Kubik
b936c9bff7 Tabs back (idk my editor changed it to spaces) 2024-05-25 14:40:42 +03:00
Kubik
b6474aa02d Hopefully no conflicts here 2024-05-25 14:34:05 +03:00
Timothy Jaeryang Baek
e9c8341d38 Merge pull request #2561 from cheahjs/feat/generate-source-maps
feat: generate production source maps to assist debugging
2024-05-24 23:51:26 -10:00
Timothy J. Baek
1b5dfb3aac chore: format 2024-05-25 02:50:29 -07:00
Timothy Jaeryang Baek
67a5020cdc Merge pull request #2505 from open-webui/dev-models
feat: openai api compatible model presets (profiles/modelfiles)
2024-05-24 23:47:25 -10:00
Timothy J. Baek
a6af20e1eb fix: codeblock rendering issue 2024-05-25 02:47:09 -07:00
Timothy J. Baek
42dcf1b08c refac: stop param 2024-05-25 02:26:26 -07:00
Timothy J. Baek
88d053833d feat: preset backend logic 2024-05-25 02:05:05 -07:00
Timothy J. Baek
7d2ab168f1 refac: repeat_penalty renamed to frequency_penalty 2024-05-25 02:04:47 -07:00
Jun Siang Cheah
a579f5f8cf feat: add git hash of build everywhere 2024-05-25 09:19:49 +01:00
Jun Siang Cheah
3d1cc1e422 feat: generate production source maps to assist debugging 2024-05-25 09:03:04 +01:00
Timothy J. Baek
1fbcf13371 fix: model edit 2024-05-25 01:01:50 -07:00
Timothy J. Baek
bd6ccf8c15 refac: num_predict renamed to max_tokens 2024-05-25 00:58:20 -07:00
Timothy J. Baek
1aa33a4667 refac 2024-05-25 00:44:21 -07:00
Timothy J. Baek
fad07d2049 feat: import models 2024-05-25 00:36:08 -07:00
Timothy J. Baek
ea69087697 fix 2024-05-25 00:24:31 -07:00
Timothy J. Baek
331941dbc1 feat: show desc in model selector 2024-05-25 00:19:26 -07:00
Timothy J. Baek
1fb4c60233 fix: playground 2024-05-25 00:02:17 -07:00
Timothy J. Baek
0c7f0f4430 refac: suggestion prompts 2024-05-24 23:54:22 -07:00
Timothy J. Baek
bbf5e37f6f fix 2024-05-24 23:43:13 -07:00
Timothy J. Baek
96c0b8a6a6 feat: clone support 2024-05-24 23:42:27 -07:00
Timothy J. Baek
0715cd2811 feat: model capabilities 2024-05-24 23:34:58 -07:00
Timothy J. Baek
89d80b58e1 fix: presets should not be allowed as a base model 2024-05-24 22:47:44 -07:00
Timothy J. Baek
83097cd8be refac: remove modelfiles completely 2024-05-24 22:40:36 -07:00
Timothy J. Baek
c8393db97c fix 2024-05-24 22:29:26 -07:00
Timothy J. Baek
ce3d582e61 refac: responseMessage 2024-05-24 22:28:32 -07:00
Timothy J. Baek
dac9634242 feat: create model 2024-05-24 22:21:57 -07:00
Timothy J. Baek
ca3108a54d refac 2024-05-24 20:29:13 -07:00
Timothy J. Baek
e316abcfc8 refac: better migration script 2024-05-24 19:26:27 -07:00
Timothy J. Baek
8bca17ee1d refac 2024-05-24 18:32:48 -07:00
Timothy J. Baek
708d755eda feat: model update 2024-05-24 18:26:36 -07:00
Timothy J. Baek
0a48114bd2 refac 2024-05-24 03:06:57 -07:00
Timothy J. Baek
468c6398cd feat: unified models integration 2024-05-24 03:02:56 -07:00
Timothy J. Baek
e80e4c304a refac 2024-05-24 02:17:48 -07:00
Timothy J. Baek
404875cab9 fix 2024-05-24 02:11:17 -07:00
Timothy J. Baek
6dbd59c7c6 fix: migration 2024-05-24 01:43:04 -07:00
Timothy J. Baek
110ed67468 feat: unified /models endpoint 2024-05-24 01:40:48 -07:00
Timothy J. Baek
4d57e08b38 feat: modelfiles to models 2024-05-24 00:26:00 -07:00
Timothy J. Baek
17e4be49c0 feat: migrate modelfiles to models 2024-05-23 23:47:01 -07:00
Timothy J. Baek
3be0fa63ee fix 2024-05-23 22:59:11 -07:00
Timothy J. Baek
363ab562c3 refac: migration 2024-05-23 22:58:26 -07:00
Timothy J. Baek
7d159ca723 fix: styling 2024-05-22 23:34:29 -07:00
Timothy J. Baek
bfdf4f7a8a refac: #2506 2024-05-22 22:05:13 -07:00
Timothy Jaeryang Baek
f34fd3fbe1 Merge pull request #2140 from cheahjs/feat/model-config
feat: configurable model name, description and vision capability
2024-05-22 14:03:54 -10:00
Timothy J. Baek
d0d76e2ad5 refac 2024-05-22 16:11:02 -07:00
Jun Siang Cheah
60433856a2 Merge remote-tracking branch 'upstream/dev' into feat/backend-web-search 2024-05-22 22:28:45 +01:00
Timothy Jaeryang Baek
98194d97df Merge pull request #2480 from sime2408/feature/translation-fixes-croatian
feat/translation fixes croatian
2024-05-22 09:42:02 -10:00
Timothy Jaeryang Baek
1c2c244041 Merge branch 'dev' into feature/translation-fixes-croatian 2024-05-22 09:41:53 -10:00
Timothy Jaeryang Baek
f8e77ea129 Merge pull request #2490 from reiebrole30/dev
Dev: Added Cebuano Translation
2024-05-22 09:41:23 -10:00
Timothy Jaeryang Baek
bf6f0c380b Merge pull request #2501 from cheahjs/feat/speed-up-loading
refac: speed up app mount by parallelizing API requests
2024-05-22 09:39:55 -10:00
Jun Siang Cheah
929384e39a refac: speed up app mount by parallelizing API requests 2024-05-22 19:40:16 +01:00
Rei Ebrole
c7027f11d6 Update languages.json
Removed duplicate entry of ceb-PH in the lang code json
2024-05-23 02:32:42 +08:00
Timothy Jaeryang Baek
f8778333e5 Merge pull request #2487 from cheahjs/chore/upgrade-deps
chore: upgrade github actions and python dependencies
2024-05-22 06:10:36 -10:00
root
ffd157d80c adding Cebuano translation
Merge remote-tracking branch 'fork/main'
2024-05-22 10:07:04 +00:00
Rei Ebrole
b4d737c2ff Update languages.json 2024-05-22 18:00:21 +08:00
root
482d615950 Added Cebuano 2024-05-22 09:57:47 +00:00
Jun Siang Cheah
1aebf88069 chore: update python dependencies 2024-05-22 09:50:22 +01:00
Jun Siang Cheah
dcae813d05 chore: update github actions 2024-05-22 09:23:26 +01:00
Timothy Jaeryang Baek
8df0429c99 Merge branch 'dev' into feat/model-config 2024-05-21 21:37:04 -10:00
Timothy J. Baek
59602eea09 chore: format 2024-05-22 00:02:20 -07:00
Timothy J. Baek
21ca55dd30 feat: toggleable ollama 2024-05-21 23:58:42 -07:00
sime2408
ab1aeba1dd translation to croatian fixes 2024-05-22 08:08:30 +02:00
Timothy J. Baek
af022947be feat: pipeline integration 2024-05-21 21:58:02 -07:00
Timothy J. Baek
798e6e4128 chore: format 2024-05-21 21:39:45 -07:00
Timothy J. Baek
793a8603ec Update package.json 2024-05-21 21:39:30 -07:00
Timothy J. Baek
771657266a chore: format 2024-05-21 21:38:58 -07:00
Timothy Jaeryang Baek
45bd0f2a91 Merge pull request #2473 from cheahjs/fix/space-litellm-config
fix: space startup script
2024-05-21 18:28:39 -10:00
Timothy Jaeryang Baek
25c9fb6a3c Merge pull request #2474 from baptistecs/patch-1
Fix .js mimetype on Windows 11
2024-05-21 18:27:57 -10:00
Timothy Jaeryang Baek
f753db603b Merge pull request #2478 from Yanyutin753/chat-message
feat make stronger
2024-05-21 18:26:56 -10:00
Timothy J. Baek
c8a60cc1ea 0.2.0.dev1 2024-05-21 21:18:42 -07:00
Timothy J. Baek
5d356bdc6b Delete release-pypi-test.yml 2024-05-21 21:02:40 -07:00
Timothy J. Baek
d08693f4eb Create release-pypi-test.yml 2024-05-21 20:56:11 -07:00
Timothy J. Baek
44d73dd62a refac 2024-05-21 20:39:37 -07:00
Timothy J. Baek
c23a944bac rename 2024-05-21 20:36:50 -07:00
Timothy Jaeryang Baek
e645679496 Merge pull request #2245 from tcztzy/dev
changed: packaging for publishing
2024-05-21 17:33:47 -10:00
Yanyutin753
bf614068c0 feat make stronger 2024-05-22 09:35:59 +08:00
Timothy J. Baek
0cc4366f70 fix 2024-05-21 17:47:53 -07:00
Timothy J. Baek
cc47e63526 fix: chat refac 2024-05-21 17:42:39 -07:00
Baptiste Clarey Sjöstrand
ecb656af70 Fix .js mimetype on Windows 11
Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "text/plain". Strict MIME type checking is enforced for module scripts per HTML spec.
2024-05-22 00:04:00 +02:00
Jun Siang Cheah
b20ec0586e refac: only create space admin user when configured 2024-05-21 22:09:25 +01:00
Jun Siang Cheah
889a2fd77e fix: wrong path for space litellm config 2024-05-21 22:07:51 +01:00
Jun Siang Cheah
f21c8626d6 refac: switch to meta and params, remove source 2024-05-21 22:05:16 +01:00
Timothy Jaeryang Baek
2502bd8aeb Merge pull request #2472 from cheahjs/fix/hf-space
fix: chown /app/backend/data after preloading models
2024-05-21 10:45:20 -10:00
Jun Siang Cheah
54c29f8bd3 fix: chown /app/backend/data after preloading models 2024-05-21 21:30:42 +01:00
Timothy Jaeryang Baek
19bb6e36e1 Merge pull request #2471 from open-webui/hf-space
feat: hf space
2024-05-21 10:00:20 -10:00
Timothy J. Baek
e3f8ca993d refac: dynamically add yaml 2024-05-21 12:59:53 -07:00
Timothy J. Baek
c1a97278a8 feat: deploy to hf spaces 2024-05-21 12:53:43 -07:00
Jun Siang Cheah
7ccef3e77a Merge remote-tracking branch 'upstream/dev' into feat/model-config 2024-05-21 19:17:42 +01:00
Timothy J. Baek
247af333b6 chore: disable dependabot 2024-05-21 11:09:27 -07:00
Timothy Jaeryang Baek
34253d216a Merge pull request #2469 from cheahjs/refac/consolidate-chat-into-single-component
refac: consolidate chat logic into single component
2024-05-21 07:58:08 -10:00
Timothy J. Baek
2e16fe9f85 Update languages.json 2024-05-21 10:56:50 -07:00
Timothy J. Baek
3878a09095 Update languages.json 2024-05-21 10:55:35 -07:00
Timothy Jaeryang Baek
928b791950 Merge pull request #2454 from AlyMobarak/patch-1
Fixed Arabic Translation
2024-05-21 07:52:42 -10:00
Timothy Jaeryang Baek
3aa201b7d5 Merge pull request #2416 from Yanyutin753/tem-trans
Organizes almost all languages and completes those without translations
2024-05-21 07:51:51 -10:00
Jun Siang Cheah
6d237999dc fix: formatting 2024-05-21 18:46:03 +01:00
Timothy Jaeryang Baek
b089986248 Merge pull request #2432 from cheahjs/feat/a1111-integration-test
ci: add image gen with automatic1111 to integration test
2024-05-21 07:36:33 -10:00
Timothy Jaeryang Baek
5d384272b7 Merge pull request #2433 from kroonen/dev
[fix] memory window is masked
2024-05-21 07:34:18 -10:00
Jun Siang Cheah
5eb9b00f8d refac: consolidate chat logic into single component 2024-05-21 18:30:31 +01:00
Timothy J. Baek
8fc64e45b9 refac: styling 2024-05-21 10:16:35 -07:00
Timothy Jaeryang Baek
d43ee0fc5b Merge pull request #2465 from open-webui/dev
fix
2024-05-21 07:07:03 -10:00
Timothy J. Baek
6b57d8d370 chore: format 2024-05-21 10:06:26 -07:00
Timothy Jaeryang Baek
b3de61247b Merge pull request #2453 from AlyMobarak/main
[fix] for issue #2447 (OpenAI-like API fails when a redundant, empty assistant message is sent over)
2024-05-21 07:05:07 -10:00
Aly Salah
87982fcab3 Fixed several translations and filled in most empty ones. 2024-05-21 21:48:13 +08:00
Aly Salah
2e953e6faa Fixed several translations and filled in most empty ones. 2024-05-21 14:09:59 +03:00
Aly Mobarak
b87cf0db7d Filters messages before going through to the OpenAI API to not include any empty messages. 2024-05-21 13:30:44 +03:00
Aly Mobarak
a07b89da6f Updated filter to filter null message objects as well. 2024-05-21 13:30:21 +03:00
Aly Mobarak
8116b04338 Filter message and don't send over any message that has empty contents to OpenAI's API. 2024-05-21 12:07:01 +03:00
Yanyutin753
968df25f35 fix Format & Build Frontend 2024-05-21 12:14:37 +08:00
Yanyutin753
4f0b6e39a8 feat sort by the first letter of the language 2024-05-21 12:03:35 +08:00
Yanyutin753
51b6f0d490 optimize some details 2024-05-21 10:47:07 +08:00
Yanyutin753
7d5a7c6194 optimize some details 2024-05-21 10:44:55 +08:00
Timothy Jaeryang Baek
7a556b2240 Merge pull request #2428 from cheahjs/feat/better-docker-caching
feat: cache docker layers to registry
2024-05-20 16:30:59 -10:00
Timothy Jaeryang Baek
3355577bac Merge pull request #2436 from open-webui/dev
fix: >:(
2024-05-20 16:21:31 -10:00
Timothy J. Baek
23859756d8 fix: >:( 2024-05-20 19:20:49 -07:00
Yanyutin753
9ee2f98a6a optimize traditional Chinese 2024-05-21 10:14:51 +08:00
Yanyutin753
88ee3a4205 optimize and complete the languages.json 2024-05-21 10:09:21 +08:00
Robin Kroonen
e344d3fbf7 Update docker-compose.yaml 2024-05-20 19:56:14 -04:00
Robin Kroonen
e9e17cd0a8 Update docker-compose.yaml 2024-05-20 19:53:35 -04:00
Robin Kroonen
cbec9ee6ca Merge branch 'dev' of https://github.com/kroonen/open-webui into dev 2024-05-20 19:34:11 -04:00
Jun Siang Cheah
a937f6504a feat: add image gen with automatic1111 to integration test 2024-05-20 23:03:05 +01:00
Robin Kroonen
33195b1c4b Update ManageModal.svelte
removed max-h-[22rem] to correct a bug in the UI/UX
2024-05-20 17:55:33 -04:00
Robin Kroonen
ec4c6e3918 Merge branch 'dev' of https://github.com/kroonen/open-webui into dev 2024-05-20 17:35:12 -04:00
Jun Siang Cheah
e142223ed1 feat: cache docker layers to registry 2024-05-20 20:05:53 +01:00
Jun Siang Cheah
224a578e6b Merge remote-tracking branch 'upstream/dev' into feat/backend-web-search 2024-05-20 19:53:23 +01:00
Timothy Jaeryang Baek
aea81fc988 Merge pull request #2427 from open-webui/dev
fix
2024-05-20 07:12:19 -10:00
Timothy J. Baek
54677a3374 fix 2024-05-20 10:10:35 -07:00
Jun Siang Cheah
69bac2a20e feat: use the conversation's model instead of the first model for query gen 2024-05-20 18:07:52 +01:00
Jun Siang Cheah
eb509c460a Merge remote-tracking branch 'origin/dev' into feat/backend-web-search 2024-05-20 18:01:29 +01:00
Yanyutin753
97e6c2ac04 Fixed the Ukrainian translation 2024-05-21 00:59:22 +08:00
Timothy Jaeryang Baek
175293fb63 Merge pull request #2422 from open-webui/dev
fix: rag
2024-05-20 04:23:20 -10:00
Timothy J. Baek
322db31dc9 fix: rag 2024-05-20 07:22:43 -07:00
Robin Kroonen
985ee1c261 Merge branch 'dev' of https://github.com/kroonen/open-webui into dev 2024-05-20 08:33:23 -04:00
Yanyutin753
6e6b1382ef optimize tranditional Chinese 2024-05-20 18:31:33 +08:00
Yanyutin753
5437d90a25 Fix Format & Build Frontend 2024-05-20 18:13:15 +08:00
Yanyutin753
baddd68741 translate all fr-CA 2024-05-20 18:07:57 +08:00
Yanyutin753
936401b456 translate all es-ES 2024-05-20 17:54:29 +08:00
Yanyutin753
f680817386 translate all fa-IR 2024-05-20 17:54:29 +08:00
Yanyutin753
9e23a3bde7 translate all fr-FR 2024-05-20 17:54:29 +08:00
Yanyutin753
a626796650 translate all he-IL 2024-05-20 17:54:29 +08:00
Yanyutin753
04c2db430d translate all ca-ES 2024-05-20 17:54:29 +08:00
Yanyutin753
23fef594fe translate all bn-BD 2024-05-20 17:54:29 +08:00
Yanyutin753
1ad8746f69 translate all bg-BG 2024-05-20 17:54:29 +08:00
Yanyutin753
60df8a6203 translate all hi-IN 2024-05-20 17:54:29 +08:00
Yanyutin753
92f46faf6e translate all ja-JP 2024-05-20 17:54:29 +08:00
Yanyutin753
d203a2a7c6 translate all ka-GE 2024-05-20 17:54:28 +08:00
Yanyutin753
5b96939342 translate all nl-NL 2024-05-20 17:54:28 +08:00
Yanyutin753
2684ef64c7 translate all ko-KR 2024-05-20 17:54:28 +08:00
Yanyutin753
8aef3934c4 translate all de-DE and fi-FI 2024-05-20 17:54:28 +08:00
Yanyutin753
0d43d9ef3a translate all pt-BR 2024-05-20 17:54:28 +08:00
Yanyutin753
e337c7827b translate all pt-PT 2024-05-20 17:54:28 +08:00
Yanyutin753
62b886a030 translate all ru-RU 2024-05-20 17:54:28 +08:00
Yanyutin753
f83dee792a translate all in hr-HR, It-IT and It-LT 2024-05-20 17:54:28 +08:00
Yanyutin753
a354d6043b translate all pa-IN and pl-PL 2024-05-20 17:54:28 +08:00
Yanyutin753
20b9e2abd8 translate all sv-SE 2024-05-20 17:54:28 +08:00
Yanyutin753
02a0a599ff translate all sr-RS and tr-TR 2024-05-20 17:54:28 +08:00
Yanyutin753
314bd28d21 translate all uk-UA 2024-05-20 17:54:28 +08:00
Yanyutin753
9fdaece415 translate all vi-VN 2024-05-20 17:54:28 +08:00
Yanyutin753
9d1013748f translate ar-BH, zh-CN,and zh-TW 2024-05-20 17:54:28 +08:00
Tang Ziya
b60989627e add: github action 2024-05-20 16:34:37 +08:00
Tang Ziya
a018df2734 infra: build 2024-05-20 16:34:37 +08:00
Tang Ziya
d5a4ab46f4 changed: packaging using rye and use file relative path instead of pwd relative. 2024-05-20 16:34:36 +08:00
Jun Siang Cheah
1a16f8fb1c Merge remote-tracking branch 'origin/dev' into feat/model-config 2024-05-20 09:02:41 +01:00
Timothy Jaeryang Baek
2ce6535a2d Merge pull request #2401 from open-webui/dev
fix
2024-05-19 15:33:50 -10:00
Timothy J. Baek
056c413e23 fix 2024-05-19 18:23:22 -07:00
Timothy J. Baek
3c55773bb2 fix 2024-05-19 18:17:18 -07:00
Timothy J. Baek
a600a32f18 fix 2024-05-19 17:53:21 -07:00
Timothy Jaeryang Baek
7068ea9279 Merge pull request #2397 from open-webui/dev
refac
2024-05-19 12:46:14 -10:00
Timothy J. Baek
b088b06575 refac 2024-05-19 15:45:57 -07:00
Robin Kroonen
064d307d96 Merge branch 'dev' of https://github.com/open-webui/open-webui into dev 2024-05-19 16:42:43 -04:00
Timothy Jaeryang Baek
66c2024d76 Merge pull request #2395 from open-webui/dev
fix
2024-05-19 10:25:07 -10:00
Timothy J. Baek
ab271c82ee Update ProfileImage.svelte 2024-05-19 13:24:46 -07:00
Robin Kroonen
dc6db5e8b0 Merge branch 'dev' of https://github.com/kroonen/open-webui into dev 2024-05-19 15:52:21 -04:00
Robin Kroonen
e10d029b06 Merge branch 'dev' of https://github.com/kroonen/open-webui into dev 2024-05-19 15:51:41 -04:00
Jun Siang Cheah
197af1264d fix: remove stray debug log 2024-05-19 22:15:27 +08:00
Jun Siang Cheah
ae26596d0d feat: log exceptions on update_all_models 2024-05-19 22:11:28 +08:00
Jun Siang Cheah
715a4a6c27 Merge remote-tracking branch 'upstream/dev' into feat/model-config 2024-05-19 22:01:33 +08:00
Jun Siang Cheah
fa4c22492f feat: add warning icon to images when some models are not vision capable 2024-05-19 18:57:13 +08:00
Jun Siang Cheah
4002ead6af feat: store model configs in the database 2024-05-19 18:46:24 +08:00
Jun Siang Cheah
1bacd5d93f Merge branch 'dev' into feat/model-config 2024-05-19 13:13:17 +08:00
Jun Siang Cheah
5d3eddf7e9 feat: update to persistent config 2024-05-17 13:51:17 +08:00
Jun Siang Cheah
0665703401 Merge branch 'dev' into feat/model-config 2024-05-17 11:41:37 +08:00
Austen Adler
c9799991f2 Also replace > with &gt; 2024-05-16 16:18:42 -04:00
Jun Siang Cheah
b95027f182 feat: add searched urls to document 2024-05-16 11:47:53 +08:00
Jun Siang Cheah
9021f068b8 Merge remote-tracking branch 'origin/dev' into feat/backend-web-search 2024-05-16 11:36:53 +08:00
Robin Kroonen
867c179e2d latest changes with upstream 2024-05-15 11:10:00 -04:00
Jun Siang Cheah
81a3c97069 Merge branch 'dev' into feat/backend-web-search 2024-05-14 15:20:52 +08:00
Jun Siang Cheah
f94690386c chore: formatting 2024-05-14 14:18:01 +08:00
Jun Siang Cheah
5e1c408937 Merge branch 'dev' into feat/backend-web-search 2024-05-14 14:03:23 +08:00
Jun Siang Cheah
f49e1afaa6 feat: inject search result doc in the response, not the query
this is to handle when we have multiple models selected or regenerate a
response, it'll only add it to the model's response and not add dupes on
the user message
2024-05-12 20:45:46 +08:00
Jun Siang Cheah
44c8b0bb83 feat: rename title generation model to task model 2024-05-12 20:45:46 +08:00
Jun Siang Cheah
466b3e3637 feat: add support for using previous messages for query generation 2024-05-12 20:45:44 +08:00
Jun Siang Cheah
654cc09128 feat: run i18next 2024-05-12 16:16:42 +08:00
Jun Siang Cheah
d98051862d feat: mark websearch docs differently from standard docs 2024-05-12 16:05:00 +08:00
Jun Siang Cheah
3baeda7edc feat: add in-message progress indicator for web search 2024-05-12 15:21:03 +08:00
Jun Siang Cheah
d45804d7f4 feat: web search available is inferred from env vars 2024-05-12 15:19:52 +08:00
Jun Siang Cheah
9ed1a31575 fix: continue with failures when bulk loading urls with WebBaseLoader 2024-05-12 15:19:07 +08:00
Jun Siang Cheah
7538dc051e feat: use url as source name for citations 2024-05-11 23:51:50 +08:00
Jun Siang Cheah
77928ae141 Merge branch 'dev' of https://github.com/open-webui/open-webui into feat/web-search-toggle 2024-05-11 23:51:37 +08:00
Jun Siang Cheah
2660a6e5b8 feat: prototype frontend web search integration 2024-05-11 23:44:34 +08:00
Jun Siang Cheah
619c2f9c71 fix: toggle style 2024-05-11 23:18:59 +08:00
Jun Siang Cheah
14a902fcfa feat: add web search toggle on chat 2024-05-11 23:18:59 +08:00
Jun Siang Cheah
fb8069123e feat: add WEB_SEARCH_RESULT_COUNT to control max number of results 2024-05-11 23:18:59 +08:00
Austen Adler
3a1fbb936b Always open links in a new tab 2024-05-11 00:47:46 -04:00
Jun Siang Cheah
0f50c12c59 fix: formatting 2024-05-09 23:57:39 +08:00
Jun Siang Cheah
fa6e21f5e0 fix: update models after deleting model info 2024-05-09 23:55:56 +08:00
Jun Siang Cheah
02a4412dfc feat: add UI support for updating model info 2024-05-09 23:54:21 +08:00
Jun Siang Cheah
0dbddebcb0 feat: add API endpoint for updating configs 2024-05-09 22:41:07 +08:00
Jun Siang Cheah
8effff6524 feat: update translation files 2024-05-09 20:52:54 +08:00
Jun Siang Cheah
9bc628ca75 fix: file input not working after rejecting images 2024-05-09 20:47:29 +08:00
Jun Siang Cheah
584385e4bf fix: revert wip code for settings UI 2024-05-09 20:27:07 +08:00
Jun Siang Cheah
e69f31267a fix: remove unused API for setting model config 2024-05-09 20:25:53 +08:00
Jun Siang Cheah
e76a444ed9 feat: allow model config via config.json 2024-05-09 20:25:30 +08:00
Timothy Jaeryang Baek
635951b55c Merge branch 'dev' into feat/backend-web-search 2024-05-06 16:26:44 -07:00
Jun Siang Cheah
8b3e370a6e fix: run formatter 2024-05-06 17:11:04 +08:00
Jun Siang Cheah
83f086ccdd fix: do not return raw search exception due to API keys in URLs 2024-05-06 17:09:04 +08:00
Jun Siang Cheah
99e4edd364 feat: add websearch endpoint to RAG API
fix: google PSE endpoint uses GET

fix: google PSE returns link, not url

fix: serper wrong field
2024-05-06 17:09:04 +08:00
Jun Siang Cheah
501ff7a98b feat: backend implementation of various search APIs 2024-05-06 12:28:41 +08:00
374 changed files with 55754 additions and 18076 deletions

View File

@@ -10,7 +10,8 @@ node_modules
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
__pycache__
.env
.idea
venv
_old
uploads
.ipynb_checkpoints

View File

@@ -10,8 +10,4 @@ OPENAI_API_KEY=''
# DO NOT TRACK
SCARF_NO_ANALYTICS=true
DO_NOT_TRACK=true
ANONYMIZED_TELEMETRY=false
# Use locally bundled version of the LiteLLM cost map json
# to avoid repetitive startup connections
LITELLM_LOCAL_MODEL_COST_MAP="True"
ANONYMIZED_TELEMETRY=false

View File

@@ -4,6 +4,7 @@ updates:
directory: '/backend'
schedule:
interval: weekly
target-branch: 'dev'
- package-ecosystem: 'github-actions'
directory: '/'
schedule:

View File

@@ -11,7 +11,7 @@
- [ ] **Dependencies:** Are there any new dependencies? Have you updated the dependency versions in the documentation?
- [ ] **Testing:** Have you written and run sufficient tests for validating the changes?
- [ ] **Code review:** Have you performed a self-review of your code, addressing any coding standard issues and ensuring adherence to the project's coding standards?
- [ ] **Label:** To cleary categorize this pull request, assign a relevant label to the pull request title, using one of the following:
- [ ] **Prefix:** To cleary categorize this pull request, prefix the pull request title, using one of the following:
- **BREAKING CHANGE**: Significant changes that may affect compatibility
- **build**: Changes that affect the build system or external dependencies
- **ci**: Changes to our continuous integration processes or workflows

View File

@@ -11,7 +11,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Check for changes in package.json
run: |
@@ -36,7 +36,7 @@ jobs:
echo "::set-output name=content::$CHANGELOG_ESCAPED"
- name: Create GitHub release
uses: actions/github-script@v5
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
@@ -51,7 +51,7 @@ jobs:
console.log(`Created release ${release.data.html_url}`)
- name: Upload package to GitHub release
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: package
path: .

View File

@@ -0,0 +1,59 @@
name: Deploy to HuggingFace Spaces
on:
push:
branches:
- dev
- main
workflow_dispatch:
jobs:
check-secret:
runs-on: ubuntu-latest
outputs:
token-set: ${{ steps.check-key.outputs.defined }}
steps:
- id: check-key
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
if: "${{ env.HF_TOKEN != '' }}"
run: echo "defined=true" >> $GITHUB_OUTPUT
deploy:
runs-on: ubuntu-latest
needs: [check-secret]
if: needs.check-secret.outputs.token-set == 'true'
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Remove git history
run: rm -rf .git
- name: Prepend YAML front matter to README.md
run: |
echo "---" > temp_readme.md
echo "title: Open WebUI" >> temp_readme.md
echo "emoji: 🐳" >> temp_readme.md
echo "colorFrom: purple" >> temp_readme.md
echo "colorTo: gray" >> temp_readme.md
echo "sdk: docker" >> temp_readme.md
echo "app_port: 8080" >> temp_readme.md
echo "---" >> temp_readme.md
cat README.md >> temp_readme.md
mv temp_readme.md README.md
- name: Configure git
run: |
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
- name: Set up Git and push to Space
run: |
git init --initial-branch=main
git lfs track "*.ttf"
rm demo.gif
git add .
git commit -m "GitHub deploy: ${{ github.sha }}"
git push --force https://open-webui:${HF_TOKEN}@huggingface.co/spaces/open-webui/open-webui main

View File

@@ -11,8 +11,6 @@ on:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
FULL_IMAGE_NAME: ghcr.io/${{ github.repository }}
jobs:
build-main-image:
@@ -28,6 +26,15 @@ jobs:
- linux/arm64
steps:
# GitHub Packages requires the entire repository name to be in lowercase
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
- name: Set repository and image name to lowercase
run: |
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
env:
IMAGE_NAME: '${{ github.repository }}'
- name: Prepare
run: |
platform=${{ matrix.platform }}
@@ -63,6 +70,18 @@ jobs:
flavor: |
latest=${{ github.ref == 'refs/heads/main' }}
- name: Extract metadata for Docker cache
id: cache-meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
flavor: |
prefix=cache-${{ matrix.platform }}-
latest=false
- name: Build Docker image (latest)
uses: docker/build-push-action@v5
id: build
@@ -72,8 +91,10 @@ jobs:
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha
cache-to: type=gha,mode=max
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
build-args: |
BUILD_HASH=${{ github.sha }}
- name: Export digest
run: |
@@ -102,6 +123,15 @@ jobs:
- linux/arm64
steps:
# GitHub Packages requires the entire repository name to be in lowercase
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
- name: Set repository and image name to lowercase
run: |
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
env:
IMAGE_NAME: '${{ github.repository }}'
- name: Prepare
run: |
platform=${{ matrix.platform }}
@@ -123,7 +153,7 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker images (default latest tag)
- name: Extract metadata for Docker images (cuda tag)
id: meta
uses: docker/metadata-action@v5
with:
@@ -139,6 +169,18 @@ jobs:
latest=${{ github.ref == 'refs/heads/main' }}
suffix=-cuda,onlatest=true
- name: Extract metadata for Docker cache
id: cache-meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
flavor: |
prefix=cache-cuda-${{ matrix.platform }}-
latest=false
- name: Build Docker image (cuda)
uses: docker/build-push-action@v5
id: build
@@ -148,9 +190,11 @@ jobs:
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: USE_CUDA=true
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
build-args: |
BUILD_HASH=${{ github.sha }}
USE_CUDA=true
- name: Export digest
run: |
@@ -179,6 +223,15 @@ jobs:
- linux/arm64
steps:
# GitHub Packages requires the entire repository name to be in lowercase
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
- name: Set repository and image name to lowercase
run: |
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
env:
IMAGE_NAME: '${{ github.repository }}'
- name: Prepare
run: |
platform=${{ matrix.platform }}
@@ -216,6 +269,18 @@ jobs:
latest=${{ github.ref == 'refs/heads/main' }}
suffix=-ollama,onlatest=true
- name: Extract metadata for Docker cache
id: cache-meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
flavor: |
prefix=cache-ollama-${{ matrix.platform }}-
latest=false
- name: Build Docker image (ollama)
uses: docker/build-push-action@v5
id: build
@@ -225,9 +290,11 @@ jobs:
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: USE_OLLAMA=true
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
build-args: |
BUILD_HASH=${{ github.sha }}
USE_OLLAMA=true
- name: Export digest
run: |
@@ -247,6 +314,15 @@ jobs:
runs-on: ubuntu-latest
needs: [ build-main-image ]
steps:
# GitHub Packages requires the entire repository name to be in lowercase
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
- name: Set repository and image name to lowercase
run: |
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
env:
IMAGE_NAME: '${{ github.repository }}'
- name: Download digests
uses: actions/download-artifact@v4
with:
@@ -293,6 +369,15 @@ jobs:
runs-on: ubuntu-latest
needs: [ build-cuda-image ]
steps:
# GitHub Packages requires the entire repository name to be in lowercase
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
- name: Set repository and image name to lowercase
run: |
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
env:
IMAGE_NAME: '${{ github.repository }}'
- name: Download digests
uses: actions/download-artifact@v4
with:
@@ -340,6 +425,15 @@ jobs:
runs-on: ubuntu-latest
needs: [ build-ollama-image ]
steps:
# GitHub Packages requires the entire repository name to be in lowercase
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
- name: Set repository and image name to lowercase
run: |
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
env:
IMAGE_NAME: '${{ github.repository }}'
- name: Download digests
uses: actions/download-artifact@v4
with:

View File

@@ -23,7 +23,7 @@ jobs:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

View File

@@ -19,7 +19,7 @@ jobs:
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: '20' # Or specify any other version you want to use

View File

@@ -20,8 +20,12 @@ jobs:
- name: Build and run Compose Stack
run: |
docker compose --file docker-compose.yaml --file docker-compose.api.yaml up --detach --build
docker compose \
--file docker-compose.yaml \
--file docker-compose.api.yaml \
--file docker-compose.a1111-test.yaml \
up --detach --build
- name: Wait for Ollama to be up
timeout-minutes: 5
run: |
@@ -31,6 +35,10 @@ jobs:
done
echo "Service is up!"
- name: Delete Docker build cache
run: |
docker builder prune --all --force
- name: Preload Ollama model
run: |
docker exec ollama ollama pull qwen:0.5b-chat-v1.5-q2_K
@@ -63,6 +71,28 @@ jobs:
path: compose-logs.txt
if-no-files-found: ignore
# pytest:
# name: Run Backend Tests
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - name: Set up Python
# uses: actions/setup-python@v4
# with:
# python-version: ${{ matrix.python-version }}
# - name: Install dependencies
# run: |
# python -m pip install --upgrade pip
# pip install -r backend/requirements.txt
# - name: pytest run
# run: |
# ls -al
# cd backend
# PYTHONPATH=. pytest . -o log_cli=true -o log_cli_level=INFO
migration_test:
name: Run Migration Tests
runs-on: ubuntu-latest
@@ -78,24 +108,24 @@ jobs:
--health-retries 5
ports:
- 5432:5432
# mysql:
# image: mysql
# env:
# MYSQL_ROOT_PASSWORD: mysql
# MYSQL_DATABASE: mysql
# options: >-
# --health-cmd "mysqladmin ping -h localhost"
# --health-interval 10s
# --health-timeout 5s
# --health-retries 5
# ports:
# - 3306:3306
# mysql:
# image: mysql
# env:
# MYSQL_ROOT_PASSWORD: mysql
# MYSQL_DATABASE: mysql
# options: >-
# --health-cmd "mysqladmin ping -h localhost"
# --health-interval 10s
# --health-timeout 5s
# --health-retries 5
# ports:
# - 3306:3306
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
@@ -122,11 +152,11 @@ jobs:
cd backend
uvicorn main:app --port "8080" --forwarded-allow-ips '*' &
UVICORN_PID=$!
# Wait up to 20 seconds for the server to start
for i in {1..20}; do
# Wait up to 40 seconds for the server to start
for i in {1..40}; do
curl -s http://localhost:8080/api/config > /dev/null && break
sleep 1
if [ $i -eq 20 ]; then
if [ $i -eq 40 ]; then
echo "Server failed to start"
kill -9 $UVICORN_PID
exit 1
@@ -138,7 +168,6 @@ jobs:
echo "Server has stopped"
exit 1
fi
- name: Test backend with Postgres
if: success() || steps.sqlite.conclusion == 'failure'
@@ -167,6 +196,25 @@ jobs:
exit 1
fi
# Check that service will reconnect to postgres when connection will be closed
status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health/db)
if [[ "$status_code" -ne 200 ]] ; then
echo "Server has failed before postgres reconnect check"
exit 1
fi
echo "Terminating all connections to postgres..."
python -c "import os, psycopg2 as pg2; \
conn = pg2.connect(dsn=os.environ['DATABASE_URL'].replace('+pool', '')); \
cur = conn.cursor(); \
cur.execute('SELECT pg_terminate_backend(psa.pid) FROM pg_stat_activity psa WHERE datname = current_database() AND pid <> pg_backend_pid();')"
status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health/db)
if [[ "$status_code" -ne 200 ]] ; then
echo "Server has not reconnected to postgres after connection was closed: returned status $status_code"
exit 1
fi
# - name: Test backend with MySQL
# if: success() || steps.sqlite.conclusion == 'failure' || steps.postgres.conclusion == 'failure'
# env:

31
.github/workflows/release-pypi.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: Release to PyPI
on:
push:
branches:
- main # or whatever branch you want to use
jobs:
release:
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/open-webui
permissions:
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- uses: actions/setup-python@v5
with:
python-version: 3.11
- name: Build
run: |
python -m pip install --upgrade pip
pip install build
python -m build .
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

1
.gitignore vendored
View File

@@ -306,3 +306,4 @@ dist
# cypress artifacts
cypress/videos
cypress/screenshots
.vscode/settings.json

View File

@@ -5,6 +5,288 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.3.8] - 2024-07-09
### Added
- **💬 Chat Controls**: Easily adjust parameters for each chat session, offering more precise control over your interactions.
- **📌 Pinned Chats**: Support for pinned chats, allowing you to keep important conversations easily accessible.
- **📄 Apache Tika Integration**: Added support for using Apache Tika as a document loader, enhancing document processing capabilities.
- **🛠️ Custom Environment for OpenID Claims**: Allows setting custom claims for OpenID, providing more flexibility in user authentication.
- **🔧 Enhanced Tools & Functions API**: Introduced 'event_emitter' and 'event_call', now you can also add citations for better documentation and tracking. Detailed documentation will be provided on our documentation website.
- **↔️ Sideways Scrolling in Settings**: Settings tabs container now supports horizontal scrolling for easier navigation.
- **🌑 Darker OLED Theme**: Includes a new, darker OLED theme and improved styling for the light theme, enhancing visual appeal.
- **🌐 Language Updates**: Updated translations for Indonesian, German, French, and Catalan languages, expanding accessibility.
### Fixed
- **⏰ OpenAI Streaming Timeout**: Resolved issues with OpenAI streaming response using the 'AIOHTTP_CLIENT_TIMEOUT' setting, ensuring reliable performance.
- **💡 User Valves**: Fixed malfunctioning user valves, ensuring proper functionality.
- **🔄 Collapsible Components**: Addressed issues with collapsible components not working, restoring expected behavior.
### Changed
- **🗃️ Database Backend**: Switched from Peewee to SQLAlchemy for improved concurrency support, enhancing database performance.
- **🔤 Primary Font Styling**: Updated primary font to Archivo for better visual consistency.
- **🔄 Font Change for Windows**: Replaced Arimo with Inter font for Windows users, improving readability.
- **🚀 Lazy Loading**: Implemented lazy loading for 'faster_whisper' and 'sentence_transformers' to reduce startup memory usage.
- **📋 Task Generation Payload**: Task generations now include only the "task" field in the body instead of "title".
## [0.3.7] - 2024-06-29
### Added
- **🌐 Enhanced Internationalization (i18n)**: Newly introduced Indonesian translation, and updated translations for Turkish, Chinese, and Catalan languages to improve user accessibility.
### Fixed
- **🕵️‍♂️ Browser Language Detection**: Corrected the issue where the application was not properly detecting and adapting to the browser's language settings.
- **🔐 OIDC Admin Role Assignment**: Fixed a bug where the admin role was not being assigned to the first user who signed up via OpenID Connect (OIDC).
- **💬 Chat/Completions Endpoint**: Resolved an issue where the chat/completions endpoint was non-functional when the stream option was set to False.
- **🚫 'WEBUI_AUTH' Configuration**: Addressed the problem where setting 'WEBUI_AUTH' to False was not being applied correctly.
### Changed
- **📦 Dependency Update**: Upgraded 'authlib' from version 1.3.0 to 1.3.1 to ensure better security and performance enhancements.
## [0.3.6] - 2024-06-27
### Added
- **✨ "Functions" Feature**: You can now utilize "Functions" like filters (middleware) and pipe (model) functions directly within the WebUI. While largely compatible with Pipelines, these native functions can be executed easily within Open WebUI. Example use cases for filter functions include usage monitoring, real-time translation, moderation, and automemory. For pipe functions, the scope ranges from Cohere and Anthropic integration directly within Open WebUI, enabling "Valves" for per-user OpenAI API key usage, and much more. If you encounter issues, SAFE_MODE has been introduced.
- **📁 Files API**: Compatible with OpenAI, this feature allows for custom Retrieval-Augmented Generation (RAG) in conjunction with the Filter Function. More examples will be shared on our community platform and official documentation website.
- **🛠️ Tool Enhancements**: Tools now support citations and "Valves". Documentation will be available shortly.
- **🔗 Iframe Support via Files API**: Enables rendering HTML directly into your chat interface using functions and tools. Use cases include playing games like DOOM and Snake, displaying a weather applet, and implementing Anthropic "artifacts"-like features. Stay tuned for updates on our community platform and documentation.
- **🔒 Experimental OAuth Support**: New experimental OAuth support. Check our documentation for more details.
- **🖼️ Custom Background Support**: Set a custom background from Settings > Interface to personalize your experience.
- **🔑 AUTOMATIC1111_API_AUTH Support**: Enhanced security for the AUTOMATIC1111 API.
- **🎨 Code Highlight Optimization**: Improved code highlighting features.
- **🎙️ Voice Interruption Feature**: Reintroduced and now toggleable from Settings > Interface.
- **💤 Wakelock API**: Now in use to prevent screen dimming during important tasks.
- **🔐 API Key Privacy**: All API keys are now hidden by default for better security.
- **🔍 New Web Search Provider**: Added jina_search as a new option.
- **🌐 Enhanced Internationalization (i18n)**: Improved Korean translation and updated Chinese and Ukrainian translations.
### Fixed
- **🔧 Conversation Mode Issue**: Fixed the issue where Conversation Mode remained active after being removed from settings.
- **📏 Scroll Button Obstruction**: Resolved the issue where the scrollToBottom button container obstructed clicks on buttons beneath it.
### Changed
- **⏲️ AIOHTTP_CLIENT_TIMEOUT**: Now set to 'None' by default for improved configuration flexibility.
- **📞 Voice Call Enhancements**: Improved by skipping code blocks and expressions during calls.
- **🚫 Error Message Handling**: Disabled the continuation of operations with error messages.
- **🗂️ Playground Relocation**: Moved the Playground from the workspace to the user menu for better user experience.
## [0.3.5] - 2024-06-16
### Added
- **📞 Enhanced Voice Call**: Text-to-speech (TTS) callback now operates in real-time for each sentence, reducing latency by not waiting for full completion.
- **👆 Tap to Interrupt**: During a call, you can now stop the assistant from speaking by simply tapping, instead of using voice. This resolves the issue of the speaker's voice being mistakenly registered as input.
- **😊 Emoji Call**: Toggle this feature on from the Settings > Interface, allowing LLMs to express emotions using emojis during voice calls for a more dynamic interaction.
- **🖱️ Quick Archive/Delete**: Use the Shift key + mouseover on the chat list to swiftly archive or delete items.
- **📝 Markdown Support in Model Descriptions**: You can now format model descriptions with markdown, enabling bold text, links, etc.
- **🧠 Editable Memories**: Adds the capability to modify memories.
- **📋 Admin Panel Sorting**: Introduces the ability to sort users/chats within the admin panel.
- **🌑 Dark Mode for Quick Selectors**: Dark mode now available for chat quick selectors (prompts, models, documents).
- **🔧 Advanced Parameters**: Adds 'num_keep' and 'num_batch' to advanced parameters for customization.
- **📅 Dynamic System Prompts**: New variables '{{CURRENT_DATETIME}}', '{{CURRENT_TIME}}', '{{USER_LOCATION}}' added for system prompts. Ensure '{{USER_LOCATION}}' is toggled on from Settings > Interface.
- **🌐 Tavily Web Search**: Includes Tavily as a web search provider option.
- **🖊️ Federated Auth Usernames**: Ability to set user names for federated authentication.
- **🔗 Auto Clean URLs**: When adding connection URLs, trailing slashes are now automatically removed.
- **🌐 Enhanced Translations**: Improved Chinese and Swedish translations.
### Fixed
- **⏳ AIOHTTP_CLIENT_TIMEOUT**: Introduced a new environment variable 'AIOHTTP_CLIENT_TIMEOUT' for requests to Ollama lasting longer than 5 minutes. Default is 300 seconds; set to blank ('') for no timeout.
- **❌ Message Delete Freeze**: Resolved an issue where message deletion would sometimes cause the web UI to freeze.
## [0.3.4] - 2024-06-12
### Fixed
- **🔒 Mixed Content with HTTPS Issue**: Resolved a problem where mixed content (HTTP and HTTPS) was causing security warnings and blocking resources on HTTPS sites.
- **🔍 Web Search Issue**: Addressed the problem where web search functionality was not working correctly. The 'ENABLE_RAG_LOCAL_WEB_FETCH' option has been reintroduced to restore proper web searching capabilities.
- **💾 RAG Template Not Being Saved**: Fixed an issue where the RAG template was not being saved correctly, ensuring your custom templates are now preserved as expected.
## [0.3.3] - 2024-06-12
### Added
- **🛠️ Native Python Function Calling**: Introducing native Python function calling within Open WebUI. Weve also included a built-in code editor to seamlessly develop and integrate function code within the 'Tools' workspace. With this, you can significantly enhance your LLMs capabilities by creating custom RAG pipelines, web search tools, and even agent-like features such as sending Discord messages.
- **🌐 DuckDuckGo Integration**: Added DuckDuckGo as a web search provider, giving you more search options.
- **🌏 Enhanced Translations**: Improved translations for Vietnamese and Chinese languages, making the interface more accessible.
### Fixed
- **🔗 Web Search URL Error Handling**: Fixed the issue where a single URL error would disrupt the data loading process in Web Search mode. Now, such errors will be handled gracefully to ensure uninterrupted data loading.
- **🖥️ Frontend Responsiveness**: Resolved the problem where the frontend would stop responding if the backend encounters an error while downloading a model. Improved error handling to maintain frontend stability.
- **🔧 Dependency Issues in pip**: Fixed issues related to pip installations, ensuring all dependencies are correctly managed to prevent installation errors.
## [0.3.2] - 2024-06-10
### Added
- **🔍 Web Search Query Status**: The web search query will now persist in the results section to aid in easier debugging and tracking of search queries.
- **🌐 New Web Search Provider**: We have added Serply as a new option for web search providers, giving you more choices for your search needs.
- **🌏 Improved Translations**: We've enhanced translations for Chinese and Portuguese.
### Fixed
- **🎤 Audio File Upload Issue**: The bug that prevented audio files from being uploaded in chat input has been fixed, ensuring smooth communication.
- **💬 Message Input Handling**: Improved the handling of message inputs by instantly clearing images and text after sending, along with immediate visual indications when a response message is loading, enhancing user feedback.
- **⚙️ Parameter Registration and Validation**: Fixed the issue where parameters were not registering in certain cases and addressed the problem where users were unable to save due to invalid input errors.
## [0.3.1] - 2024-06-09
### Fixed
- **💬 Chat Functionality**: Resolved the issue where chat functionality was not working for specific models.
## [0.3.0] - 2024-06-09
### Added
- **📚 Knowledge Support for Models**: Attach documents directly to models from the models workspace, enhancing the information available to each model.
- **🎙️ Hands-Free Voice Call Feature**: Initiate voice calls without needing to use your hands, making interactions more seamless.
- **📹 Video Call Feature**: Enable video calls with supported vision models like Llava and GPT-4o, adding a visual dimension to your communications.
- **🎛️ Enhanced UI for Voice Recording**: Improved user interface for the voice recording feature, making it more intuitive and user-friendly.
- **🌐 External STT Support**: Now support for external Speech-To-Text services, providing more flexibility in choosing your STT provider.
- **⚙️ Unified Settings**: Consolidated settings including document settings under a new admin settings section for easier management.
- **🌑 Dark Mode Splash Screen**: A new splash screen for dark mode, ensuring a consistent and visually appealing experience for dark mode users.
- **📥 Upload Pipeline**: Directly upload pipelines from the admin settings > pipelines section, streamlining the pipeline management process.
- **🌍 Improved Language Support**: Enhanced support for Chinese and Ukrainian languages, better catering to a global user base.
### Fixed
- **🛠️ Playground Issue**: Fixed the playground not functioning properly, ensuring a smoother user experience.
- **🔥 Temperature Parameter Issue**: Corrected the issue where the temperature value '0' was not being passed correctly.
- **📝 Prompt Input Clearing**: Resolved prompt input textarea not being cleared right away, ensuring a clean slate for new inputs.
- **✨ Various UI Styling Issues**: Fixed numerous user interface styling problems for a more cohesive look.
- **👥 Active Users Display**: Fixed active users showing active sessions instead of actual users, now reflecting accurate user activity.
- **🌐 Community Platform Compatibility**: The Community Platform is back online and fully compatible with Open WebUI.
### Changed
- **📝 RAG Implementation**: Updated the RAG (Retrieval-Augmented Generation) implementation to use a system prompt for context, instead of overriding the user's prompt.
- **🔄 Settings Relocation**: Moved Models, Connections, Audio, and Images settings to the admin settings for better organization.
- **✍️ Improved Title Generation**: Enhanced the default prompt for title generation, yielding better results.
- **🔧 Backend Task Management**: Tasks like title generation and search query generation are now managed on the backend side and controlled only by the admin.
- **🔍 Editable Search Query Prompt**: You can now edit the search query generation prompt, offering more control over how queries are generated.
- **📏 Prompt Length Threshold**: Set the prompt length threshold for search query generation from the admin settings, giving more customization options.
- **📣 Settings Consolidation**: Merged the Banners admin setting with the Interface admin setting for a more streamlined settings area.
## [0.2.5] - 2024-06-05
### Added
- **👥 Active Users Indicator**: Now you can see how many people are currently active and what they are running. This helps you gauge when performance might slow down due to a high number of users.
- **🗂️ Create Ollama Modelfile**: The option to create a modelfile for Ollama has been reintroduced in the Settings > Models section, making it easier to manage your models.
- **⚙️ Default Model Setting**: Added an option to set the default model from Settings > Interface. This feature is now easily accessible, especially convenient for mobile users as it was previously hidden.
- **🌐 Enhanced Translations**: We've improved the Chinese translations and added support for Turkmen and Norwegian languages to make the interface more accessible globally.
### Fixed
- **📱 Mobile View Improvements**: The UI now uses dvh (dynamic viewport height) instead of vh (viewport height), providing a better and more responsive experience for mobile users.
## [0.2.4] - 2024-06-03
### Added
- **👤 Improved Account Pending Page**: The account pending page now displays admin details by default to avoid confusion. You can disable this feature in the admin settings if needed.
- **🌐 HTTP Proxy Support**: We have enabled the use of the 'http_proxy' environment variable in OpenAI and Ollama API calls, making it easier to configure network settings.
- **❓ Quick Access to Documentation**: You can now easily access Open WebUI documents via a question mark button located at the bottom right corner of the screen (available on larger screens like PCs).
- **🌍 Enhanced Translation**: Improvements have been made to translations.
### Fixed
- **🔍 SearxNG Web Search**: Fixed the issue where the SearxNG web search functionality was not working properly.
## [0.2.3] - 2024-06-03
### Added
- **📁 Export Chat as JSON**: You can now export individual chats as JSON files from the navbar menu by navigating to 'Download > Export Chat'. This makes sharing specific conversations easier.
- **✏️ Edit Titles with Double Click**: Double-click on titles to rename them quickly and efficiently.
- **🧩 Batch Multiple Embeddings**: Introduced 'RAG_EMBEDDING_OPENAI_BATCH_SIZE' to process multiple embeddings in a batch, enhancing performance for large datasets.
- **🌍 Improved Translations**: Enhanced the translation quality across various languages for a better user experience.
### Fixed
- **🛠️ Modelfile Migration Script**: Fixed an issue where the modelfile migration script would fail if an invalid modelfile was encountered.
- **💬 Zhuyin Input Method on Mac**: Resolved an issue where using the Zhuyin input method in the Web UI on a Mac caused text to send immediately upon pressing the enter key, leading to incorrect input.
- **🔊 Local TTS Voice Selection**: Fixed the issue where the selected local Text-to-Speech (TTS) voice was not being displayed in settings.
## [0.2.2] - 2024-06-02
### Added
- **🌊 Mermaid Rendering Support**: We've included support for Mermaid rendering. This allows you to create beautiful diagrams and flowcharts directly within Open WebUI.
- **🔄 New Environment Variable 'RESET_CONFIG_ON_START'**: Introducing a new environment variable: 'RESET_CONFIG_ON_START'. Set this variable to reset your configuration settings upon starting the application, making it easier to revert to default settings.
### Fixed
- **🔧 Pipelines Filter Issue**: We've addressed an issue with the pipelines where filters were not functioning as expected.
## [0.2.1] - 2024-06-02
### Added
- **🖱️ Single Model Export Button**: Easily export models with just one click using the new single model export button.
- **🖥️ Advanced Parameters Support**: Added support for 'num_thread', 'use_mmap', and 'use_mlock' parameters for Ollama.
- **🌐 Improved Vietnamese Translation**: Enhanced Vietnamese language support for a better user experience for our Vietnamese-speaking community.
### Fixed
- **🔧 OpenAI URL API Save Issue**: Corrected a problem preventing the saving of OpenAI URL API settings.
- **🚫 Display Issue with Disabled Ollama API**: Fixed the display bug causing models to appear in settings when the Ollama API was disabled.
### Changed
- **💡 Versioning Update**: As a reminder from our previous update, version 0.2.y will focus primarily on bug fixes, while major updates will be designated as 0.x from now on for better version tracking.
## [0.2.0] - 2024-06-01
### Added
- **🔧 Pipelines Support**: Open WebUI now includes a plugin framework for enhanced customization and functionality (https://github.com/open-webui/pipelines). Easily add custom logic and integrate Python libraries, from AI agents to home automation APIs.
- **🔗 Function Calling via Pipelines**: Integrate function calling seamlessly through Pipelines.
- **⚖️ User Rate Limiting via Pipelines**: Implement user-specific rate limits to manage API usage efficiently.
- **📊 Usage Monitoring with Langfuse**: Track and analyze usage statistics with Langfuse integration through Pipelines.
- **🕒 Conversation Turn Limits**: Set limits on conversation turns to manage interactions better through Pipelines.
- **🛡️ Toxic Message Filtering**: Automatically filter out toxic messages to maintain a safe environment using Pipelines.
- **🔍 Web Search Support**: Introducing built-in web search capabilities via RAG API, allowing users to search using SearXNG, Google Programmatic Search Engine, Brave Search, serpstack, and serper. Activate it effortlessly by adding necessary variables from Document settings > Web Params.
- **🗂️ Models Workspace**: Create and manage model presets for both Ollama/OpenAI API. Note: The old Modelfiles workspace is deprecated.
- **🛠️ Model Builder Feature**: Build and edit all models with persistent builder mode.
- **🏷️ Model Tagging Support**: Organize models with tagging features in the models workspace.
- **📋 Model Ordering Support**: Effortlessly organize models by dragging and dropping them into the desired positions within the models workspace.
- **📈 OpenAI Generation Stats**: Access detailed generation statistics for OpenAI models.
- **📅 System Prompt Variables**: New variables added: '{{CURRENT_DATE}}' and '{{USER_NAME}}' for dynamic prompts.
- **📢 Global Banner Support**: Manage global banners from admin settings > banners.
- **🗃️ Enhanced Archived Chats Modal**: Search and export archived chats easily.
- **📂 Archive All Button**: Quickly archive all chats from settings > chats.
- **🌐 Improved Translations**: Added and improved translations for French, Croatian, Cebuano, and Vietnamese.
### Fixed
- **🔍 Archived Chats Visibility**: Resolved issue with archived chats not showing in the admin panel.
- **💬 Message Styling**: Fixed styling issues affecting message appearance.
- **🔗 Shared Chat Responses**: Corrected the issue where shared chat response messages were not readonly.
- **🖥️ UI Enhancement**: Fixed the scrollbar overlapping issue with the message box in the user interface.
### Changed
- **💾 User Settings Storage**: User settings are now saved on the backend, ensuring consistency across all devices.
- **📡 Unified API Requests**: The API request for getting models is now unified to '/api/models' for easier usage.
- **🔄 Versioning Update**: Our versioning will now follow the format 0.x for major updates and 0.x.y for patches.
- **📦 Export All Chats (All Users)**: Moved this functionality to the Admin Panel settings for better organization and accessibility.
### Removed
- **🚫 Bundled LiteLLM Support Deprecated**: Migrate your LiteLLM config.yaml to a self-hosted LiteLLM instance. LiteLLM can still be added via OpenAI Connections. Download the LiteLLM config.yaml from admin settings > database > export LiteLLM config.yaml.
## [0.1.125] - 2024-05-19
### Added

77
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,77 @@
# Contributor Covenant Code of Conduct
## Our Pledge
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.
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contribute to a positive environment for our community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
- Focusing on what is best not just for us as individuals, but for the overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email address, without their explicit permission
- **Spamming of any kind**
- Aggressive sales tactics targeting our community members are strictly prohibited. You can mention your product if it's relevant to the discussion, but under no circumstances should you push it forcefully
- Other conduct which could reasonably be considered inappropriate in a professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies within all community spaces and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, spamming, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at hello@openwebui.com. All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
### 1. Temporary Ban
**Community Impact**: Any violation of community standards, including but not limited to inappropriate language, unprofessional behavior, harassment, or spamming.
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
### 2. Permanent Ban
**Community Impact**: Repeated or severe violations of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View File

@@ -11,12 +11,14 @@ ARG USE_CUDA_VER=cu121
# IMPORTANT: If you change the embedding model (sentence-transformers/all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them.
ARG USE_EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
ARG USE_RERANKING_MODEL=""
ARG BUILD_HASH=dev-build
# Override at your own risk - non-root configurations are untested
ARG UID=0
ARG GID=0
######## WebUI frontend ########
FROM --platform=$BUILDPLATFORM node:21-alpine3.19 as build
ARG BUILD_HASH
WORKDIR /app
@@ -24,6 +26,7 @@ COPY package.json package-lock.json ./
RUN npm ci
COPY . .
ENV APP_BUILD_HASH=${BUILD_HASH}
RUN npm run build
######## WebUI backend ########
@@ -59,11 +62,6 @@ ENV OPENAI_API_KEY="" \
DO_NOT_TRACK=true \
ANONYMIZED_TELEMETRY=false
# Use locally bundled version of the LiteLLM cost map json
# to avoid repetitive startup connections
ENV LITELLM_LOCAL_MODEL_COST_MAP="True"
#### Other models #########################################################
## whisper TTS model settings ##
ENV WHISPER_MODEL="base" \
@@ -83,10 +81,10 @@ WORKDIR /app/backend
ENV HOME /root
# Create user and group if not root
RUN if [ $UID -ne 0 ]; then \
if [ $GID -ne 0 ]; then \
addgroup --gid $GID app; \
fi; \
adduser --uid $UID --gid $GID --home $HOME --disabled-password --no-create-home app; \
if [ $GID -ne 0 ]; then \
addgroup --gid $GID app; \
fi; \
adduser --uid $UID --gid $GID --home $HOME --disabled-password --no-create-home app; \
fi
RUN mkdir -p $HOME/.cache/chroma
@@ -132,7 +130,8 @@ RUN pip3 install uv && \
uv pip install --system -r requirements.txt --no-cache-dir && \
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
fi
fi; \
chown -R $UID:$GID /app/backend/data/
@@ -154,4 +153,7 @@ HEALTHCHECK CMD curl --silent --fail http://localhost:8080/health | jq -e '.stat
USER $UID:$GID
ARG BUILD_HASH
ENV WEBUI_BUILD_VERSION=${BUILD_HASH}
CMD [ "bash", "start.sh"]

View File

@@ -11,97 +11,47 @@
[![Discord](https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white)](https://discord.gg/5rJgQTnV4s)
[![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/tjbck)
Open WebUI is an extensible, feature-rich, and user-friendly self-hosted WebUI designed to operate entirely offline. It supports various LLM runners, including Ollama and OpenAI-compatible APIs. For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/).
Open WebUI is an [extensible](https://github.com/open-webui/pipelines), feature-rich, and user-friendly self-hosted WebUI designed to operate entirely offline. It supports various LLM runners, including Ollama and OpenAI-compatible APIs. For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/).
![Open WebUI Demo](./demo.gif)
## Features ⭐
## Key Features of Open WebUI
- 🖥️ **Intuitive Interface**: Our chat interface takes inspiration from ChatGPT, ensuring a user-friendly experience.
- 🚀 **Effortless Setup**: Install seamlessly using Docker or Kubernetes (kubectl, kustomize or helm) for a hassle-free experience with support for both `:ollama` and `:cuda` tagged images.
- 📱 **Responsive Design**: Enjoy a seamless experience on both desktop and mobile devices.
- 🤝 **Ollama/OpenAI API Integration**: Effortlessly integrate OpenAI-compatible APIs for versatile conversations alongside Ollama models. Customize the OpenAI API URL to link with **LMStudio, GroqCloud, Mistral, OpenRouter, and more**.
- **Swift Responsiveness**: Enjoy fast and responsive performance.
- 🧩 **Pipelines, Open WebUI Plugin Support**: Seamlessly integrate custom logic and Python libraries into Open WebUI using [Pipelines Plugin Framework](https://github.com/open-webui/pipelines). Launch your Pipelines instance, set the OpenAI URL to the Pipelines URL, and explore endless possibilities. [Examples](https://github.com/open-webui/pipelines/tree/main/examples) include **Function Calling**, User **Rate Limiting** to control access, **Usage Monitoring** with tools like Langfuse, **Live Translation with LibreTranslate** for multilingual support, **Toxic Message Filtering** and much more.
- 🚀 **Effortless Setup**: Install seamlessly using Docker or Kubernetes (kubectl, kustomize or helm) for a hassle-free experience.
- 📱 **Responsive Design**: Enjoy a seamless experience across Desktop PC, Laptop, and Mobile devices.
- 🌈 **Theme Customization**: Choose from a variety of themes to personalize your Open WebUI experience.
- 💻 **Code Syntax Highlighting**: Enjoy enhanced code readability with our syntax highlighting feature.
- 📱 **Progressive Web App (PWA) for Mobile**: Enjoy a native app-like experience on your mobile device with our PWA, providing offline access on localhost and a seamless user interface.
- ✒️🔢 **Full Markdown and LaTeX Support**: Elevate your LLM experience with comprehensive Markdown and LaTeX capabilities for enriched interaction.
- 📚 **Local RAG Integration**: Dive into the future of chat interactions with the groundbreaking Retrieval Augmented Generation (RAG) support. This feature seamlessly integrates document interactions into your chat experience. You can load documents directly into the chat or add files to your document library, effortlessly accessing them using `#` command in the prompt. In its alpha phase, occasional issues may arise as we actively refine and enhance this feature to ensure optimal performance and reliability.
- 🎤📹 **Hands-Free Voice/Video Call**: Experience seamless communication with integrated hands-free voice and video call features, allowing for a more dynamic and interactive chat environment.
- 🔍 **RAG Embedding Support**: Change the RAG embedding model directly in document settings, enhancing document processing. This feature supports Ollama and OpenAI models.
- 🛠️ **Model Builder**: Easily create Ollama models via the Web UI. Create and add custom characters/agents, customize chat elements, and import models effortlessly through [Open WebUI Community](https://openwebui.com/) integration.
- 🌐 **Web Browsing Capability**: Seamlessly integrate websites into your chat experience using the `#` command followed by the URL. This feature allows you to incorporate web content directly into your conversations, enhancing the richness and depth of your interactions.
- 🐍 **Native Python Function Calling Tool**: Enhance your LLMs with built-in code editor support in the tools workspace. Bring Your Own Function (BYOF) by simply adding your pure Python functions, enabling seamless integration with LLMs.
- 📜 **Prompt Preset Support**: Instantly access preset prompts using the `/` command in the chat input. Load predefined conversation starters effortlessly and expedite your interactions. Effortlessly import prompts through [Open WebUI Community](https://openwebui.com/) integration.
- 📚 **Local RAG Integration**: Dive into the future of chat interactions with groundbreaking Retrieval Augmented Generation (RAG) support. This feature seamlessly integrates document interactions into your chat experience. You can load documents directly into the chat or add files to your document library, effortlessly accessing them using the `#` command before a query.
- 👍👎 **RLHF Annotation**: Empower your messages by rating them with thumbs up and thumbs down, followed by the option to provide textual feedback, facilitating the creation of datasets for Reinforcement Learning from Human Feedback (RLHF). Utilize your messages to train or fine-tune models, all while ensuring the confidentiality of locally saved data.
- 🔍 **Web Search for RAG**: Perform web searches using providers like `SearXNG`, `Google PSE`, `Brave Search`, `serpstack`, `serper`, `Serply`, `DuckDuckGo` and `TavilySearch` and inject the results directly into your chat experience.
- 🏷️ **Conversation Tagging**: Effortlessly categorize and locate specific chats for quick reference and streamlined data collection.
- 🌐 **Web Browsing Capability**: Seamlessly integrate websites into your chat experience using the `#` command followed by a URL. This feature allows you to incorporate web content directly into your conversations, enhancing the richness and depth of your interactions.
- 📥🗑️ **Download/Delete Models**: Easily download or remove models directly from the web UI.
- 🔄 **Update All Ollama Models**: Easily update locally installed models all at once with a convenient button, streamlining model management.
- ⬆️ **GGUF File Model Creation**: Effortlessly create Ollama models by uploading GGUF files directly from the web UI. Streamlined process with options to upload from your machine or download GGUF files from Hugging Face.
- 🤖 **Multiple Model Support**: Seamlessly switch between different chat models for diverse interactions.
- 🔄 **Multi-Modal Support**: Seamlessly engage with models that support multimodal interactions, including images (e.g., LLava).
- 🧩 **Modelfile Builder**: Easily create Ollama modelfiles via the web UI. Create and add characters/agents, customize chat elements, and import modelfiles effortlessly through [Open WebUI Community](https://openwebui.com/) integration.
- 🎨 **Image Generation Integration**: Seamlessly incorporate image generation capabilities using options such as AUTOMATIC1111 API or ComfyUI (local), and OpenAI's DALL-E (external), enriching your chat experience with dynamic visual content.
- ⚙️ **Many Models Conversations**: Effortlessly engage with various models simultaneously, harnessing their unique strengths for optimal responses. Enhance your experience by leveraging a diverse set of models in parallel.
- 💬 **Collaborative Chat**: Harness the collective intelligence of multiple models by seamlessly orchestrating group conversations. Use the `@` command to specify the model, enabling dynamic and diverse dialogues within your chat interface. Immerse yourself in the collective intelligence woven into your chat environment.
- 🗨️ **Local Chat Sharing**: Generate and share chat links seamlessly between users, enhancing collaboration and communication.
- 🔄 **Regeneration History Access**: Easily revisit and explore your entire regeneration history.
- 📜 **Chat History**: Effortlessly access and manage your conversation history.
- 📬 **Archive Chats**: Effortlessly store away completed conversations with LLMs for future reference, maintaining a tidy and clutter-free chat interface while allowing for easy retrieval and reference.
- 📤📥 **Import/Export Chat History**: Seamlessly move your chat data in and out of the platform.
- 🗣️ **Voice Input Support**: Engage with your model through voice interactions; enjoy the convenience of talking to your model directly. Additionally, explore the option for sending voice input automatically after 3 seconds of silence for a streamlined experience.
- 🔊 **Configurable Text-to-Speech Endpoint**: Customize your Text-to-Speech experience with configurable OpenAI endpoints.
- ⚙️ **Fine-Tuned Control with Advanced Parameters**: Gain a deeper level of control by adjusting parameters such as temperature and defining your system prompts to tailor the conversation to your specific preferences and needs.
- 🎨🤖 **Image Generation Integration**: Seamlessly incorporate image generation capabilities using options such as AUTOMATIC1111 API (local), ComfyUI (local), and DALL-E, enriching your chat experience with dynamic visual content.
- 🤝 **OpenAI API Integration**: Effortlessly integrate OpenAI-compatible API for versatile conversations alongside Ollama models. Customize the API Base URL to link with **LMStudio, Mistral, OpenRouter, and more**.
-**Multiple OpenAI-Compatible API Support**: Seamlessly integrate and customize various OpenAI-compatible APIs, enhancing the versatility of your chat interactions.
- 🔑 **API Key Generation Support**: Generate secret keys to leverage Open WebUI with OpenAI libraries, simplifying integration and development.
- 🔗 **External Ollama Server Connection**: Seamlessly link to an external Ollama server hosted on a different address by configuring the environment variable.
- 🔀 **Multiple Ollama Instance Load Balancing**: Effortlessly distribute chat requests across multiple Ollama instances for enhanced performance and reliability.
- 👥 **Multi-User Management**: Easily oversee and administer users via our intuitive admin panel, streamlining user management processes.
- 🔗 **Webhook Integration**: Subscribe to new user sign-up events via webhook (compatible with Google Chat and Microsoft Teams), providing real-time notifications and automation capabilities.
- 🛡️ **Model Whitelisting**: Admins can whitelist models for users with the 'user' role, enhancing security and access control.
- 📧 **Trusted Email Authentication**: Authenticate using a trusted email header, adding an additional layer of security and authentication.
- 🔐 **Role-Based Access Control (RBAC)**: Ensure secure access with restricted permissions; only authorized individuals can access your Ollama, and exclusive model creation/pulling rights are reserved for administrators.
- 🔒 **Backend Reverse Proxy Support**: Bolster security through direct communication between Open WebUI backend and Ollama. This key feature eliminates the need to expose Ollama over LAN. Requests made to the '/ollama/api' route from the web UI are seamlessly redirected to Ollama from the backend, enhancing overall system security.
- 🌐🌍 **Multilingual Support**: Experience Open WebUI in your preferred language with our internationalization (i18n) support. Join us in expanding our supported languages! We're actively seeking contributors!
- 🌟 **Continuous Updates**: We are committed to improving Open WebUI with regular updates and new features.
- 🌟 **Continuous Updates**: We are committed to improving Open WebUI with regular updates, fixes, and new features.
Want to learn more about Open WebUI's features? Check out our [Open WebUI documentation](https://docs.openwebui.com/features) for a comprehensive overview!
## 🔗 Also Check Out Open WebUI Community!
@@ -200,10 +150,19 @@ docker run --rm --volume /var/run/docker.sock:/var/run/docker.sock containrrr/wa
In the last part of the command, replace `open-webui` with your container name if it is different.
### Moving from Ollama WebUI to Open WebUI
Check our Migration Guide available in our [Open WebUI Documentation](https://docs.openwebui.com/migration/).
### Using the Dev Branch 🌙
> [!WARNING]
> The `:dev` branch contains the latest unstable features and changes. Use it at your own risk as it may have bugs or incomplete features.
If you want to try out the latest bleeding-edge features and are okay with occasional instability, you can use the `:dev` tag like this:
```bash
docker run -d -p 3000:8080 -v open-webui:/app/backend/data --name open-webui --add-host=host.docker.internal:host-gateway --restart always ghcr.io/open-webui/open-webui:dev
```
## What's Next? 🌟
Discover upcoming features on our roadmap in the [Open WebUI Documentation](https://docs.openwebui.com/roadmap/).

View File

@@ -18,6 +18,10 @@ If you're experiencing connection issues, its often due to the WebUI docker c
docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://127.0.0.1:11434 --name open-webui --restart always ghcr.io/open-webui/open-webui:main
```
### Error on Slow Reponses for Ollama
Open WebUI has a default timeout of 5 minutes for Ollama to finish generating the response. If needed, this can be adjusted via the environment variable AIOHTTP_CLIENT_TIMEOUT, which sets the timeout in seconds.
### General Connection Errors
**Ensure Ollama Version is Up-to-Date**: Always start by checking that you have the latest version of Ollama. Visit [Ollama's official site](https://ollama.com/) for the latest updates.

114
backend/alembic.ini Normal file
View File

@@ -0,0 +1,114 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = migrations
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python>=3.9 or backports.zoneinfo library.
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to migrations/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
# sqlalchemy.url = REPLACE_WITH_DATABASE_URL
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
# hooks = ruff
# ruff.type = exec
# ruff.executable = %(here)s/.venv/bin/ruff
# ruff.options = --fix REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

View File

@@ -14,16 +14,14 @@ from fastapi import (
from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
from fastapi.middleware.cors import CORSMiddleware
from faster_whisper import WhisperModel
from pydantic import BaseModel
import uuid
import requests
import hashlib
from pathlib import Path
import json
from constants import ERROR_MESSAGES
from utils.utils import (
decode_token,
@@ -41,10 +39,15 @@ from config import (
WHISPER_MODEL_DIR,
WHISPER_MODEL_AUTO_UPDATE,
DEVICE_TYPE,
AUDIO_OPENAI_API_BASE_URL,
AUDIO_OPENAI_API_KEY,
AUDIO_OPENAI_API_MODEL,
AUDIO_OPENAI_API_VOICE,
AUDIO_STT_OPENAI_API_BASE_URL,
AUDIO_STT_OPENAI_API_KEY,
AUDIO_TTS_OPENAI_API_BASE_URL,
AUDIO_TTS_OPENAI_API_KEY,
AUDIO_STT_ENGINE,
AUDIO_STT_MODEL,
AUDIO_TTS_ENGINE,
AUDIO_TTS_MODEL,
AUDIO_TTS_VOICE,
AppConfig,
)
@@ -61,10 +64,17 @@ app.add_middleware(
)
app.state.config = AppConfig()
app.state.config.OPENAI_API_BASE_URL = AUDIO_OPENAI_API_BASE_URL
app.state.config.OPENAI_API_KEY = AUDIO_OPENAI_API_KEY
app.state.config.OPENAI_API_MODEL = AUDIO_OPENAI_API_MODEL
app.state.config.OPENAI_API_VOICE = AUDIO_OPENAI_API_VOICE
app.state.config.STT_OPENAI_API_BASE_URL = AUDIO_STT_OPENAI_API_BASE_URL
app.state.config.STT_OPENAI_API_KEY = AUDIO_STT_OPENAI_API_KEY
app.state.config.STT_ENGINE = AUDIO_STT_ENGINE
app.state.config.STT_MODEL = AUDIO_STT_MODEL
app.state.config.TTS_OPENAI_API_BASE_URL = AUDIO_TTS_OPENAI_API_BASE_URL
app.state.config.TTS_OPENAI_API_KEY = AUDIO_TTS_OPENAI_API_KEY
app.state.config.TTS_ENGINE = AUDIO_TTS_ENGINE
app.state.config.TTS_MODEL = AUDIO_TTS_MODEL
app.state.config.TTS_VOICE = AUDIO_TTS_VOICE
# setting device type for whisper model
whisper_device_type = DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == "cuda" else "cpu"
@@ -74,41 +84,101 @@ SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/")
SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
class OpenAIConfigUpdateForm(BaseModel):
url: str
key: str
model: str
speaker: str
class TTSConfigForm(BaseModel):
OPENAI_API_BASE_URL: str
OPENAI_API_KEY: str
ENGINE: str
MODEL: str
VOICE: str
class STTConfigForm(BaseModel):
OPENAI_API_BASE_URL: str
OPENAI_API_KEY: str
ENGINE: str
MODEL: str
class AudioConfigUpdateForm(BaseModel):
tts: TTSConfigForm
stt: STTConfigForm
from pydub import AudioSegment
from pydub.utils import mediainfo
def is_mp4_audio(file_path):
"""Check if the given file is an MP4 audio file."""
if not os.path.isfile(file_path):
print(f"File not found: {file_path}")
return False
info = mediainfo(file_path)
if (
info.get("codec_name") == "aac"
and info.get("codec_type") == "audio"
and info.get("codec_tag_string") == "mp4a"
):
return True
return False
def convert_mp4_to_wav(file_path, output_path):
"""Convert MP4 audio file to WAV format."""
audio = AudioSegment.from_file(file_path, format="mp4")
audio.export(output_path, format="wav")
print(f"Converted {file_path} to {output_path}")
@app.get("/config")
async def get_openai_config(user=Depends(get_admin_user)):
async def get_audio_config(user=Depends(get_admin_user)):
return {
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
"OPENAI_API_MODEL": app.state.config.OPENAI_API_MODEL,
"OPENAI_API_VOICE": app.state.config.OPENAI_API_VOICE,
"tts": {
"OPENAI_API_BASE_URL": app.state.config.TTS_OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.config.TTS_OPENAI_API_KEY,
"ENGINE": app.state.config.TTS_ENGINE,
"MODEL": app.state.config.TTS_MODEL,
"VOICE": app.state.config.TTS_VOICE,
},
"stt": {
"OPENAI_API_BASE_URL": app.state.config.STT_OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.config.STT_OPENAI_API_KEY,
"ENGINE": app.state.config.STT_ENGINE,
"MODEL": app.state.config.STT_MODEL,
},
}
@app.post("/config/update")
async def update_openai_config(
form_data: OpenAIConfigUpdateForm, user=Depends(get_admin_user)
async def update_audio_config(
form_data: AudioConfigUpdateForm, user=Depends(get_admin_user)
):
if form_data.key == "":
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
app.state.config.TTS_OPENAI_API_BASE_URL = form_data.tts.OPENAI_API_BASE_URL
app.state.config.TTS_OPENAI_API_KEY = form_data.tts.OPENAI_API_KEY
app.state.config.TTS_ENGINE = form_data.tts.ENGINE
app.state.config.TTS_MODEL = form_data.tts.MODEL
app.state.config.TTS_VOICE = form_data.tts.VOICE
app.state.config.OPENAI_API_BASE_URL = form_data.url
app.state.config.OPENAI_API_KEY = form_data.key
app.state.config.OPENAI_API_MODEL = form_data.model
app.state.config.OPENAI_API_VOICE = form_data.speaker
app.state.config.STT_OPENAI_API_BASE_URL = form_data.stt.OPENAI_API_BASE_URL
app.state.config.STT_OPENAI_API_KEY = form_data.stt.OPENAI_API_KEY
app.state.config.STT_ENGINE = form_data.stt.ENGINE
app.state.config.STT_MODEL = form_data.stt.MODEL
return {
"status": True,
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
"OPENAI_API_MODEL": app.state.config.OPENAI_API_MODEL,
"OPENAI_API_VOICE": app.state.config.OPENAI_API_VOICE,
"tts": {
"OPENAI_API_BASE_URL": app.state.config.TTS_OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.config.TTS_OPENAI_API_KEY,
"ENGINE": app.state.config.TTS_ENGINE,
"MODEL": app.state.config.TTS_MODEL,
"VOICE": app.state.config.TTS_VOICE,
},
"stt": {
"OPENAI_API_BASE_URL": app.state.config.STT_OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.config.STT_OPENAI_API_KEY,
"ENGINE": app.state.config.STT_ENGINE,
"MODEL": app.state.config.STT_MODEL,
},
}
@@ -125,13 +195,21 @@ async def speech(request: Request, user=Depends(get_verified_user)):
return FileResponse(file_path)
headers = {}
headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEY}"
headers["Authorization"] = f"Bearer {app.state.config.TTS_OPENAI_API_KEY}"
headers["Content-Type"] = "application/json"
try:
body = body.decode("utf-8")
body = json.loads(body)
body["model"] = app.state.config.TTS_MODEL
body = json.dumps(body).encode("utf-8")
except Exception as e:
pass
r = None
try:
r = requests.post(
url=f"{app.state.config.OPENAI_API_BASE_URL}/audio/speech",
url=f"{app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech",
data=body,
headers=headers,
stream=True,
@@ -181,41 +259,112 @@ def transcribe(
)
try:
filename = file.filename
file_path = f"{UPLOAD_DIR}/{filename}"
ext = file.filename.split(".")[-1]
id = uuid.uuid4()
filename = f"{id}.{ext}"
file_dir = f"{CACHE_DIR}/audio/transcriptions"
os.makedirs(file_dir, exist_ok=True)
file_path = f"{file_dir}/{filename}"
print(filename)
contents = file.file.read()
with open(file_path, "wb") as f:
f.write(contents)
f.close()
whisper_kwargs = {
"model_size_or_path": WHISPER_MODEL,
"device": whisper_device_type,
"compute_type": "int8",
"download_root": WHISPER_MODEL_DIR,
"local_files_only": not WHISPER_MODEL_AUTO_UPDATE,
}
if app.state.config.STT_ENGINE == "":
from faster_whisper import WhisperModel
log.debug(f"whisper_kwargs: {whisper_kwargs}")
whisper_kwargs = {
"model_size_or_path": WHISPER_MODEL,
"device": whisper_device_type,
"compute_type": "int8",
"download_root": WHISPER_MODEL_DIR,
"local_files_only": not WHISPER_MODEL_AUTO_UPDATE,
}
try:
model = WhisperModel(**whisper_kwargs)
except:
log.warning(
"WhisperModel initialization failed, attempting download with local_files_only=False"
log.debug(f"whisper_kwargs: {whisper_kwargs}")
try:
model = WhisperModel(**whisper_kwargs)
except:
log.warning(
"WhisperModel initialization failed, attempting download with local_files_only=False"
)
whisper_kwargs["local_files_only"] = False
model = WhisperModel(**whisper_kwargs)
segments, info = model.transcribe(file_path, beam_size=5)
log.info(
"Detected language '%s' with probability %f"
% (info.language, info.language_probability)
)
whisper_kwargs["local_files_only"] = False
model = WhisperModel(**whisper_kwargs)
segments, info = model.transcribe(file_path, beam_size=5)
log.info(
"Detected language '%s' with probability %f"
% (info.language, info.language_probability)
)
transcript = "".join([segment.text for segment in list(segments)])
transcript = "".join([segment.text for segment in list(segments)])
data = {"text": transcript.strip()}
return {"text": transcript.strip()}
# save the transcript to a json file
transcript_file = f"{file_dir}/{id}.json"
with open(transcript_file, "w") as f:
json.dump(data, f)
print(data)
return data
elif app.state.config.STT_ENGINE == "openai":
if is_mp4_audio(file_path):
print("is_mp4_audio")
os.rename(file_path, file_path.replace(".wav", ".mp4"))
# Convert MP4 audio file to WAV format
convert_mp4_to_wav(file_path.replace(".wav", ".mp4"), file_path)
headers = {"Authorization": f"Bearer {app.state.config.STT_OPENAI_API_KEY}"}
files = {"file": (filename, open(file_path, "rb"))}
data = {"model": app.state.config.STT_MODEL}
print(files, data)
r = None
try:
r = requests.post(
url=f"{app.state.config.STT_OPENAI_API_BASE_URL}/audio/transcriptions",
headers=headers,
files=files,
data=data,
)
r.raise_for_status()
data = r.json()
# save the transcript to a json file
transcript_file = f"{file_dir}/{id}.json"
with open(transcript_file, "w") as f:
json.dump(data, f)
print(data)
return data
except Exception as e:
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
res = r.json()
if "error" in res:
error_detail = f"External: {res['error']['message']}"
except:
error_detail = f"External: {e}"
raise HTTPException(
status_code=r.status_code if r != None else 500,
detail=error_detail,
)
except Exception as e:
log.exception(e)

View File

@@ -1,5 +1,6 @@
import re
import requests
import base64
from fastapi import (
FastAPI,
Request,
@@ -11,11 +12,10 @@ from fastapi import (
Form,
)
from fastapi.middleware.cors import CORSMiddleware
from faster_whisper import WhisperModel
from constants import ERROR_MESSAGES
from utils.utils import (
get_current_user,
get_verified_user,
get_admin_user,
)
@@ -36,7 +36,12 @@ from config import (
IMAGE_GENERATION_ENGINE,
ENABLE_IMAGE_GENERATION,
AUTOMATIC1111_BASE_URL,
AUTOMATIC1111_API_AUTH,
COMFYUI_BASE_URL,
COMFYUI_CFG_SCALE,
COMFYUI_SAMPLER,
COMFYUI_SCHEDULER,
COMFYUI_SD3,
IMAGES_OPENAI_API_BASE_URL,
IMAGES_OPENAI_API_KEY,
IMAGE_GENERATION_MODEL,
@@ -45,7 +50,6 @@ from config import (
AppConfig,
)
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["IMAGES"])
@@ -71,13 +75,26 @@ app.state.config.OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
app.state.config.MODEL = IMAGE_GENERATION_MODEL
app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
app.state.config.AUTOMATIC1111_API_AUTH = AUTOMATIC1111_API_AUTH
app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
app.state.config.IMAGE_SIZE = IMAGE_SIZE
app.state.config.IMAGE_STEPS = IMAGE_STEPS
app.state.config.COMFYUI_CFG_SCALE = COMFYUI_CFG_SCALE
app.state.config.COMFYUI_SAMPLER = COMFYUI_SAMPLER
app.state.config.COMFYUI_SCHEDULER = COMFYUI_SCHEDULER
app.state.config.COMFYUI_SD3 = COMFYUI_SD3
def get_automatic1111_api_auth():
if app.state.config.AUTOMATIC1111_API_AUTH == None:
return ""
else:
auth1111_byte_string = app.state.config.AUTOMATIC1111_API_AUTH.encode("utf-8")
auth1111_base64_encoded_bytes = base64.b64encode(auth1111_byte_string)
auth1111_base64_encoded_string = auth1111_base64_encoded_bytes.decode("utf-8")
return f"Basic {auth1111_base64_encoded_string}"
@app.get("/config")
@@ -105,6 +122,7 @@ async def update_config(form_data: ConfigUpdateForm, user=Depends(get_admin_user
class EngineUrlUpdateForm(BaseModel):
AUTOMATIC1111_BASE_URL: Optional[str] = None
AUTOMATIC1111_API_AUTH: Optional[str] = None
COMFYUI_BASE_URL: Optional[str] = None
@@ -112,6 +130,7 @@ class EngineUrlUpdateForm(BaseModel):
async def get_engine_url(user=Depends(get_admin_user)):
return {
"AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
"AUTOMATIC1111_API_AUTH": app.state.config.AUTOMATIC1111_API_AUTH,
"COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
}
@@ -120,7 +139,6 @@ async def get_engine_url(user=Depends(get_admin_user)):
async def update_engine_url(
form_data: EngineUrlUpdateForm, user=Depends(get_admin_user)
):
if form_data.AUTOMATIC1111_BASE_URL == None:
app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
else:
@@ -142,8 +160,14 @@ async def update_engine_url(
except Exception as e:
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
if form_data.AUTOMATIC1111_API_AUTH == None:
app.state.config.AUTOMATIC1111_API_AUTH = AUTOMATIC1111_API_AUTH
else:
app.state.config.AUTOMATIC1111_API_AUTH = form_data.AUTOMATIC1111_API_AUTH
return {
"AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
"AUTOMATIC1111_API_AUTH": app.state.config.AUTOMATIC1111_API_AUTH,
"COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
"status": True,
}
@@ -233,7 +257,7 @@ async def update_image_size(
@app.get("/models")
def get_models(user=Depends(get_current_user)):
def get_models(user=Depends(get_verified_user)):
try:
if app.state.config.ENGINE == "openai":
return [
@@ -254,7 +278,8 @@ def get_models(user=Depends(get_current_user)):
else:
r = requests.get(
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models"
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models",
headers={"authorization": get_automatic1111_api_auth()},
)
models = r.json()
return list(
@@ -281,7 +306,8 @@ async def get_default_model(user=Depends(get_admin_user)):
return {"model": (app.state.config.MODEL if app.state.config.MODEL else "")}
else:
r = requests.get(
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options"
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
headers={"authorization": get_automatic1111_api_auth()},
)
options = r.json()
return {"model": options["sd_model_checkpoint"]}
@@ -299,8 +325,10 @@ def set_model_handler(model: str):
app.state.config.MODEL = model
return app.state.config.MODEL
else:
api_auth = get_automatic1111_api_auth()
r = requests.get(
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options"
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
headers={"authorization": api_auth},
)
options = r.json()
@@ -309,6 +337,7 @@ def set_model_handler(model: str):
r = requests.post(
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
json=options,
headers={"authorization": api_auth},
)
return options
@@ -317,7 +346,7 @@ def set_model_handler(model: str):
@app.post("/models/default/update")
def update_default_model(
form_data: UpdateModelForm,
user=Depends(get_current_user),
user=Depends(get_verified_user),
):
return set_model_handler(form_data.model)
@@ -394,9 +423,8 @@ def save_url_image(url):
@app.post("/generations")
def generate_image(
form_data: GenerateImageForm,
user=Depends(get_current_user),
user=Depends(get_verified_user),
):
width, height = tuple(map(int, app.state.config.IMAGE_SIZE.split("x")))
r = None
@@ -457,6 +485,18 @@ def generate_image(
if form_data.negative_prompt is not None:
data["negative_prompt"] = form_data.negative_prompt
if app.state.config.COMFYUI_CFG_SCALE:
data["cfg_scale"] = app.state.config.COMFYUI_CFG_SCALE
if app.state.config.COMFYUI_SAMPLER is not None:
data["sampler"] = app.state.config.COMFYUI_SAMPLER
if app.state.config.COMFYUI_SCHEDULER is not None:
data["scheduler"] = app.state.config.COMFYUI_SCHEDULER
if app.state.config.COMFYUI_SD3 is not None:
data["sd3"] = app.state.config.COMFYUI_SD3
data = ImageGenerationPayload(**data)
res = comfyui_generate_image(
@@ -499,6 +539,7 @@ def generate_image(
r = requests.post(
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/txt2img",
json=data,
headers={"authorization": get_automatic1111_api_auth()},
)
res = r.json()

View File

@@ -190,6 +190,10 @@ class ImageGenerationPayload(BaseModel):
width: int
height: int
n: int = 1
cfg_scale: Optional[float] = None
sampler: Optional[str] = None
scheduler: Optional[str] = None
sd3: Optional[bool] = None
def comfyui_generate_image(
@@ -199,6 +203,18 @@ def comfyui_generate_image(
comfyui_prompt = json.loads(COMFYUI_DEFAULT_PROMPT)
if payload.cfg_scale:
comfyui_prompt["3"]["inputs"]["cfg"] = payload.cfg_scale
if payload.sampler:
comfyui_prompt["3"]["inputs"]["sampler"] = payload.sampler
if payload.scheduler:
comfyui_prompt["3"]["inputs"]["scheduler"] = payload.scheduler
if payload.sd3:
comfyui_prompt["5"]["class_type"] = "EmptySD3LatentImage"
comfyui_prompt["4"]["inputs"]["ckpt_name"] = model
comfyui_prompt["5"]["inputs"]["batch_size"] = payload.n
comfyui_prompt["5"]["inputs"]["width"] = payload.width

View File

@@ -1,379 +0,0 @@
import sys
from contextlib import asynccontextmanager
from fastapi import FastAPI, Depends, HTTPException
from fastapi.routing import APIRoute
from fastapi.middleware.cors import CORSMiddleware
import logging
from fastapi import FastAPI, Request, Depends, status, Response
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.responses import StreamingResponse
import json
import time
import requests
from pydantic import BaseModel, ConfigDict
from typing import Optional, List
from utils.utils import get_verified_user, get_current_user, get_admin_user
from config import SRC_LOG_LEVELS, ENV
from constants import MESSAGES
import os
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["LITELLM"])
from config import (
ENABLE_LITELLM,
ENABLE_MODEL_FILTER,
MODEL_FILTER_LIST,
DATA_DIR,
LITELLM_PROXY_PORT,
LITELLM_PROXY_HOST,
)
import warnings
warnings.simplefilter("ignore")
from litellm.utils import get_llm_provider
import asyncio
import subprocess
import yaml
@asynccontextmanager
async def lifespan(app: FastAPI):
log.info("startup_event")
# TODO: Check config.yaml file and create one
asyncio.create_task(start_litellm_background())
yield
app = FastAPI(lifespan=lifespan)
origins = ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
LITELLM_CONFIG_DIR = f"{DATA_DIR}/litellm/config.yaml"
with open(LITELLM_CONFIG_DIR, "r") as file:
litellm_config = yaml.safe_load(file)
app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER.value
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST.value
app.state.ENABLE = ENABLE_LITELLM
app.state.CONFIG = litellm_config
# Global variable to store the subprocess reference
background_process = None
CONFLICT_ENV_VARS = [
# Uvicorn uses PORT, so LiteLLM might use it as well
"PORT",
# LiteLLM uses DATABASE_URL for Prisma connections
"DATABASE_URL",
]
async def run_background_process(command):
global background_process
log.info("run_background_process")
try:
# Log the command to be executed
log.info(f"Executing command: {command}")
# Filter environment variables known to conflict with litellm
env = {k: v for k, v in os.environ.items() if k not in CONFLICT_ENV_VARS}
# Execute the command and create a subprocess
process = await asyncio.create_subprocess_exec(
*command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env
)
background_process = process
log.info("Subprocess started successfully.")
# Capture STDERR for debugging purposes
stderr_output = await process.stderr.read()
stderr_text = stderr_output.decode().strip()
if stderr_text:
log.info(f"Subprocess STDERR: {stderr_text}")
# log.info output line by line
async for line in process.stdout:
log.info(line.decode().strip())
# Wait for the process to finish
returncode = await process.wait()
log.info(f"Subprocess exited with return code {returncode}")
except Exception as e:
log.error(f"Failed to start subprocess: {e}")
raise # Optionally re-raise the exception if you want it to propagate
async def start_litellm_background():
log.info("start_litellm_background")
# Command to run in the background
command = [
"litellm",
"--port",
str(LITELLM_PROXY_PORT),
"--host",
LITELLM_PROXY_HOST,
"--telemetry",
"False",
"--config",
LITELLM_CONFIG_DIR,
]
await run_background_process(command)
async def shutdown_litellm_background():
log.info("shutdown_litellm_background")
global background_process
if background_process:
background_process.terminate()
await background_process.wait() # Ensure the process has terminated
log.info("Subprocess terminated")
background_process = None
@app.get("/")
async def get_status():
return {"status": True}
async def restart_litellm():
"""
Endpoint to restart the litellm background service.
"""
log.info("Requested restart of litellm service.")
try:
# Shut down the existing process if it is running
await shutdown_litellm_background()
log.info("litellm service shutdown complete.")
# Restart the background service
asyncio.create_task(start_litellm_background())
log.info("litellm service restart complete.")
return {
"status": "success",
"message": "litellm service restarted successfully.",
}
except Exception as e:
log.info(f"Error restarting litellm service: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
)
@app.get("/restart")
async def restart_litellm_handler(user=Depends(get_admin_user)):
return await restart_litellm()
@app.get("/config")
async def get_config(user=Depends(get_admin_user)):
return app.state.CONFIG
class LiteLLMConfigForm(BaseModel):
general_settings: Optional[dict] = None
litellm_settings: Optional[dict] = None
model_list: Optional[List[dict]] = None
router_settings: Optional[dict] = None
model_config = ConfigDict(protected_namespaces=())
@app.post("/config/update")
async def update_config(form_data: LiteLLMConfigForm, user=Depends(get_admin_user)):
app.state.CONFIG = form_data.model_dump(exclude_none=True)
with open(LITELLM_CONFIG_DIR, "w") as file:
yaml.dump(app.state.CONFIG, file)
await restart_litellm()
return app.state.CONFIG
@app.get("/models")
@app.get("/v1/models")
async def get_models(user=Depends(get_current_user)):
if app.state.ENABLE:
while not background_process:
await asyncio.sleep(0.1)
url = f"http://localhost:{LITELLM_PROXY_PORT}/v1"
r = None
try:
r = requests.request(method="GET", url=f"{url}/models")
r.raise_for_status()
data = r.json()
if app.state.ENABLE_MODEL_FILTER:
if user and user.role == "user":
data["data"] = list(
filter(
lambda model: model["id"] in app.state.MODEL_FILTER_LIST,
data["data"],
)
)
return data
except Exception as e:
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
res = r.json()
if "error" in res:
error_detail = f"External: {res['error']}"
except:
error_detail = f"External: {e}"
return {
"data": [
{
"id": model["model_name"],
"object": "model",
"created": int(time.time()),
"owned_by": "openai",
}
for model in app.state.CONFIG["model_list"]
],
"object": "list",
}
else:
return {
"data": [],
"object": "list",
}
@app.get("/model/info")
async def get_model_list(user=Depends(get_admin_user)):
return {"data": app.state.CONFIG["model_list"]}
class AddLiteLLMModelForm(BaseModel):
model_name: str
litellm_params: dict
model_config = ConfigDict(protected_namespaces=())
@app.post("/model/new")
async def add_model_to_config(
form_data: AddLiteLLMModelForm, user=Depends(get_admin_user)
):
try:
get_llm_provider(model=form_data.model_name)
app.state.CONFIG["model_list"].append(form_data.model_dump())
with open(LITELLM_CONFIG_DIR, "w") as file:
yaml.dump(app.state.CONFIG, file)
await restart_litellm()
return {"message": MESSAGES.MODEL_ADDED(form_data.model_name)}
except Exception as e:
print(e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
)
class DeleteLiteLLMModelForm(BaseModel):
id: str
@app.post("/model/delete")
async def delete_model_from_config(
form_data: DeleteLiteLLMModelForm, user=Depends(get_admin_user)
):
app.state.CONFIG["model_list"] = [
model
for model in app.state.CONFIG["model_list"]
if model["model_name"] != form_data.id
]
with open(LITELLM_CONFIG_DIR, "w") as file:
yaml.dump(app.state.CONFIG, file)
await restart_litellm()
return {"message": MESSAGES.MODEL_DELETED(form_data.id)}
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
body = await request.body()
url = f"http://localhost:{LITELLM_PROXY_PORT}"
target_url = f"{url}/{path}"
headers = {}
# headers["Authorization"] = f"Bearer {key}"
headers["Content-Type"] = "application/json"
r = None
try:
r = requests.request(
method=request.method,
url=target_url,
data=body,
headers=headers,
stream=True,
)
r.raise_for_status()
# Check if response is SSE
if "text/event-stream" in r.headers.get("Content-Type", ""):
return StreamingResponse(
r.iter_content(chunk_size=8192),
status_code=r.status_code,
headers=dict(r.headers),
)
else:
response_data = r.json()
return response_data
except Exception as e:
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
res = r.json()
if "error" in res:
error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
except:
error_detail = f"External: {e}"
raise HTTPException(
status_code=r.status_code if r else 500, detail=error_detail
)

File diff suppressed because it is too large Load Diff

View File

@@ -9,19 +9,23 @@ import json
import logging
from pydantic import BaseModel
from starlette.background import BackgroundTask
from apps.web.models.users import Users
from apps.webui.models.models import Models
from apps.webui.models.users import Users
from constants import ERROR_MESSAGES
from utils.utils import (
decode_token,
get_current_user,
get_verified_user,
get_verified_user,
get_admin_user,
)
from utils.task import prompt_template
from config import (
SRC_LOG_LEVELS,
ENABLE_OPENAI_API,
AIOHTTP_CLIENT_TIMEOUT,
OPENAI_API_BASE_URLS,
OPENAI_API_KEYS,
CACHE_DIR,
@@ -53,7 +57,6 @@ app.state.config = AppConfig()
app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API
app.state.config.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
app.state.config.OPENAI_API_KEYS = OPENAI_API_KEYS
@@ -185,28 +188,41 @@ async def speech(request: Request, user=Depends(get_verified_user)):
async def fetch_url(url, key):
timeout = aiohttp.ClientTimeout(total=5)
try:
if key != "":
headers = {"Authorization": f"Bearer {key}"}
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(url, headers=headers) as response:
return await response.json()
else:
return None
headers = {"Authorization": f"Bearer {key}"}
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
async with session.get(url, headers=headers) as response:
return await response.json()
except Exception as e:
# Handle connection error here
log.error(f"Connection error: {e}")
return None
async def cleanup_response(
response: Optional[aiohttp.ClientResponse],
session: Optional[aiohttp.ClientSession],
):
if response:
response.close()
if session:
await session.close()
def merge_models_lists(model_lists):
log.info(f"merge_models_lists {model_lists}")
log.debug(f"merge_models_lists {model_lists}")
merged_list = []
for idx, models in enumerate(model_lists):
if models is not None and "error" not in models:
merged_list.extend(
[
{**model, "urlIdx": idx}
{
**model,
"name": model.get("name", model["id"]),
"owned_by": "openai",
"openai": model,
"urlIdx": idx,
}
for model in models
if "api.openai.com"
not in app.state.config.OPENAI_API_BASE_URLS[idx]
@@ -217,7 +233,7 @@ def merge_models_lists(model_lists):
return merged_list
async def get_all_models():
async def get_all_models(raw: bool = False):
log.info("get_all_models()")
if (
@@ -226,13 +242,37 @@ async def get_all_models():
) or not app.state.config.ENABLE_OPENAI_API:
models = {"data": []}
else:
# Check if API KEYS length is same than API URLS length
if len(app.state.config.OPENAI_API_KEYS) != len(
app.state.config.OPENAI_API_BASE_URLS
):
# if there are more keys than urls, remove the extra keys
if len(app.state.config.OPENAI_API_KEYS) > len(
app.state.config.OPENAI_API_BASE_URLS
):
app.state.config.OPENAI_API_KEYS = app.state.config.OPENAI_API_KEYS[
: len(app.state.config.OPENAI_API_BASE_URLS)
]
# if there are more urls than keys, add empty keys
else:
app.state.config.OPENAI_API_KEYS += [
""
for _ in range(
len(app.state.config.OPENAI_API_BASE_URLS)
- len(app.state.config.OPENAI_API_KEYS)
)
]
tasks = [
fetch_url(f"{url}/models", app.state.config.OPENAI_API_KEYS[idx])
for idx, url in enumerate(app.state.config.OPENAI_API_BASE_URLS)
]
responses = await asyncio.gather(*tasks)
log.info(f"get_all_models:responses() {responses}")
log.debug(f"get_all_models:responses() {responses}")
if raw:
return responses
models = {
"data": merge_models_lists(
@@ -249,15 +289,15 @@ async def get_all_models():
)
}
log.info(f"models: {models}")
log.debug(f"models: {models}")
app.state.MODELS = {model["id"]: model for model in models["data"]}
return models
return models
@app.get("/models")
@app.get("/models/{url_idx}")
async def get_models(url_idx: Optional[int] = None, user=Depends(get_current_user)):
async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_user)):
if url_idx == None:
models = await get_all_models()
if app.state.config.ENABLE_MODEL_FILTER:
@@ -272,11 +312,16 @@ async def get_models(url_idx: Optional[int] = None, user=Depends(get_current_use
return models
else:
url = app.state.config.OPENAI_API_BASE_URLS[url_idx]
key = app.state.config.OPENAI_API_KEYS[url_idx]
headers = {}
headers["Authorization"] = f"Bearer {key}"
headers["Content-Type"] = "application/json"
r = None
try:
r = requests.request(method="GET", url=f"{url}/models")
r = requests.request(method="GET", url=f"{url}/models", headers=headers)
r.raise_for_status()
response_data = r.json()
@@ -303,84 +348,225 @@ async def get_models(url_idx: Optional[int] = None, user=Depends(get_current_use
)
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
@app.post("/chat/completions")
@app.post("/chat/completions/{url_idx}")
async def generate_chat_completion(
form_data: dict,
url_idx: Optional[int] = None,
user=Depends(get_verified_user),
):
idx = 0
payload = {**form_data}
body = await request.body()
# TODO: Remove below after gpt-4-vision fix from Open AI
# Try to decode the body of the request from bytes to a UTF-8 string (Require add max_token to fix gpt-4-vision)
try:
body = body.decode("utf-8")
body = json.loads(body)
model_id = form_data.get("model")
model_info = Models.get_model_by_id(model_id)
idx = app.state.MODELS[body.get("model")]["urlIdx"]
if model_info:
if model_info.base_model_id:
payload["model"] = model_info.base_model_id
# Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000
# This is a workaround until OpenAI fixes the issue with this model
if body.get("model") == "gpt-4-vision-preview":
if "max_tokens" not in body:
body["max_tokens"] = 4000
log.debug("Modified body_dict:", body)
model_info.params = model_info.params.model_dump()
# Fix for ChatGPT calls failing because the num_ctx key is in body
if "num_ctx" in body:
# If 'num_ctx' is in the dictionary, delete it
# Leaving it there generates an error with the
# OpenAI API (Feb 2024)
del body["num_ctx"]
if model_info.params:
if model_info.params.get("temperature", None) is not None:
payload["temperature"] = float(model_info.params.get("temperature"))
# Convert the modified body back to JSON
body = json.dumps(body)
except json.JSONDecodeError as e:
log.error("Error loading request body into a dictionary:", e)
if model_info.params.get("top_p", None):
payload["top_p"] = int(model_info.params.get("top_p", None))
if model_info.params.get("max_tokens", None):
payload["max_tokens"] = int(model_info.params.get("max_tokens", None))
if model_info.params.get("frequency_penalty", None):
payload["frequency_penalty"] = int(
model_info.params.get("frequency_penalty", None)
)
if model_info.params.get("seed", None):
payload["seed"] = model_info.params.get("seed", None)
if model_info.params.get("stop", None):
payload["stop"] = (
[
bytes(stop, "utf-8").decode("unicode_escape")
for stop in model_info.params["stop"]
]
if model_info.params.get("stop", None)
else None
)
system = model_info.params.get("system", None)
if system:
system = prompt_template(
system,
**(
{
"user_name": user.name,
"user_location": (
user.info.get("location") if user.info else None
),
}
if user
else {}
),
)
# Check if the payload already has a system message
# If not, add a system message to the payload
if payload.get("messages"):
for message in payload["messages"]:
if message.get("role") == "system":
message["content"] = system + message["content"]
break
else:
payload["messages"].insert(
0,
{
"role": "system",
"content": system,
},
)
else:
pass
model = app.state.MODELS[payload.get("model")]
idx = model["urlIdx"]
if "pipeline" in model and model.get("pipeline"):
payload["user"] = {
"name": user.name,
"id": user.id,
"email": user.email,
"role": user.role,
}
# Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000
# This is a workaround until OpenAI fixes the issue with this model
if payload.get("model") == "gpt-4-vision-preview":
if "max_tokens" not in payload:
payload["max_tokens"] = 4000
log.debug("Modified payload:", payload)
# Convert the modified body back to JSON
payload = json.dumps(payload)
log.debug(payload)
url = app.state.config.OPENAI_API_BASE_URLS[idx]
key = app.state.config.OPENAI_API_KEYS[idx]
target_url = f"{url}/{path}"
if key == "":
raise HTTPException(status_code=401, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
headers = {}
headers["Authorization"] = f"Bearer {key}"
headers["Content-Type"] = "application/json"
r = None
session = None
streaming = False
try:
r = requests.request(
method=request.method,
url=target_url,
data=body,
session = aiohttp.ClientSession(
trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
)
r = await session.request(
method="POST",
url=f"{url}/chat/completions",
data=payload,
headers=headers,
stream=True,
)
r.raise_for_status()
# Check if response is SSE
if "text/event-stream" in r.headers.get("Content-Type", ""):
streaming = True
return StreamingResponse(
r.iter_content(chunk_size=8192),
status_code=r.status_code,
r.content,
status_code=r.status,
headers=dict(r.headers),
background=BackgroundTask(
cleanup_response, response=r, session=session
),
)
else:
response_data = r.json()
response_data = await r.json()
return response_data
except Exception as e:
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
res = r.json()
res = await r.json()
print(res)
if "error" in res:
error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
except:
error_detail = f"External: {e}"
raise HTTPException(status_code=r.status if r else 500, detail=error_detail)
finally:
if not streaming and session:
if r:
r.close()
await session.close()
raise HTTPException(
status_code=r.status_code if r else 500, detail=error_detail
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
idx = 0
body = await request.body()
url = app.state.config.OPENAI_API_BASE_URLS[idx]
key = app.state.config.OPENAI_API_KEYS[idx]
target_url = f"{url}/{path}"
headers = {}
headers["Authorization"] = f"Bearer {key}"
headers["Content-Type"] = "application/json"
r = None
session = None
streaming = False
try:
session = aiohttp.ClientSession(trust_env=True)
r = await session.request(
method=request.method,
url=target_url,
data=body,
headers=headers,
)
r.raise_for_status()
# Check if response is SSE
if "text/event-stream" in r.headers.get("Content-Type", ""):
streaming = True
return StreamingResponse(
r.content,
status_code=r.status,
headers=dict(r.headers),
background=BackgroundTask(
cleanup_response, response=r, session=session
),
)
else:
response_data = await r.json()
return response_data
except Exception as e:
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
res = await r.json()
print(res)
if "error" in res:
error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
except:
error_detail = f"External: {e}"
raise HTTPException(status_code=r.status if r else 500, detail=error_detail)
finally:
if not streaming and session:
if r:
r.close()
await session.close()

View File

@@ -8,12 +8,15 @@ from fastapi import (
Form,
)
from fastapi.middleware.cors import CORSMiddleware
import requests
import os, shutil, logging, re
from datetime import datetime
from pathlib import Path
from typing import List
from typing import List, Union, Sequence, Iterator, Any
from chromadb.utils.batch_utils import create_batches
from langchain_core.documents import Document
from langchain_community.document_loaders import (
WebBaseLoader,
@@ -28,7 +31,9 @@ from langchain_community.document_loaders import (
UnstructuredXMLLoader,
UnstructuredRSTLoader,
UnstructuredExcelLoader,
UnstructuredPowerPointLoader,
YoutubeLoader,
OutlookMessageLoader,
)
from langchain.text_splitter import RecursiveCharacterTextSplitter
@@ -43,13 +48,14 @@ import mimetypes
import uuid
import json
import sentence_transformers
from apps.web.models.documents import (
from apps.webui.models.documents import (
Documents,
DocumentForm,
DocumentResponse,
)
from apps.webui.models.files import (
Files,
)
from apps.rag.utils import (
get_model_path,
@@ -60,19 +66,33 @@ from apps.rag.utils import (
query_collection_with_hybrid_search,
)
from apps.rag.search.brave import search_brave
from apps.rag.search.google_pse import search_google_pse
from apps.rag.search.main import SearchResult
from apps.rag.search.searxng import search_searxng
from apps.rag.search.serper import search_serper
from apps.rag.search.serpstack import search_serpstack
from apps.rag.search.serply import search_serply
from apps.rag.search.duckduckgo import search_duckduckgo
from apps.rag.search.tavily import search_tavily
from apps.rag.search.jina_search import search_jina
from utils.misc import (
calculate_sha256,
calculate_sha256_string,
sanitize_filename,
extract_folders_after_data_docs,
)
from utils.utils import get_current_user, get_admin_user
from utils.utils import get_verified_user, get_admin_user
from config import (
AppConfig,
ENV,
SRC_LOG_LEVELS,
UPLOAD_DIR,
DOCS_DIR,
CONTENT_EXTRACTION_ENGINE,
TIKA_SERVER_URL,
RAG_TOP_K,
RAG_RELEVANCE_THRESHOLD,
RAG_EMBEDDING_ENGINE,
@@ -94,7 +114,21 @@ from config import (
RAG_TEMPLATE,
ENABLE_RAG_LOCAL_WEB_FETCH,
YOUTUBE_LOADER_LANGUAGE,
AppConfig,
ENABLE_RAG_WEB_SEARCH,
RAG_WEB_SEARCH_ENGINE,
RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
SEARXNG_QUERY_URL,
GOOGLE_PSE_API_KEY,
GOOGLE_PSE_ENGINE_ID,
BRAVE_SEARCH_API_KEY,
SERPSTACK_API_KEY,
SERPSTACK_HTTPS,
SERPER_API_KEY,
SERPLY_API_KEY,
TAVILY_API_KEY,
RAG_WEB_SEARCH_RESULT_COUNT,
RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
RAG_EMBEDDING_OPENAI_BATCH_SIZE,
)
from constants import ERROR_MESSAGES
@@ -114,11 +148,15 @@ app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
)
app.state.config.CONTENT_EXTRACTION_ENGINE = CONTENT_EXTRACTION_ENGINE
app.state.config.TIKA_SERVER_URL = TIKA_SERVER_URL
app.state.config.CHUNK_SIZE = CHUNK_SIZE
app.state.config.CHUNK_OVERLAP = CHUNK_OVERLAP
app.state.config.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE
app.state.config.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE = RAG_EMBEDDING_OPENAI_BATCH_SIZE
app.state.config.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL
app.state.config.RAG_TEMPLATE = RAG_TEMPLATE
@@ -133,11 +171,30 @@ app.state.config.YOUTUBE_LOADER_LANGUAGE = YOUTUBE_LOADER_LANGUAGE
app.state.YOUTUBE_LOADER_TRANSLATION = None
app.state.config.ENABLE_RAG_WEB_SEARCH = ENABLE_RAG_WEB_SEARCH
app.state.config.RAG_WEB_SEARCH_ENGINE = RAG_WEB_SEARCH_ENGINE
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = RAG_WEB_SEARCH_DOMAIN_FILTER_LIST
app.state.config.SEARXNG_QUERY_URL = SEARXNG_QUERY_URL
app.state.config.GOOGLE_PSE_API_KEY = GOOGLE_PSE_API_KEY
app.state.config.GOOGLE_PSE_ENGINE_ID = GOOGLE_PSE_ENGINE_ID
app.state.config.BRAVE_SEARCH_API_KEY = BRAVE_SEARCH_API_KEY
app.state.config.SERPSTACK_API_KEY = SERPSTACK_API_KEY
app.state.config.SERPSTACK_HTTPS = SERPSTACK_HTTPS
app.state.config.SERPER_API_KEY = SERPER_API_KEY
app.state.config.SERPLY_API_KEY = SERPLY_API_KEY
app.state.config.TAVILY_API_KEY = TAVILY_API_KEY
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = RAG_WEB_SEARCH_RESULT_COUNT
app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = RAG_WEB_SEARCH_CONCURRENT_REQUESTS
def update_embedding_model(
embedding_model: str,
update_model: bool = False,
):
if embedding_model and app.state.config.RAG_EMBEDDING_ENGINE == "":
import sentence_transformers
app.state.sentence_transformer_ef = sentence_transformers.SentenceTransformer(
get_model_path(embedding_model, update_model),
device=DEVICE_TYPE,
@@ -152,6 +209,8 @@ def update_reranking_model(
update_model: bool = False,
):
if reranking_model:
import sentence_transformers
app.state.sentence_transformer_rf = sentence_transformers.CrossEncoder(
get_model_path(reranking_model, update_model),
device=DEVICE_TYPE,
@@ -178,6 +237,7 @@ app.state.EMBEDDING_FUNCTION = get_embedding_function(
app.state.sentence_transformer_ef,
app.state.config.OPENAI_API_KEY,
app.state.config.OPENAI_API_BASE_URL,
app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
)
origins = ["*"]
@@ -200,6 +260,10 @@ class UrlForm(CollectionNameForm):
url: str
class SearchForm(CollectionNameForm):
query: str
@app.get("/")
async def get_status():
return {
@@ -210,6 +274,7 @@ async def get_status():
"embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
"embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
"reranking_model": app.state.config.RAG_RERANKING_MODEL,
"openai_batch_size": app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
}
@@ -222,6 +287,7 @@ async def get_embedding_config(user=Depends(get_admin_user)):
"openai_config": {
"url": app.state.config.OPENAI_API_BASE_URL,
"key": app.state.config.OPENAI_API_KEY,
"batch_size": app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
},
}
@@ -237,6 +303,7 @@ async def get_reraanking_config(user=Depends(get_admin_user)):
class OpenAIConfigForm(BaseModel):
url: str
key: str
batch_size: Optional[int] = None
class EmbeddingModelUpdateForm(BaseModel):
@@ -257,9 +324,14 @@ async def update_embedding_config(
app.state.config.RAG_EMBEDDING_MODEL = form_data.embedding_model
if app.state.config.RAG_EMBEDDING_ENGINE in ["ollama", "openai"]:
if form_data.openai_config != None:
if form_data.openai_config is not None:
app.state.config.OPENAI_API_BASE_URL = form_data.openai_config.url
app.state.config.OPENAI_API_KEY = form_data.openai_config.key
app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE = (
form_data.openai_config.batch_size
if form_data.openai_config.batch_size
else 1
)
update_embedding_model(app.state.config.RAG_EMBEDDING_MODEL)
@@ -269,6 +341,7 @@ async def update_embedding_config(
app.state.sentence_transformer_ef,
app.state.config.OPENAI_API_KEY,
app.state.config.OPENAI_API_BASE_URL,
app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
)
return {
@@ -278,6 +351,7 @@ async def update_embedding_config(
"openai_config": {
"url": app.state.config.OPENAI_API_BASE_URL,
"key": app.state.config.OPENAI_API_KEY,
"batch_size": app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
},
}
except Exception as e:
@@ -321,18 +395,44 @@ async def get_rag_config(user=Depends(get_admin_user)):
return {
"status": True,
"pdf_extract_images": app.state.config.PDF_EXTRACT_IMAGES,
"content_extraction": {
"engine": app.state.config.CONTENT_EXTRACTION_ENGINE,
"tika_server_url": app.state.config.TIKA_SERVER_URL,
},
"chunk": {
"chunk_size": app.state.config.CHUNK_SIZE,
"chunk_overlap": app.state.config.CHUNK_OVERLAP,
},
"web_loader_ssl_verification": app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
"youtube": {
"language": app.state.config.YOUTUBE_LOADER_LANGUAGE,
"translation": app.state.YOUTUBE_LOADER_TRANSLATION,
},
"web": {
"ssl_verification": app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
"search": {
"enabled": app.state.config.ENABLE_RAG_WEB_SEARCH,
"engine": app.state.config.RAG_WEB_SEARCH_ENGINE,
"searxng_query_url": app.state.config.SEARXNG_QUERY_URL,
"google_pse_api_key": app.state.config.GOOGLE_PSE_API_KEY,
"google_pse_engine_id": app.state.config.GOOGLE_PSE_ENGINE_ID,
"brave_search_api_key": app.state.config.BRAVE_SEARCH_API_KEY,
"serpstack_api_key": app.state.config.SERPSTACK_API_KEY,
"serpstack_https": app.state.config.SERPSTACK_HTTPS,
"serper_api_key": app.state.config.SERPER_API_KEY,
"serply_api_key": app.state.config.SERPLY_API_KEY,
"tavily_api_key": app.state.config.TAVILY_API_KEY,
"result_count": app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
"concurrent_requests": app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
},
},
}
class ContentExtractionConfig(BaseModel):
engine: str = ""
tika_server_url: Optional[str] = None
class ChunkParamUpdateForm(BaseModel):
chunk_size: int
chunk_overlap: int
@@ -343,11 +443,33 @@ class YoutubeLoaderConfig(BaseModel):
translation: Optional[str] = None
class WebSearchConfig(BaseModel):
enabled: bool
engine: Optional[str] = None
searxng_query_url: Optional[str] = None
google_pse_api_key: Optional[str] = None
google_pse_engine_id: Optional[str] = None
brave_search_api_key: Optional[str] = None
serpstack_api_key: Optional[str] = None
serpstack_https: Optional[bool] = None
serper_api_key: Optional[str] = None
serply_api_key: Optional[str] = None
tavily_api_key: Optional[str] = None
result_count: Optional[int] = None
concurrent_requests: Optional[int] = None
class WebConfig(BaseModel):
search: WebSearchConfig
web_loader_ssl_verification: Optional[bool] = None
class ConfigUpdateForm(BaseModel):
pdf_extract_images: Optional[bool] = None
content_extraction: Optional[ContentExtractionConfig] = None
chunk: Optional[ChunkParamUpdateForm] = None
web_loader_ssl_verification: Optional[bool] = None
youtube: Optional[YoutubeLoaderConfig] = None
web: Optional[WebConfig] = None
@app.post("/config/update")
@@ -358,53 +480,82 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_
else app.state.config.PDF_EXTRACT_IMAGES
)
app.state.config.CHUNK_SIZE = (
form_data.chunk.chunk_size
if form_data.chunk is not None
else app.state.config.CHUNK_SIZE
)
if form_data.content_extraction is not None:
log.info(f"Updating text settings: {form_data.content_extraction}")
app.state.config.CONTENT_EXTRACTION_ENGINE = form_data.content_extraction.engine
app.state.config.TIKA_SERVER_URL = form_data.content_extraction.tika_server_url
app.state.config.CHUNK_OVERLAP = (
form_data.chunk.chunk_overlap
if form_data.chunk is not None
else app.state.config.CHUNK_OVERLAP
)
if form_data.chunk is not None:
app.state.config.CHUNK_SIZE = form_data.chunk.chunk_size
app.state.config.CHUNK_OVERLAP = form_data.chunk.chunk_overlap
app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
form_data.web_loader_ssl_verification
if form_data.web_loader_ssl_verification != None
else app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
)
if form_data.youtube is not None:
app.state.config.YOUTUBE_LOADER_LANGUAGE = form_data.youtube.language
app.state.YOUTUBE_LOADER_TRANSLATION = form_data.youtube.translation
app.state.config.YOUTUBE_LOADER_LANGUAGE = (
form_data.youtube.language
if form_data.youtube is not None
else app.state.config.YOUTUBE_LOADER_LANGUAGE
)
if form_data.web is not None:
app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
form_data.web.web_loader_ssl_verification
)
app.state.YOUTUBE_LOADER_TRANSLATION = (
form_data.youtube.translation
if form_data.youtube is not None
else app.state.YOUTUBE_LOADER_TRANSLATION
)
app.state.config.ENABLE_RAG_WEB_SEARCH = form_data.web.search.enabled
app.state.config.RAG_WEB_SEARCH_ENGINE = form_data.web.search.engine
app.state.config.SEARXNG_QUERY_URL = form_data.web.search.searxng_query_url
app.state.config.GOOGLE_PSE_API_KEY = form_data.web.search.google_pse_api_key
app.state.config.GOOGLE_PSE_ENGINE_ID = (
form_data.web.search.google_pse_engine_id
)
app.state.config.BRAVE_SEARCH_API_KEY = (
form_data.web.search.brave_search_api_key
)
app.state.config.SERPSTACK_API_KEY = form_data.web.search.serpstack_api_key
app.state.config.SERPSTACK_HTTPS = form_data.web.search.serpstack_https
app.state.config.SERPER_API_KEY = form_data.web.search.serper_api_key
app.state.config.SERPLY_API_KEY = form_data.web.search.serply_api_key
app.state.config.TAVILY_API_KEY = form_data.web.search.tavily_api_key
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = form_data.web.search.result_count
app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = (
form_data.web.search.concurrent_requests
)
return {
"status": True,
"pdf_extract_images": app.state.config.PDF_EXTRACT_IMAGES,
"content_extraction": {
"engine": app.state.config.CONTENT_EXTRACTION_ENGINE,
"tika_server_url": app.state.config.TIKA_SERVER_URL,
},
"chunk": {
"chunk_size": app.state.config.CHUNK_SIZE,
"chunk_overlap": app.state.config.CHUNK_OVERLAP,
},
"web_loader_ssl_verification": app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
"youtube": {
"language": app.state.config.YOUTUBE_LOADER_LANGUAGE,
"translation": app.state.YOUTUBE_LOADER_TRANSLATION,
},
"web": {
"ssl_verification": app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
"search": {
"enabled": app.state.config.ENABLE_RAG_WEB_SEARCH,
"engine": app.state.config.RAG_WEB_SEARCH_ENGINE,
"searxng_query_url": app.state.config.SEARXNG_QUERY_URL,
"google_pse_api_key": app.state.config.GOOGLE_PSE_API_KEY,
"google_pse_engine_id": app.state.config.GOOGLE_PSE_ENGINE_ID,
"brave_search_api_key": app.state.config.BRAVE_SEARCH_API_KEY,
"serpstack_api_key": app.state.config.SERPSTACK_API_KEY,
"serpstack_https": app.state.config.SERPSTACK_HTTPS,
"serper_api_key": app.state.config.SERPER_API_KEY,
"serply_api_key": app.state.config.SERPLY_API_KEY,
"tavily_api_key": app.state.config.TAVILY_API_KEY,
"result_count": app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
"concurrent_requests": app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
},
},
}
@app.get("/template")
async def get_rag_template(user=Depends(get_current_user)):
async def get_rag_template(user=Depends(get_verified_user)):
return {
"status": True,
"template": app.state.config.RAG_TEMPLATE,
@@ -461,7 +612,7 @@ class QueryDocForm(BaseModel):
@app.post("/query/doc")
def query_doc_handler(
form_data: QueryDocForm,
user=Depends(get_current_user),
user=Depends(get_verified_user),
):
try:
if app.state.config.ENABLE_RAG_HYBRID_SEARCH:
@@ -501,7 +652,7 @@ class QueryCollectionsForm(BaseModel):
@app.post("/query/collection")
def query_collection_handler(
form_data: QueryCollectionsForm,
user=Depends(get_current_user),
user=Depends(get_verified_user),
):
try:
if app.state.config.ENABLE_RAG_HYBRID_SEARCH:
@@ -532,7 +683,7 @@ def query_collection_handler(
@app.post("/youtube")
def store_youtube_video(form_data: UrlForm, user=Depends(get_current_user)):
def store_youtube_video(form_data: UrlForm, user=Depends(get_verified_user)):
try:
loader = YoutubeLoader.from_youtube_url(
form_data.url,
@@ -561,7 +712,7 @@ def store_youtube_video(form_data: UrlForm, user=Depends(get_current_user)):
@app.post("/web")
def store_web(form_data: UrlForm, user=Depends(get_current_user)):
def store_web(form_data: UrlForm, user=Depends(get_verified_user)):
# "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm"
try:
loader = get_web_loader(
@@ -588,24 +739,40 @@ def store_web(form_data: UrlForm, user=Depends(get_current_user)):
)
def get_web_loader(url: str, verify_ssl: bool = True):
def get_web_loader(url: Union[str, Sequence[str]], verify_ssl: bool = True):
# Check if the URL is valid
if isinstance(validators.url(url), validators.ValidationError):
if not validate_url(url):
raise ValueError(ERROR_MESSAGES.INVALID_URL)
if not ENABLE_RAG_LOCAL_WEB_FETCH:
# Local web fetch is disabled, filter out any URLs that resolve to private IP addresses
parsed_url = urllib.parse.urlparse(url)
# Get IPv4 and IPv6 addresses
ipv4_addresses, ipv6_addresses = resolve_hostname(parsed_url.hostname)
# Check if any of the resolved addresses are private
# This is technically still vulnerable to DNS rebinding attacks, as we don't control WebBaseLoader
for ip in ipv4_addresses:
if validators.ipv4(ip, private=True):
raise ValueError(ERROR_MESSAGES.INVALID_URL)
for ip in ipv6_addresses:
if validators.ipv6(ip, private=True):
raise ValueError(ERROR_MESSAGES.INVALID_URL)
return WebBaseLoader(url, verify_ssl=verify_ssl)
return SafeWebBaseLoader(
url,
verify_ssl=verify_ssl,
requests_per_second=RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
continue_on_failure=True,
)
def validate_url(url: Union[str, Sequence[str]]):
if isinstance(url, str):
if isinstance(validators.url(url), validators.ValidationError):
raise ValueError(ERROR_MESSAGES.INVALID_URL)
if not ENABLE_RAG_LOCAL_WEB_FETCH:
# Local web fetch is disabled, filter out any URLs that resolve to private IP addresses
parsed_url = urllib.parse.urlparse(url)
# Get IPv4 and IPv6 addresses
ipv4_addresses, ipv6_addresses = resolve_hostname(parsed_url.hostname)
# Check if any of the resolved addresses are private
# This is technically still vulnerable to DNS rebinding attacks, as we don't control WebBaseLoader
for ip in ipv4_addresses:
if validators.ipv4(ip, private=True):
raise ValueError(ERROR_MESSAGES.INVALID_URL)
for ip in ipv6_addresses:
if validators.ipv6(ip, private=True):
raise ValueError(ERROR_MESSAGES.INVALID_URL)
return True
elif isinstance(url, Sequence):
return all(validate_url(u) for u in url)
else:
return False
def resolve_hostname(hostname):
@@ -619,6 +786,150 @@ def resolve_hostname(hostname):
return ipv4_addresses, ipv6_addresses
def search_web(engine: str, query: str) -> list[SearchResult]:
"""Search the web using a search engine and return the results as a list of SearchResult objects.
Will look for a search engine API key in environment variables in the following order:
- SEARXNG_QUERY_URL
- GOOGLE_PSE_API_KEY + GOOGLE_PSE_ENGINE_ID
- BRAVE_SEARCH_API_KEY
- SERPSTACK_API_KEY
- SERPER_API_KEY
- SERPLY_API_KEY
- TAVILY_API_KEY
Args:
query (str): The query to search for
"""
# TODO: add playwright to search the web
if engine == "searxng":
if app.state.config.SEARXNG_QUERY_URL:
return search_searxng(
app.state.config.SEARXNG_QUERY_URL,
query,
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
)
else:
raise Exception("No SEARXNG_QUERY_URL found in environment variables")
elif engine == "google_pse":
if (
app.state.config.GOOGLE_PSE_API_KEY
and app.state.config.GOOGLE_PSE_ENGINE_ID
):
return search_google_pse(
app.state.config.GOOGLE_PSE_API_KEY,
app.state.config.GOOGLE_PSE_ENGINE_ID,
query,
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
)
else:
raise Exception(
"No GOOGLE_PSE_API_KEY or GOOGLE_PSE_ENGINE_ID found in environment variables"
)
elif engine == "brave":
if app.state.config.BRAVE_SEARCH_API_KEY:
return search_brave(
app.state.config.BRAVE_SEARCH_API_KEY,
query,
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
)
else:
raise Exception("No BRAVE_SEARCH_API_KEY found in environment variables")
elif engine == "serpstack":
if app.state.config.SERPSTACK_API_KEY:
return search_serpstack(
app.state.config.SERPSTACK_API_KEY,
query,
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
https_enabled=app.state.config.SERPSTACK_HTTPS,
)
else:
raise Exception("No SERPSTACK_API_KEY found in environment variables")
elif engine == "serper":
if app.state.config.SERPER_API_KEY:
return search_serper(
app.state.config.SERPER_API_KEY,
query,
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
)
else:
raise Exception("No SERPER_API_KEY found in environment variables")
elif engine == "serply":
if app.state.config.SERPLY_API_KEY:
return search_serply(
app.state.config.SERPLY_API_KEY,
query,
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
)
else:
raise Exception("No SERPLY_API_KEY found in environment variables")
elif engine == "duckduckgo":
return search_duckduckgo(
query,
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
)
elif engine == "tavily":
if app.state.config.TAVILY_API_KEY:
return search_tavily(
app.state.config.TAVILY_API_KEY,
query,
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
)
else:
raise Exception("No TAVILY_API_KEY found in environment variables")
elif engine == "jina":
return search_jina(query, app.state.config.RAG_WEB_SEARCH_RESULT_COUNT)
else:
raise Exception("No search engine API key found in environment variables")
@app.post("/web/search")
def store_web_search(form_data: SearchForm, user=Depends(get_verified_user)):
try:
logging.info(
f"trying to web search with {app.state.config.RAG_WEB_SEARCH_ENGINE, form_data.query}"
)
web_results = search_web(
app.state.config.RAG_WEB_SEARCH_ENGINE, form_data.query
)
except Exception as e:
log.exception(e)
print(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.WEB_SEARCH_ERROR(e),
)
try:
urls = [result.link for result in web_results]
loader = get_web_loader(urls)
data = loader.load()
collection_name = form_data.collection_name
if collection_name == "":
collection_name = calculate_sha256_string(form_data.query)[:63]
store_data_in_vector_db(data, collection_name, overwrite=True)
return {
"status": True,
"collection_name": collection_name,
"filenames": urls,
}
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
def store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> bool:
text_splitter = RecursiveCharacterTextSplitter(
@@ -654,6 +965,13 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b
texts = [doc.page_content for doc in docs]
metadatas = [doc.metadata for doc in docs]
# ChromaDB does not like datetime formats
# for meta-data so convert them to string.
for metadata in metadatas:
for key, value in metadata.items():
if isinstance(value, datetime):
metadata[key] = str(value)
try:
if overwrite:
for collection in CHROMA_CLIENT.list_collections():
@@ -669,6 +987,7 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b
app.state.sentence_transformer_ef,
app.state.config.OPENAI_API_KEY,
app.state.config.OPENAI_API_BASE_URL,
app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
)
embedding_texts = list(map(lambda x: x.replace("\n", " "), texts))
@@ -685,13 +1004,49 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b
return True
except Exception as e:
log.exception(e)
if e.__class__.__name__ == "UniqueConstraintError":
return True
log.exception(e)
return False
class TikaLoader:
def __init__(self, file_path, mime_type=None):
self.file_path = file_path
self.mime_type = mime_type
def load(self) -> List[Document]:
with open(self.file_path, "rb") as f:
data = f.read()
if self.mime_type is not None:
headers = {"Content-Type": self.mime_type}
else:
headers = {}
endpoint = app.state.config.TIKA_SERVER_URL
if not endpoint.endswith("/"):
endpoint += "/"
endpoint += "tika/text"
r = requests.put(endpoint, data=data, headers=headers)
if r.ok:
raw_metadata = r.json()
text = raw_metadata.get("X-TIKA:content", "<No text content found>")
if "Content-Type" in raw_metadata:
headers["Content-Type"] = raw_metadata["Content-Type"]
log.info("Tika extracted text: %s", text)
return [Document(page_content=text, metadata=headers)]
else:
raise Exception(f"Error calling Tika: {r.reason}")
def get_loader(filename: str, file_content_type: str, file_path: str):
file_ext = filename.split(".")[-1].lower()
known_type = True
@@ -739,42 +1094,61 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
"swift",
"vue",
"svelte",
"msg",
]
if file_ext == "pdf":
loader = PyPDFLoader(
file_path, extract_images=app.state.config.PDF_EXTRACT_IMAGES
)
elif file_ext == "csv":
loader = CSVLoader(file_path)
elif file_ext == "rst":
loader = UnstructuredRSTLoader(file_path, mode="elements")
elif file_ext == "xml":
loader = UnstructuredXMLLoader(file_path)
elif file_ext in ["htm", "html"]:
loader = BSHTMLLoader(file_path, open_encoding="unicode_escape")
elif file_ext == "md":
loader = UnstructuredMarkdownLoader(file_path)
elif file_content_type == "application/epub+zip":
loader = UnstructuredEPubLoader(file_path)
elif (
file_content_type
== "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
or file_ext in ["doc", "docx"]
if (
app.state.config.CONTENT_EXTRACTION_ENGINE == "tika"
and app.state.config.TIKA_SERVER_URL
):
loader = Docx2txtLoader(file_path)
elif file_content_type in [
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
] or file_ext in ["xls", "xlsx"]:
loader = UnstructuredExcelLoader(file_path)
elif file_ext in known_source_ext or (
file_content_type and file_content_type.find("text/") >= 0
):
loader = TextLoader(file_path, autodetect_encoding=True)
if file_ext in known_source_ext or (
file_content_type and file_content_type.find("text/") >= 0
):
loader = TextLoader(file_path, autodetect_encoding=True)
else:
loader = TikaLoader(file_path, file_content_type)
else:
loader = TextLoader(file_path, autodetect_encoding=True)
known_type = False
if file_ext == "pdf":
loader = PyPDFLoader(
file_path, extract_images=app.state.config.PDF_EXTRACT_IMAGES
)
elif file_ext == "csv":
loader = CSVLoader(file_path)
elif file_ext == "rst":
loader = UnstructuredRSTLoader(file_path, mode="elements")
elif file_ext == "xml":
loader = UnstructuredXMLLoader(file_path)
elif file_ext in ["htm", "html"]:
loader = BSHTMLLoader(file_path, open_encoding="unicode_escape")
elif file_ext == "md":
loader = UnstructuredMarkdownLoader(file_path)
elif file_content_type == "application/epub+zip":
loader = UnstructuredEPubLoader(file_path)
elif (
file_content_type
== "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
or file_ext in ["doc", "docx"]
):
loader = Docx2txtLoader(file_path)
elif file_content_type in [
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
] or file_ext in ["xls", "xlsx"]:
loader = UnstructuredExcelLoader(file_path)
elif file_content_type in [
"application/vnd.ms-powerpoint",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
] or file_ext in ["ppt", "pptx"]:
loader = UnstructuredPowerPointLoader(file_path)
elif file_ext == "msg":
loader = OutlookMessageLoader(file_path)
elif file_ext in known_source_ext or (
file_content_type and file_content_type.find("text/") >= 0
):
loader = TextLoader(file_path, autodetect_encoding=True)
else:
loader = TextLoader(file_path, autodetect_encoding=True)
known_type = False
return loader, known_type
@@ -783,7 +1157,7 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
def store_doc(
collection_name: Optional[str] = Form(None),
file: UploadFile = File(...),
user=Depends(get_current_user),
user=Depends(get_verified_user),
):
# "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm"
@@ -836,6 +1210,60 @@ def store_doc(
)
class ProcessDocForm(BaseModel):
file_id: str
collection_name: Optional[str] = None
@app.post("/process/doc")
def process_doc(
form_data: ProcessDocForm,
user=Depends(get_verified_user),
):
try:
file = Files.get_file_by_id(form_data.file_id)
file_path = file.meta.get("path", f"{UPLOAD_DIR}/{file.filename}")
f = open(file_path, "rb")
collection_name = form_data.collection_name
if collection_name == None:
collection_name = calculate_sha256(f)[:63]
f.close()
loader, known_type = get_loader(
file.filename, file.meta.get("content_type"), file_path
)
data = loader.load()
try:
result = store_data_in_vector_db(data, collection_name)
if result:
return {
"status": True,
"collection_name": collection_name,
"known_type": known_type,
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=e,
)
except Exception as e:
log.exception(e)
if "No pandoc was found" in str(e):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.PANDOC_NOT_INSTALLED,
)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
class TextRAGForm(BaseModel):
name: str
content: str
@@ -845,7 +1273,7 @@ class TextRAGForm(BaseModel):
@app.post("/text")
def store_text(
form_data: TextRAGForm,
user=Depends(get_current_user),
user=Depends(get_verified_user),
):
collection_name = form_data.collection_name
@@ -933,6 +1361,30 @@ def reset_vector_db(user=Depends(get_admin_user)):
CHROMA_CLIENT.reset()
@app.get("/reset/uploads")
def reset_upload_dir(user=Depends(get_admin_user)) -> bool:
folder = f"{UPLOAD_DIR}"
try:
# Check if the directory exists
if os.path.exists(folder):
# Iterate over all the files and directories in the specified directory
for filename in os.listdir(folder):
file_path = os.path.join(folder, filename)
try:
if os.path.isfile(file_path) or os.path.islink(file_path):
os.unlink(file_path) # Remove the file or link
elif os.path.isdir(file_path):
shutil.rmtree(file_path) # Remove the directory
except Exception as e:
print(f"Failed to delete {file_path}. Reason: {e}")
else:
print(f"The directory {folder} does not exist")
except Exception as e:
print(f"Failed to process the directory {folder}. Reason: {e}")
return True
@app.get("/reset")
def reset(user=Depends(get_admin_user)) -> bool:
folder = f"{UPLOAD_DIR}"
@@ -954,6 +1406,33 @@ def reset(user=Depends(get_admin_user)) -> bool:
return True
class SafeWebBaseLoader(WebBaseLoader):
"""WebBaseLoader with enhanced error handling for URLs."""
def lazy_load(self) -> Iterator[Document]:
"""Lazy load text from the url(s) in web_path with error handling."""
for path in self.web_paths:
try:
soup = self._scrape(path, bs_kwargs=self.bs_kwargs)
text = soup.get_text(**self.bs_get_text_kwargs)
# Build metadata
metadata = {"source": path}
if title := soup.find("title"):
metadata["title"] = title.get_text()
if description := soup.find("meta", attrs={"name": "description"}):
metadata["description"] = description.get(
"content", "No description found."
)
if html := soup.find("html"):
metadata["language"] = html.get("lang", "No language found.")
yield Document(page_content=text, metadata=metadata)
except Exception as e:
# Log the error and continue with the next URL
log.error(f"Error loading {path}: {e}")
if ENV == "dev":
@app.get("/ef")

View File

@@ -0,0 +1,42 @@
import logging
from typing import List, Optional
import requests
from apps.rag.search.main import SearchResult, get_filtered_results
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_brave(
api_key: str, query: str, count: int, filter_list: Optional[List[str]] = None
) -> list[SearchResult]:
"""Search using Brave's Search API and return the results as a list of SearchResult objects.
Args:
api_key (str): A Brave Search API key
query (str): The query to search for
"""
url = "https://api.search.brave.com/res/v1/web/search"
headers = {
"Accept": "application/json",
"Accept-Encoding": "gzip",
"X-Subscription-Token": api_key,
}
params = {"q": query, "count": count}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
json_response = response.json()
results = json_response.get("web", {}).get("results", [])
if filter_list:
results = get_filtered_results(results, filter_list)
return [
SearchResult(
link=result["url"], title=result.get("title"), snippet=result.get("snippet")
)
for result in results[:count]
]

View File

@@ -0,0 +1,49 @@
import logging
from typing import List, Optional
from apps.rag.search.main import SearchResult, get_filtered_results
from duckduckgo_search import DDGS
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_duckduckgo(
query: str, count: int, filter_list: Optional[List[str]] = None
) -> list[SearchResult]:
"""
Search using DuckDuckGo's Search API and return the results as a list of SearchResult objects.
Args:
query (str): The query to search for
count (int): The number of results to return
Returns:
List[SearchResult]: A list of search results
"""
# Use the DDGS context manager to create a DDGS object
with DDGS() as ddgs:
# Use the ddgs.text() method to perform the search
ddgs_gen = ddgs.text(
query, safesearch="moderate", max_results=count, backend="api"
)
# Check if there are search results
if ddgs_gen:
# Convert the search results into a list
search_results = [r for r in ddgs_gen]
# Create an empty list to store the SearchResult objects
results = []
# Iterate over each search result
for result in search_results:
# Create a SearchResult object and append it to the results list
results.append(
SearchResult(
link=result["href"],
title=result.get("title"),
snippet=result.get("body"),
)
)
if filter_list:
results = get_filtered_results(results, filter_list)
# Return the list of search results
return results

View File

@@ -0,0 +1,51 @@
import json
import logging
from typing import List, Optional
import requests
from apps.rag.search.main import SearchResult, get_filtered_results
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_google_pse(
api_key: str,
search_engine_id: str,
query: str,
count: int,
filter_list: Optional[List[str]] = None,
) -> list[SearchResult]:
"""Search using Google's Programmable Search Engine API and return the results as a list of SearchResult objects.
Args:
api_key (str): A Programmable Search Engine API key
search_engine_id (str): A Programmable Search Engine ID
query (str): The query to search for
"""
url = "https://www.googleapis.com/customsearch/v1"
headers = {"Content-Type": "application/json"}
params = {
"cx": search_engine_id,
"q": query,
"key": api_key,
"num": count,
}
response = requests.request("GET", url, headers=headers, params=params)
response.raise_for_status()
json_response = response.json()
results = json_response.get("items", [])
if filter_list:
results = get_filtered_results(results, filter_list)
return [
SearchResult(
link=result["link"],
title=result.get("title"),
snippet=result.get("snippet"),
)
for result in results
]

View File

@@ -0,0 +1,41 @@
import logging
import requests
from yarl import URL
from apps.rag.search.main import SearchResult
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_jina(query: str, count: int) -> list[SearchResult]:
"""
Search using Jina's Search API and return the results as a list of SearchResult objects.
Args:
query (str): The query to search for
count (int): The number of results to return
Returns:
List[SearchResult]: A list of search results
"""
jina_search_endpoint = "https://s.jina.ai/"
headers = {
"Accept": "application/json",
}
url = str(URL(jina_search_endpoint + query))
response = requests.get(url, headers=headers)
response.raise_for_status()
data = response.json()
results = []
for result in data["data"][:count]:
results.append(
SearchResult(
link=result["url"],
title=result.get("title"),
snippet=result.get("content"),
)
)
return results

View File

@@ -0,0 +1,20 @@
from typing import Optional
from urllib.parse import urlparse
from pydantic import BaseModel
def get_filtered_results(results, filter_list):
if not filter_list:
return results
filtered_results = []
for result in results:
domain = urlparse(result["url"]).netloc
if any(domain.endswith(filtered_domain) for filtered_domain in filter_list):
filtered_results.append(result)
return filtered_results
class SearchResult(BaseModel):
link: str
title: Optional[str]
snippet: Optional[str]

View File

@@ -0,0 +1,92 @@
import logging
import requests
from typing import List, Optional
from apps.rag.search.main import SearchResult, get_filtered_results
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_searxng(
query_url: str,
query: str,
count: int,
filter_list: Optional[List[str]] = None,
**kwargs,
) -> List[SearchResult]:
"""
Search a SearXNG instance for a given query and return the results as a list of SearchResult objects.
The function allows passing additional parameters such as language or time_range to tailor the search result.
Args:
query_url (str): The base URL of the SearXNG server.
query (str): The search term or question to find in the SearXNG database.
count (int): The maximum number of results to retrieve from the search.
Keyword Args:
language (str): Language filter for the search results; e.g., "en-US". Defaults to an empty string.
safesearch (int): Safe search filter for safer web results; 0 = off, 1 = moderate, 2 = strict. Defaults to 1 (moderate).
time_range (str): Time range for filtering results by date; e.g., "2023-04-05..today" or "all-time". Defaults to ''.
categories: (Optional[List[str]]): Specific categories within which the search should be performed, defaulting to an empty string if not provided.
Returns:
List[SearchResult]: A list of SearchResults sorted by relevance score in descending order.
Raise:
requests.exceptions.RequestException: If a request error occurs during the search process.
"""
# Default values for optional parameters are provided as empty strings or None when not specified.
language = kwargs.get("language", "en-US")
safesearch = kwargs.get("safesearch", "1")
time_range = kwargs.get("time_range", "")
categories = "".join(kwargs.get("categories", []))
params = {
"q": query,
"format": "json",
"pageno": 1,
"safesearch": safesearch,
"language": language,
"time_range": time_range,
"categories": categories,
"theme": "simple",
"image_proxy": 0,
}
# Legacy query format
if "<query>" in query_url:
# Strip all query parameters from the URL
query_url = query_url.split("?")[0]
log.debug(f"searching {query_url}")
response = requests.get(
query_url,
headers={
"User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) RAG Bot",
"Accept": "text/html",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "en-US,en;q=0.5",
"Connection": "keep-alive",
},
params=params,
)
response.raise_for_status() # Raise an exception for HTTP errors.
json_response = response.json()
results = json_response.get("results", [])
sorted_results = sorted(results, key=lambda x: x.get("score", 0), reverse=True)
if filter_list:
sorted_results = get_filtered_results(sorted_results, filter_list)
return [
SearchResult(
link=result["url"], title=result.get("title"), snippet=result.get("content")
)
for result in sorted_results[:count]
]

View File

@@ -0,0 +1,43 @@
import json
import logging
from typing import List, Optional
import requests
from apps.rag.search.main import SearchResult, get_filtered_results
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_serper(
api_key: str, query: str, count: int, filter_list: Optional[List[str]] = None
) -> list[SearchResult]:
"""Search using serper.dev's API and return the results as a list of SearchResult objects.
Args:
api_key (str): A serper.dev API key
query (str): The query to search for
"""
url = "https://google.serper.dev/search"
payload = json.dumps({"q": query})
headers = {"X-API-KEY": api_key, "Content-Type": "application/json"}
response = requests.request("POST", url, headers=headers, data=payload)
response.raise_for_status()
json_response = response.json()
results = sorted(
json_response.get("organic", []), key=lambda x: x.get("position", 0)
)
if filter_list:
results = get_filtered_results(results, filter_list)
return [
SearchResult(
link=result["link"],
title=result.get("title"),
snippet=result.get("description"),
)
for result in results[:count]
]

View File

@@ -0,0 +1,70 @@
import json
import logging
from typing import List, Optional
import requests
from urllib.parse import urlencode
from apps.rag.search.main import SearchResult, get_filtered_results
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_serply(
api_key: str,
query: str,
count: int,
hl: str = "us",
limit: int = 10,
device_type: str = "desktop",
proxy_location: str = "US",
filter_list: Optional[List[str]] = None,
) -> list[SearchResult]:
"""Search using serper.dev's API and return the results as a list of SearchResult objects.
Args:
api_key (str): A serply.io API key
query (str): The query to search for
hl (str): Host Language code to display results in (reference https://developers.google.com/custom-search/docs/xml_results?hl=en#wsInterfaceLanguages)
limit (int): The maximum number of results to return [10-100, defaults to 10]
"""
log.info("Searching with Serply")
url = "https://api.serply.io/v1/search/"
query_payload = {
"q": query,
"language": "en",
"num": limit,
"gl": proxy_location.upper(),
"hl": hl.lower(),
}
url = f"{url}{urlencode(query_payload)}"
headers = {
"X-API-KEY": api_key,
"X-User-Agent": device_type,
"User-Agent": "open-webui",
"X-Proxy-Location": proxy_location,
}
response = requests.request("GET", url, headers=headers)
response.raise_for_status()
json_response = response.json()
log.info(f"results from serply search: {json_response}")
results = sorted(
json_response.get("results", []), key=lambda x: x.get("realPosition", 0)
)
if filter_list:
results = get_filtered_results(results, filter_list)
return [
SearchResult(
link=result["link"],
title=result.get("title"),
snippet=result.get("description"),
)
for result in results[:count]
]

View File

@@ -0,0 +1,49 @@
import json
import logging
from typing import List, Optional
import requests
from apps.rag.search.main import SearchResult, get_filtered_results
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_serpstack(
api_key: str,
query: str,
count: int,
filter_list: Optional[List[str]] = None,
https_enabled: bool = True,
) -> list[SearchResult]:
"""Search using serpstack.com's and return the results as a list of SearchResult objects.
Args:
api_key (str): A serpstack.com API key
query (str): The query to search for
https_enabled (bool): Whether to use HTTPS or HTTP for the API request
"""
url = f"{'https' if https_enabled else 'http'}://api.serpstack.com/search"
headers = {"Content-Type": "application/json"}
params = {
"access_key": api_key,
"query": query,
}
response = requests.request("POST", url, headers=headers, params=params)
response.raise_for_status()
json_response = response.json()
results = sorted(
json_response.get("organic_results", []), key=lambda x: x.get("position", 0)
)
if filter_list:
results = get_filtered_results(results, filter_list)
return [
SearchResult(
link=result["url"], title=result.get("title"), snippet=result.get("snippet")
)
for result in results[:count]
]

View File

@@ -0,0 +1,39 @@
import logging
import requests
from apps.rag.search.main import SearchResult
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_tavily(api_key: str, query: str, count: int) -> list[SearchResult]:
"""Search using Tavily's Search API and return the results as a list of SearchResult objects.
Args:
api_key (str): A Tavily Search API key
query (str): The query to search for
Returns:
List[SearchResult]: A list of search results
"""
url = "https://api.tavily.com/search"
data = {"query": query, "api_key": api_key}
response = requests.post(url, json=data)
response.raise_for_status()
json_response = response.json()
raw_search_results = json_response.get("results", [])
return [
SearchResult(
link=result["url"],
title=result.get("title", ""),
snippet=result.get("content"),
)
for result in raw_search_results[:count]
]

View File

@@ -0,0 +1,998 @@
{
"query": {
"original": "python",
"show_strict_warning": false,
"is_navigational": true,
"is_news_breaking": false,
"spellcheck_off": true,
"country": "us",
"bad_results": false,
"should_fallback": false,
"postal_code": "",
"city": "",
"header_country": "",
"more_results_available": true,
"state": ""
},
"mixed": {
"type": "mixed",
"main": [
{
"type": "web",
"index": 0,
"all": false
},
{
"type": "web",
"index": 1,
"all": false
},
{
"type": "news",
"all": true
},
{
"type": "web",
"index": 2,
"all": false
},
{
"type": "videos",
"all": true
},
{
"type": "web",
"index": 3,
"all": false
},
{
"type": "web",
"index": 4,
"all": false
},
{
"type": "web",
"index": 5,
"all": false
},
{
"type": "web",
"index": 6,
"all": false
},
{
"type": "web",
"index": 7,
"all": false
},
{
"type": "web",
"index": 8,
"all": false
},
{
"type": "web",
"index": 9,
"all": false
},
{
"type": "web",
"index": 10,
"all": false
},
{
"type": "web",
"index": 11,
"all": false
},
{
"type": "web",
"index": 12,
"all": false
},
{
"type": "web",
"index": 13,
"all": false
},
{
"type": "web",
"index": 14,
"all": false
},
{
"type": "web",
"index": 15,
"all": false
},
{
"type": "web",
"index": 16,
"all": false
},
{
"type": "web",
"index": 17,
"all": false
},
{
"type": "web",
"index": 18,
"all": false
},
{
"type": "web",
"index": 19,
"all": false
}
],
"top": [],
"side": []
},
"news": {
"type": "news",
"results": [
{
"title": "Google lays off staff from Flutter, Dart and Python teams weeks before its developer conference | TechCrunch",
"url": "https://techcrunch.com/2024/05/01/google-lays-off-staff-from-flutter-dart-python-weeks-before-its-developer-conference/",
"is_source_local": false,
"is_source_both": false,
"description": "Google told TechCrunch that Flutter will have new updates to share at I/O this year.",
"page_age": "2024-05-02T17:40:05",
"family_friendly": true,
"meta_url": {
"scheme": "https",
"netloc": "techcrunch.com",
"hostname": "techcrunch.com",
"favicon": "https://imgs.search.brave.com/N6VSEVahheQOb7lqfb47dhUOB4XD-6sfQOP94sCe3Oo/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZGI5Njk0Yzlk/YWM3ZWMwZjg1MTM1/NmIyMWEyNzBjZDZj/ZDQyNmFlNGU0NDRi/MDgyYjQwOGU0Y2Qy/ZWMwNWQ2ZC90ZWNo/Y3J1bmNoLmNvbS8",
"path": " 2024 05 01 google-lays-off-staff-from-flutter-dart-python-weeks-before-its-developer-conference"
},
"breaking": false,
"thumbnail": {
"src": "https://imgs.search.brave.com/gCI5UG8muOEOZDAx9vpu6L6r6R00mD7jOF08-biFoyQ/rs:fit:200:200:1/g:ce/aHR0cHM6Ly90ZWNo/Y3J1bmNoLmNvbS93/cC1jb250ZW50L3Vw/bG9hZHMvMjAxOC8x/MS9HZXR0eUltYWdl/cy0xMDAyNDg0NzQ2/LmpwZz9yZXNpemU9/MTIwMCw4MDA"
},
"age": "3 days ago",
"extra_snippets": [
"Ahead of Googles annual I/O developer conference in May, the tech giant has laid off staff across key teams like Flutter, Dart, Python and others, according to reports from affected employees shared on social media. Google confirmed the layoffs to TechCrunch, but not the specific teams, roles or how many people were let go.",
"In a separate post on Reddit, another commenter noted the Python team affected by the layoffs were those who managed the internal Python runtimes and toolchains and worked with OSS Python. Included in this group were “multiple current and former core devs and steering council members,” they said.",
"Meanwhile, others shared on Y Combinators Hacker News, where a Python team member detailed their specific duties on the technical front and noted that, for years, much of the work was done with fewer than 10 people. Another Hacker News commenter said their early years on the Python team were spent paying down internal technical debt accumulated from not having a strong Python strategy.",
"CNBC reports that a total of 200 people were let go across Googles “Core” teams, which included those working on Python, app platforms, and other engineering roles. Some jobs were being shifted to India and Mexico, it said, citing internal documents."
]
}
],
"mutated_by_goggles": false
},
"type": "search",
"videos": {
"type": "videos",
"results": [
{
"type": "video_result",
"url": "https://www.youtube.com/watch?v=b093aqAZiPU",
"title": "👩‍💻 Python for Beginners Tutorial - YouTube",
"description": "In this step-by-step Python for beginner's tutorial, learn how you can get started programming in Python. In this video, I assume that you are completely new...",
"age": "March 25, 2021",
"page_age": "2021-03-25T10:00:08",
"video": {},
"meta_url": {
"scheme": "https",
"netloc": "youtube.com",
"hostname": "www.youtube.com",
"favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v",
"path": " watch"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/tZI4Do4_EYcTCsD_MvE3Jx8FzjIXwIJ5ZuKhwiWTyZs/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9i/MDkzYXFBWmlQVS9t/YXhyZXNkZWZhdWx0/LmpwZw"
}
},
{
"type": "video_result",
"url": "https://www.youtube.com/watch?v=rfscVS0vtbw",
"title": "Learn Python - Full Course for Beginners [Tutorial] - YouTube",
"description": "This course will give you a full introduction into all of the core concepts in python. Follow along with the videos and you'll be a python programmer in no t...",
"age": "July 11, 2018",
"page_age": "2018-07-11T18:00:42",
"video": {},
"meta_url": {
"scheme": "https",
"netloc": "youtube.com",
"hostname": "www.youtube.com",
"favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v",
"path": " watch"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/65zkx_kPU_zJb-4nmvvY-q5-ZZwzceChz-N00V8cqvk/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9y/ZnNjVlMwdnRidy9t/YXhyZXNkZWZhdWx0/LmpwZw"
}
},
{
"type": "video_result",
"url": "https://www.youtube.com/watch?v=_uQrJ0TkZlc",
"title": "Python Tutorial - Python Full Course for Beginners - YouTube",
"description": "Become a Python pro! 🚀 This comprehensive tutorial takes you from beginner to hero, covering the basics, machine learning, and web development projects.🚀 W...",
"age": "February 18, 2019",
"page_age": "2019-02-18T15:00:08",
"video": {},
"meta_url": {
"scheme": "https",
"netloc": "youtube.com",
"hostname": "www.youtube.com",
"favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v",
"path": " watch"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/Djiv1pXLq1ClqBSE_86jQnEYR8bW8UJP6Cs7LrgyQzQ/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9f/dVFySjBUa1psYy9t/YXhyZXNkZWZhdWx0/LmpwZw"
}
},
{
"type": "video_result",
"url": "https://www.youtube.com/watch?v=wRKgzC-MhIc",
"title": "[] and {} vs list() and dict(), which is better?",
"description": "Enjoy the videos and music you love, upload original content, and share it all with friends, family, and the world on YouTube.",
"video": {},
"meta_url": {
"scheme": "https",
"netloc": "youtube.com",
"hostname": "www.youtube.com",
"favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v",
"path": " watch"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/Hw9ep2Pio13X1VZjRw_h9R2VH_XvZFOuGlQJVnVkeq0/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS93/UktnekMtTWhJYy9o/cWRlZmF1bHQuanBn"
}
},
{
"type": "video_result",
"url": "https://www.youtube.com/watch?v=LWdsF79H1Pg",
"title": "print() vs. return in Python Functions - YouTube",
"description": "In this video, you will learn the differences between the return statement and the print function when they are used inside Python functions. We will see an ...",
"age": "June 11, 2022",
"page_age": "2022-06-11T21:33:26",
"video": {},
"meta_url": {
"scheme": "https",
"netloc": "youtube.com",
"hostname": "www.youtube.com",
"favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v",
"path": " watch"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/ebglnr5_jwHHpvon3WU-5hzt0eHdTZSVGg3Ts6R38xY/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9M/V2RzRjc5SDFQZy9t/YXhyZXNkZWZhdWx0/LmpwZw"
}
},
{
"type": "video_result",
"url": "https://www.youtube.com/watch?v=AovxLr8jUH4",
"title": "Python Tutorial for Beginners 5 - Python print() and input() Function ...",
"description": "In this Video I am going to show How to use print() Function and input() Function in Python. In python The print() function is used to print the specified ...",
"age": "August 28, 2018",
"page_age": "2018-08-28T20:11:09",
"video": {},
"meta_url": {
"scheme": "https",
"netloc": "youtube.com",
"hostname": "www.youtube.com",
"favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v",
"path": " watch"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/nCoLEcWkKtiecprWbS6nufwGCaSbPH7o0-sMeIkFmjI/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9B/b3Z4THI4alVINC9o/cWRlZmF1bHQuanBn"
}
}
],
"mutated_by_goggles": false
},
"web": {
"type": "search",
"results": [
{
"title": "Welcome to Python.org",
"url": "https://www.python.org",
"is_source_local": false,
"is_source_both": false,
"description": "The official home of the <strong>Python</strong> Programming Language",
"page_age": "2023-09-09T15:55:05",
"profile": {
"name": "Python",
"url": "https://www.python.org",
"long_name": "python.org",
"img": "https://imgs.search.brave.com/vBaRH-v6oPS4csO4cdvuKhZ7-xDVvydin3oe3zXYxAI/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNTJjMzZjNDBj/MmIzODgwMGUyOTRj/Y2E5MjM3YjRkYTZj/YWI1Yzk1NTlmYTgw/ZDBjNzM0MGMxZjQz/YWFjNTczYy93d3cu/cHl0aG9uLm9yZy8"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "python.org",
"hostname": "www.python.org",
"favicon": "https://imgs.search.brave.com/vBaRH-v6oPS4csO4cdvuKhZ7-xDVvydin3oe3zXYxAI/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNTJjMzZjNDBj/MmIzODgwMGUyOTRj/Y2E5MjM3YjRkYTZj/YWI1Yzk1NTlmYTgw/ZDBjNzM0MGMxZjQz/YWFjNTczYy93d3cu/cHl0aG9uLm9yZy8",
"path": ""
},
"thumbnail": {
"src": "https://imgs.search.brave.com/GGfNfe5rxJ8QWEoxXniSLc0-POLU3qPyTIpuqPdbmXk/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/cHl0aG9uLm9yZy9z/dGF0aWMvb3Blbmdy/YXBoLWljb24tMjAw/eDIwMC5wbmc",
"original": "https://www.python.org/static/opengraph-icon-200x200.png",
"logo": false
},
"age": "September 9, 2023",
"cluster_type": "generic",
"cluster": [
{
"title": "Downloads",
"url": "https://www.python.org/downloads/",
"is_source_local": false,
"is_source_both": false,
"description": "The official home of the <strong>Python</strong> Programming Language",
"family_friendly": true
},
{
"title": "Macos",
"url": "https://www.python.org/downloads/macos/",
"is_source_local": false,
"is_source_both": false,
"description": "The official home of the <strong>Python</strong> Programming Language",
"family_friendly": true
},
{
"title": "Windows",
"url": "https://www.python.org/downloads/windows/",
"is_source_local": false,
"is_source_both": false,
"description": "The official home of the <strong>Python</strong> Programming Language",
"family_friendly": true
},
{
"title": "Getting Started",
"url": "https://www.python.org/about/gettingstarted/",
"is_source_local": false,
"is_source_both": false,
"description": "The official home of the <strong>Python</strong> Programming Language",
"family_friendly": true
}
],
"extra_snippets": [
"Calculations are simple with Python, and expression syntax is straightforward: the operators +, -, * and / work as expected; parentheses () can be used for grouping. More about simple math functions in Python 3.",
"The core of extensible programming is defining functions. Python allows mandatory and optional arguments, keyword arguments, and even arbitrary argument lists. More about defining functions in Python 3",
"Lists (known as arrays in other languages) are one of the compound data types that Python understands. Lists can be indexed, sliced and manipulated with other built-in functions. More about lists in Python 3",
"# Python 3: Simple output (with Unicode) >>> print(\"Hello, I'm Python!\") Hello, I'm Python! # Input, assignment >>> name = input('What is your name?\\n') >>> print('Hi, %s.' % name) What is your name? Python Hi, Python."
]
},
{
"title": "Python (programming language) - Wikipedia",
"url": "https://en.wikipedia.org/wiki/Python_(programming_language)",
"is_source_local": false,
"is_source_both": false,
"description": "<strong>Python</strong> is a high-level, general-purpose programming language. Its design philosophy emphasizes code readability with the use of significant indentation. <strong>Python</strong> is dynamically typed and garbage-collected. It supports multiple programming paradigms, including structured (particularly procedural), ...",
"page_age": "2024-05-01T12:54:03",
"profile": {
"name": "Wikipedia",
"url": "https://en.wikipedia.org/wiki/Python_(programming_language)",
"long_name": "en.wikipedia.org",
"img": "https://imgs.search.brave.com/0kxnVOiqv-faZvOJc7zpym4Zin1CTs1f1svfNZSzmfU/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNjQwNGZhZWY0/ZTQ1YWUzYzQ3MDUw/MmMzMGY3NTQ0ZjNj/NDUwMDk5ZTI3MWRk/NWYyNTM4N2UwOTE0/NTI3ZDQzNy9lbi53/aWtpcGVkaWEub3Jn/Lw"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "en.wikipedia.org",
"hostname": "en.wikipedia.org",
"favicon": "https://imgs.search.brave.com/0kxnVOiqv-faZvOJc7zpym4Zin1CTs1f1svfNZSzmfU/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNjQwNGZhZWY0/ZTQ1YWUzYzQ3MDUw/MmMzMGY3NTQ0ZjNj/NDUwMDk5ZTI3MWRk/NWYyNTM4N2UwOTE0/NTI3ZDQzNy9lbi53/aWtpcGVkaWEub3Jn/Lw",
"path": " wiki Python_(programming_language)"
},
"age": "4 days ago",
"extra_snippets": [
"Python is dynamically typed and garbage-collected. It supports multiple programming paradigms, including structured (particularly procedural), object-oriented and functional programming. It is often described as a \"batteries included\" language due to its comprehensive standard library.",
"Guido van Rossum began working on Python in the late 1980s as a successor to the ABC programming language and first released it in 1991 as Python 0.9.0. Python 2.0 was released in 2000. Python 3.0, released in 2008, was a major revision not completely backward-compatible with earlier versions. Python 2.7.18, released in 2020, was the last release of Python 2.",
"Python was invented in the late 1980s by Guido van Rossum at Centrum Wiskunde & Informatica (CWI) in the Netherlands as a successor to the ABC programming language, which was inspired by SETL, capable of exception handling and interfacing with the Amoeba operating system.",
"Python consistently ranks as one of the most popular programming languages, and has gained widespread use in the machine learning community."
]
},
{
"title": "Python Tutorial",
"url": "https://www.w3schools.com/python/",
"is_source_local": false,
"is_source_both": false,
"description": "W3Schools offers free online tutorials, references and exercises in all the major languages of the web. Covering popular subjects like HTML, CSS, JavaScript, <strong>Python</strong>, SQL, Java, and many, many more.",
"page_age": "2017-12-07T00:00:00",
"profile": {
"name": "W3Schools",
"url": "https://www.w3schools.com/python/",
"long_name": "w3schools.com",
"img": "https://imgs.search.brave.com/JwO5r7z3HTBkU29vgNH_4rrSWLf2M4-8FMWNvbxrKX8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjVlMGVkZDVj/ZGMyZWRmMzAwODRi/ZDAwZGE4NWI3NmU4/MjRhNjEzOGFhZWY3/ZGViMjY1OWY2ZDYw/YTZiOGUyZS93d3cu/dzNzY2hvb2xzLmNv/bS8"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "w3schools.com",
"hostname": "www.w3schools.com",
"favicon": "https://imgs.search.brave.com/JwO5r7z3HTBkU29vgNH_4rrSWLf2M4-8FMWNvbxrKX8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjVlMGVkZDVj/ZGMyZWRmMzAwODRi/ZDAwZGE4NWI3NmU4/MjRhNjEzOGFhZWY3/ZGViMjY1OWY2ZDYw/YTZiOGUyZS93d3cu/dzNzY2hvb2xzLmNv/bS8",
"path": " python"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/EMfp8dodbJehmj0yCJh8317RHuaumsddnHI4bujvFcg/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/dzNzY2hvb2xzLmNv/bS9pbWFnZXMvdzNz/Y2hvb2xzX2xvZ29f/NDM2XzIucG5n",
"original": "https://www.w3schools.com/images/w3schools_logo_436_2.png",
"logo": true
},
"age": "December 7, 2017",
"extra_snippets": [
"Well organized and easy to understand Web building tutorials with lots of examples of how to use HTML, CSS, JavaScript, SQL, Python, PHP, Bootstrap, Java, XML and more.",
"HTML CSS JAVASCRIPT SQL PYTHON JAVA PHP HOW TO W3.CSS C C++ C# BOOTSTRAP REACT MYSQL JQUERY EXCEL XML DJANGO NUMPY PANDAS NODEJS R TYPESCRIPT ANGULAR GIT POSTGRESQL MONGODB ASP AI GO KOTLIN SASS VUE DSA GEN AI SCIPY AWS CYBERSECURITY DATA SCIENCE",
"Python Variables Variable Names Assign Multiple Values Output Variables Global Variables Variable Exercises Python Data Types Python Numbers Python Casting Python Strings",
"Python Strings Slicing Strings Modify Strings Concatenate Strings Format Strings Escape Characters String Methods String Exercises Python Booleans Python Operators Python Lists"
]
},
{
"title": "Online Python - IDE, Editor, Compiler, Interpreter",
"url": "https://www.online-python.com/",
"is_source_local": false,
"is_source_both": false,
"description": "Build and Run your <strong>Python</strong> code instantly. Online-<strong>Python</strong> is a quick and easy tool that helps you to build, compile, test your <strong>python</strong> programs.",
"profile": {
"name": "Online-python",
"url": "https://www.online-python.com/",
"long_name": "online-python.com",
"img": "https://imgs.search.brave.com/kfaEvapwHxSsRObO52-I-otYFPHpG1h7UXJyUqDM2Ec/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZGYxODdjNWQ0/NjZjZTNiMjk5NDY1/MWI5MTgyYjU3Y2Q3/MTI3NGM5MjUzY2Fi/OGQ3MTQ4MmIxMTQx/ZTcxNWFhMC93d3cu/b25saW5lLXB5dGhv/bi5jb20v"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "online-python.com",
"hostname": "www.online-python.com",
"favicon": "https://imgs.search.brave.com/kfaEvapwHxSsRObO52-I-otYFPHpG1h7UXJyUqDM2Ec/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZGYxODdjNWQ0/NjZjZTNiMjk5NDY1/MWI5MTgyYjU3Y2Q3/MTI3NGM5MjUzY2Fi/OGQ3MTQ4MmIxMTQx/ZTcxNWFhMC93d3cu/b25saW5lLXB5dGhv/bi5jb20v",
"path": ""
},
"extra_snippets": [
"Build, run, and share Python code online for free with the help of online-integrated python's development environment (IDE). It is one of the most efficient, dependable, and potent online compilers for the Python programming language. It is not necessary for you to bother about establishing a Python environment in your local.",
"It is one of the most efficient, dependable, and potent online compilers for the Python programming language. It is not necessary for you to bother about establishing a Python environment in your local. Now You can immediately execute the Python code in the web browser of your choice.",
"It is not necessary for you to bother about establishing a Python environment in your local. Now You can immediately execute the Python code in the web browser of your choice. Using this Python editor is simple and quick to get up and running with. Simply type in the programme, and then press the RUN button!",
"Now You can immediately execute the Python code in the web browser of your choice. Using this Python editor is simple and quick to get up and running with. Simply type in the programme, and then press the RUN button! The code can be saved online by choosing the SHARE option, which also gives you the ability to access your code from any location providing you have internet access."
]
},
{
"title": "Python · GitHub",
"url": "https://github.com/python",
"is_source_local": false,
"is_source_both": false,
"description": "Repositories related to the <strong>Python</strong> Programming language - <strong>Python</strong>",
"page_age": "2023-03-06T00:00:00",
"profile": {
"name": "GitHub",
"url": "https://github.com/python",
"long_name": "github.com",
"img": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "github.com",
"hostname": "github.com",
"favicon": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw",
"path": " python"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/POoaRfu_7gfp-D_O3qMNJrwDqJNbiDu1HuBpNJ_MpVQ/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9hdmF0/YXJzLmdpdGh1YnVz/ZXJjb250ZW50LmNv/bS91LzE1MjU5ODE_/cz0yMDAmYW1wO3Y9/NA",
"original": "https://avatars.githubusercontent.com/u/1525981?s=200&amp;v=4",
"logo": false
},
"age": "March 6, 2023",
"extra_snippets": ["Configuration for Python planets (e.g. http://planetpython.org)"]
},
{
"title": "Online Python Compiler (Interpreter)",
"url": "https://www.programiz.com/python-programming/online-compiler/",
"is_source_local": false,
"is_source_both": false,
"description": "Write and run <strong>Python</strong> code using our online compiler (interpreter). You can use <strong>Python</strong> Shell like IDLE, and take inputs from the user in our <strong>Python</strong> compiler.",
"page_age": "2020-06-02T00:00:00",
"profile": {
"name": "Programiz",
"url": "https://www.programiz.com/python-programming/online-compiler/",
"long_name": "programiz.com",
"img": "https://imgs.search.brave.com/ozj4JFayZ3Fs5c9eTp7M5g12azQ_Hblgu4dpTuHRz6U/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMGJlN2U1YjVi/Y2M3ZDU5OGMwMWNi/M2Q3YjhjOTM1ZTFk/Y2NkZjE4NGQwOGIx/MTQ4NjI2YmNhODVj/MzFkMmJhYy93d3cu/cHJvZ3JhbWl6LmNv/bS8"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "programiz.com",
"hostname": "www.programiz.com",
"favicon": "https://imgs.search.brave.com/ozj4JFayZ3Fs5c9eTp7M5g12azQ_Hblgu4dpTuHRz6U/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMGJlN2U1YjVi/Y2M3ZDU5OGMwMWNi/M2Q3YjhjOTM1ZTFk/Y2NkZjE4NGQwOGIx/MTQ4NjI2YmNhODVj/MzFkMmJhYy93d3cu/cHJvZ3JhbWl6LmNv/bS8",
"path": " python-programming online-compiler"
},
"age": "June 2, 2020",
"extra_snippets": [
"Python Online Compiler Online R Compiler SQL Online Editor Online HTML/CSS Editor Online Java Compiler C Online Compiler C++ Online Compiler C# Online Compiler JavaScript Online Compiler Online GoLang Compiler Online PHP Compiler Online Swift Compiler Online Rust Compiler",
"# Online Python compiler (interpreter) to run Python online. # Write Python 3 code in this online editor and run it. print(\"Try programiz.pro\")"
]
},
{
"title": "Python Developer",
"url": "https://twitter.com/Python_Dv/status/1786763460992544791",
"is_source_local": false,
"is_source_both": false,
"description": "<strong>Python</strong> Developer",
"page_age": "2024-05-04T14:30:03",
"profile": {
"name": "X",
"url": "https://twitter.com/Python_Dv/status/1786763460992544791",
"long_name": "twitter.com",
"img": "https://imgs.search.brave.com/Zq483bGX0GnSgym-1P7iyOyEDX3PkDZSNT8m56F862A/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvN2MxOTUxNzhj/OTY1ZTQ3N2I0MjJk/MTY5NGM0MTRlYWVi/MjU1YWE2NDUwYmQ2/YTA2MDFhMDlkZDEx/NTAzZGNiNi90d2l0/dGVyLmNvbS8"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "twitter.com",
"hostname": "twitter.com",
"favicon": "https://imgs.search.brave.com/Zq483bGX0GnSgym-1P7iyOyEDX3PkDZSNT8m56F862A/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvN2MxOTUxNzhj/OTY1ZTQ3N2I0MjJk/MTY5NGM0MTRlYWVi/MjU1YWE2NDUwYmQ2/YTA2MDFhMDlkZDEx/NTAzZGNiNi90d2l0/dGVyLmNvbS8",
"path": " Python_Dv status 1786763460992544791"
},
"age": "20 hours ago"
},
{
"title": "input table name? - python script - KNIME Extensions - KNIME Community Forum",
"url": "https://forum.knime.com/t/input-table-name-python-script/78978",
"is_source_local": false,
"is_source_both": false,
"description": "Hi, when running a <strong>python</strong> script node, I get the error seen on the screenshot Same happens with this code too: The script input is output from the csv reader node. How can I get the right name for that table? Best wishes, Dario",
"page_age": "2024-05-04T09:20:44",
"profile": {
"name": "Knime",
"url": "https://forum.knime.com/t/input-table-name-python-script/78978",
"long_name": "forum.knime.com",
"img": "https://imgs.search.brave.com/WQoOhAD5i6uEhJ-qXvlWMJwbGA52f2Ycc_ns36EK698/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTAxNzMxNjFl/MzJjNzU5NzRkOTMz/Mjg4NDU2OWUxM2Rj/YzVkOGM3MzIwNzI2/YTY1NzYxNzA1MDE5/NzQzOWU3NC9mb3J1/bS5rbmltZS5jb20v"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "article",
"meta_url": {
"scheme": "https",
"netloc": "forum.knime.com",
"hostname": "forum.knime.com",
"favicon": "https://imgs.search.brave.com/WQoOhAD5i6uEhJ-qXvlWMJwbGA52f2Ycc_ns36EK698/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTAxNzMxNjFl/MzJjNzU5NzRkOTMz/Mjg4NDU2OWUxM2Rj/YzVkOGM3MzIwNzI2/YTY1NzYxNzA1MDE5/NzQzOWU3NC9mb3J1/bS5rbmltZS5jb20v",
"path": " knime extensions"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/DtEl38dcvuM1kGfhN0T5HfOrsMJcztWNyriLvtDJmKI/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9mb3J1/bS1jZG4ua25pbWUu/Y29tL3VwbG9hZHMv/ZGVmYXVsdC9vcmln/aW5hbC8zWC9lLzYv/ZTY0M2M2NzFlNzAz/MDg2MjkwMWY2YzJh/OWFjOWI5ZmEwM2M3/ZjMwZi5wbmc",
"original": "https://forum-cdn.knime.com/uploads/default/original/3X/e/6/e643c671e7030862901f6c2a9ac9b9fa03c7f30f.png",
"logo": false
},
"age": "1 day ago",
"extra_snippets": [
"Hi, when running a python script node, I get the error seen on the screenshot Same happens with this code too: The script input is output from the csv reader node. How can I get the right name for that table? …"
]
},
{
"title": "What does the Double Star operator mean in Python? - GeeksforGeeks",
"url": "https://www.geeksforgeeks.org/what-does-the-double-star-operator-mean-in-python/",
"is_source_local": false,
"is_source_both": false,
"description": "A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.",
"page_age": "2023-03-14T17:15:04",
"profile": {
"name": "GeeksforGeeks",
"url": "https://www.geeksforgeeks.org/what-does-the-double-star-operator-mean-in-python/",
"long_name": "geeksforgeeks.org",
"img": "https://imgs.search.brave.com/fhzcfv5xltx6-YBvJI9RZgS7xZo0dPNaASsrB8YOsCs/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjBhOGQ3MmNi/ZWE5N2EwMmZjYzA1/ZTI0ZTFhMGUyMTE0/MGM0ZTBmMWZlM2Y2/Yzk2ODMxZTRhYTBi/NDdjYTE0OS93d3cu/Z2Vla3Nmb3JnZWVr/cy5vcmcv"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "article",
"meta_url": {
"scheme": "https",
"netloc": "geeksforgeeks.org",
"hostname": "www.geeksforgeeks.org",
"favicon": "https://imgs.search.brave.com/fhzcfv5xltx6-YBvJI9RZgS7xZo0dPNaASsrB8YOsCs/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjBhOGQ3MmNi/ZWE5N2EwMmZjYzA1/ZTI0ZTFhMGUyMTE0/MGM0ZTBmMWZlM2Y2/Yzk2ODMxZTRhYTBi/NDdjYTE0OS93d3cu/Z2Vla3Nmb3JnZWVr/cy5vcmcv",
"path": " what-does-the-double-star-operator-mean-in-python"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/GcR-j_dLbyHkbHEI3ffLMi6xpXGhF_2Z8POIoqtokhM/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9tZWRp/YS5nZWVrc2Zvcmdl/ZWtzLm9yZy93cC1j/b250ZW50L3VwbG9h/ZHMvZ2ZnXzIwMFgy/MDAtMTAweDEwMC5w/bmc",
"original": "https://media.geeksforgeeks.org/wp-content/uploads/gfg_200X200-100x100.png",
"logo": false
},
"age": "March 14, 2023",
"extra_snippets": [
"Difference between / vs. // operator in Python",
"Double Star or (**) is one of the Arithmetic Operator (Like +, -, *, **, /, //, %) in Python Language. It is also known as Power Operator.",
"The time complexity of the given Python program is O(n), where n is the number of key-value pairs in the input dictionary.",
"Inplace Operators in Python | Set 2 (ixor(), iand(), ipow(),…)"
]
},
{
"title": "r/Python",
"url": "https://www.reddit.com/r/Python/",
"is_source_local": false,
"is_source_both": false,
"description": "The official <strong>Python</strong> community for Reddit! Stay up to date with the latest news, packages, and meta information relating to the <strong>Python</strong> programming language. --- If you have questions or are new to <strong>Python</strong> use r/LearnPython",
"page_age": "2022-12-30T16:25:02",
"profile": {
"name": "Reddit",
"url": "https://www.reddit.com/r/Python/",
"long_name": "reddit.com",
"img": "https://imgs.search.brave.com/mAZYEK9Wi13WLDUge7XZ8YuDTwm6DP6gBjvz1GdYZVY/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvN2ZiNTU0M2Nj/MTFhZjRiYWViZDlk/MjJiMjBjMzFjMDRk/Y2IzYWI0MGI0MjVk/OGY5NzQzOGQ5NzQ5/NWJhMWI0NC93d3cu/cmVkZGl0LmNvbS8"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "reddit.com",
"hostname": "www.reddit.com",
"favicon": "https://imgs.search.brave.com/mAZYEK9Wi13WLDUge7XZ8YuDTwm6DP6gBjvz1GdYZVY/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvN2ZiNTU0M2Nj/MTFhZjRiYWViZDlk/MjJiMjBjMzFjMDRk/Y2IzYWI0MGI0MjVk/OGY5NzQzOGQ5NzQ5/NWJhMWI0NC93d3cu/cmVkZGl0LmNvbS8",
"path": " r Python"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/zWd10t3zg34ciHiAB-K5WWK3h_H4LedeDot9BVX7Ydo/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9zdHls/ZXMucmVkZGl0bWVk/aWEuY29tL3Q1XzJx/aDB5L3N0eWxlcy9j/b21tdW5pdHlJY29u/X2NpZmVobDR4dDdu/YzEucG5n",
"original": "https://styles.redditmedia.com/t5_2qh0y/styles/communityIcon_cifehl4xt7nc1.png",
"logo": false
},
"age": "December 30, 2022",
"extra_snippets": [
"r/Python: The official Python community for Reddit! Stay up to date with the latest news, packages, and meta information relating to the Python…",
"By default, Python allows you to import and use anything, anywhere. Over time, this results in modules that were intended to be separate getting tightly coupled together, and domain boundaries breaking down. We experienced this first-hand at a unicorn startup, where the eng team paused development for over a year in an attempt to split up packages into independent services.",
"Hello r/Python! It's time to share what you've been working on! Whether it's a work-in-progress, a completed masterpiece, or just a rough idea, let us know what you're up to!",
"Whether it's your job, your hobby, or your passion project, all Python-related work is welcome here."
]
},
{
"title": "GitHub - python/cpython: The Python programming language",
"url": "https://github.com/python/cpython",
"is_source_local": false,
"is_source_both": false,
"description": "The <strong>Python</strong> programming language. Contribute to <strong>python</strong>/cpython development by creating an account on GitHub.",
"page_age": "2022-10-29T00:00:00",
"profile": {
"name": "GitHub",
"url": "https://github.com/python/cpython",
"long_name": "github.com",
"img": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "software",
"meta_url": {
"scheme": "https",
"netloc": "github.com",
"hostname": "github.com",
"favicon": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw",
"path": " python cpython"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/BJbWFRUqgP-tKIyGK9ByXjuYjHO2mtYigUOEFNz_gXk/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9vcGVu/Z3JhcGguZ2l0aHVi/YXNzZXRzLmNvbS82/MTY5YmJkNTQ0YzAy/NDg0MGU4NDdjYTU1/YTU3ZGZmMDA2ZDAw/YWQ1NDIzOTFmYTQ3/YmJjODg3OWM0NWYw/MTZhL3B5dGhvbi9j/cHl0aG9u",
"original": "https://opengraph.githubassets.com/6169bbd544c024840e847ca55a57dff006d00ad542391fa47bbc8879c45f016a/python/cpython",
"logo": false
},
"age": "October 29, 2022",
"extra_snippets": [
"You can pass many options to the configure script; run ./configure --help to find out more. On macOS case-insensitive file systems and on Cygwin, the executable is called python.exe; elsewhere it's just python.",
"Building a complete Python installation requires the use of various additional third-party libraries, depending on your build platform and configure options. Not all standard library modules are buildable or useable on all platforms. Refer to the Install dependencies section of the Developer Guide for current detailed information on dependencies for various Linux distributions and macOS.",
"To get an optimized build of Python, configure --enable-optimizations before you run make. This sets the default make targets up to enable Profile Guided Optimization (PGO) and may be used to auto-enable Link Time Optimization (LTO) on some platforms. For more details, see the sections below.",
"Copyright © 2001-2024 Python Software Foundation. All rights reserved."
]
},
{
"title": "5. Data Structures — Python 3.12.3 documentation",
"url": "https://docs.python.org/3/tutorial/datastructures.html",
"is_source_local": false,
"is_source_both": false,
"description": "This chapter describes some things youve learned about already in more detail, and adds some new things as well. More on Lists: The list data type has some more methods. Here are all of the method...",
"page_age": "2023-07-04T00:00:00",
"profile": {
"name": "Python documentation",
"url": "https://docs.python.org/3/tutorial/datastructures.html",
"long_name": "docs.python.org",
"img": "https://imgs.search.brave.com/F5Ym7eSElhGdGUFKLRxDj9Z_tc180ldpeMvQ2Q6ARbA/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMTUzOTFjOGVi/YTcyOTVmODA3ODIy/YjE2NzFjY2ViMjhl/NzRlY2JhYTc5YjNm/ZjhmODAyZWI2OGUw/ZjU4NDVlNy9kb2Nz/LnB5dGhvbi5vcmcv"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "docs.python.org",
"hostname": "docs.python.org",
"favicon": "https://imgs.search.brave.com/F5Ym7eSElhGdGUFKLRxDj9Z_tc180ldpeMvQ2Q6ARbA/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMTUzOTFjOGVi/YTcyOTVmODA3ODIy/YjE2NzFjY2ViMjhl/NzRlY2JhYTc5YjNm/ZjhmODAyZWI2OGUw/ZjU4NDVlNy9kb2Nz/LnB5dGhvbi5vcmcv",
"path": " 3 tutorial datastructures.html"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/Y7GrMRF8WorDIMLuOl97XC8ltYpoOCqNwWF2pQIIKls/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9kb2Nz/LnB5dGhvbi5vcmcv/My9fc3RhdGljL29n/LWltYWdlLnBuZw",
"original": "https://docs.python.org/3/_static/og-image.png",
"logo": false
},
"age": "July 4, 2023",
"extra_snippets": [
"You might have noticed that methods like insert, remove or sort that only modify the list have no return value printed they return the default None. [1] This is a design principle for all mutable data structures in Python.",
"We saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types — list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.",
"Python also includes a data type for sets. A set is an unordered collection with no duplicate elements. Basic uses include membership testing and eliminating duplicate entries. Set objects also support mathematical operations like union, intersection, difference, and symmetric difference.",
"Another useful data type built into Python is the dictionary (see Mapping Types — dict). Dictionaries are sometimes found in other languages as “associative memories” or “associative arrays”. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys."
]
},
{
"title": "Something wrong with python packages / AUR Issues, Discussion & PKGBUILD Requests / Arch Linux Forums",
"url": "https://bbs.archlinux.org/viewtopic.php?id=295466",
"is_source_local": false,
"is_source_both": false,
"description": "Big <strong>Python</strong> updates require <strong>Python</strong> packages to be rebuild. For some reason they didn&#x27;t think a bump that made it necessary to rebuild half the official repo was a news post.",
"page_age": "2024-05-04T08:30:02",
"profile": {
"name": "Archlinux",
"url": "https://bbs.archlinux.org/viewtopic.php?id=295466",
"long_name": "bbs.archlinux.org",
"img": "https://imgs.search.brave.com/3au9oqkzSri_aLEec3jo-0bFgLuICkydrWfjFcC8lkI/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNWNkODM1MWJl/ZmJhMzkzNzYzMDkz/NmEyMWMxNjI5MjNk/NGJmZjFhNTBlZDNl/Mzk5MzJjOGZkYjZl/MjNmY2IzNS9iYnMu/YXJjaGxpbnV4Lm9y/Zy8"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "bbs.archlinux.org",
"hostname": "bbs.archlinux.org",
"favicon": "https://imgs.search.brave.com/3au9oqkzSri_aLEec3jo-0bFgLuICkydrWfjFcC8lkI/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNWNkODM1MWJl/ZmJhMzkzNzYzMDkz/NmEyMWMxNjI5MjNk/NGJmZjFhNTBlZDNl/Mzk5MzJjOGZkYjZl/MjNmY2IzNS9iYnMu/YXJjaGxpbnV4Lm9y/Zy8",
"path": " viewtopic.php"
},
"age": "1 day ago",
"extra_snippets": [
"Traceback (most recent call last): File \"/usr/lib/python3.12/importlib/metadata/__init__.py\", line 397, in from_name return next(cls.discover(name=name)) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ StopIteration During handling of the above exception, another exception occurred: Traceback (most recent call last): File \"/usr/bin/informant\", line 33, in <module> sys.exit(load_entry_point('informant==0.5.0', 'console_scripts', 'informant')()) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File \"/usr/bin/informant\", line 22, in importlib_load_entry_point for entry_point in distribution(dis"
]
},
{
"title": "Introduction to Python",
"url": "https://www.w3schools.com/python/python_intro.asp",
"is_source_local": false,
"is_source_both": false,
"description": "W3Schools offers free online tutorials, references and exercises in all the major languages of the web. Covering popular subjects like HTML, CSS, JavaScript, <strong>Python</strong>, SQL, Java, and many, many more.",
"profile": {
"name": "W3Schools",
"url": "https://www.w3schools.com/python/python_intro.asp",
"long_name": "w3schools.com",
"img": "https://imgs.search.brave.com/JwO5r7z3HTBkU29vgNH_4rrSWLf2M4-8FMWNvbxrKX8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjVlMGVkZDVj/ZGMyZWRmMzAwODRi/ZDAwZGE4NWI3NmU4/MjRhNjEzOGFhZWY3/ZGViMjY1OWY2ZDYw/YTZiOGUyZS93d3cu/dzNzY2hvb2xzLmNv/bS8"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "w3schools.com",
"hostname": "www.w3schools.com",
"favicon": "https://imgs.search.brave.com/JwO5r7z3HTBkU29vgNH_4rrSWLf2M4-8FMWNvbxrKX8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjVlMGVkZDVj/ZGMyZWRmMzAwODRi/ZDAwZGE4NWI3NmU4/MjRhNjEzOGFhZWY3/ZGViMjY1OWY2ZDYw/YTZiOGUyZS93d3cu/dzNzY2hvb2xzLmNv/bS8",
"path": " python python_intro.asp"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/EMfp8dodbJehmj0yCJh8317RHuaumsddnHI4bujvFcg/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/dzNzY2hvb2xzLmNv/bS9pbWFnZXMvdzNz/Y2hvb2xzX2xvZ29f/NDM2XzIucG5n",
"original": "https://www.w3schools.com/images/w3schools_logo_436_2.png",
"logo": true
},
"extra_snippets": [
"Well organized and easy to understand Web building tutorials with lots of examples of how to use HTML, CSS, JavaScript, SQL, Python, PHP, Bootstrap, Java, XML and more.",
"HTML CSS JAVASCRIPT SQL PYTHON JAVA PHP HOW TO W3.CSS C C++ C# BOOTSTRAP REACT MYSQL JQUERY EXCEL XML DJANGO NUMPY PANDAS NODEJS R TYPESCRIPT ANGULAR GIT POSTGRESQL MONGODB ASP AI GO KOTLIN SASS VUE DSA GEN AI SCIPY AWS CYBERSECURITY DATA SCIENCE",
"Python Variables Variable Names Assign Multiple Values Output Variables Global Variables Variable Exercises Python Data Types Python Numbers Python Casting Python Strings",
"Python Strings Slicing Strings Modify Strings Concatenate Strings Format Strings Escape Characters String Methods String Exercises Python Booleans Python Operators Python Lists"
]
},
{
"title": "bug: AUR package wants to use python but does not find any preset version · Issue #1740 · asdf-vm/asdf",
"url": "https://github.com/asdf-vm/asdf/issues/1740",
"is_source_local": false,
"is_source_both": false,
"description": "Describe the Bug I am not sure why this is happening, I am trying to install tlpui from AUR and it fails, here are some logs to help: ==&gt; Making package: tlpui 2:1.6.5-1 (Mi 10 apr 2024 23:19:15 +0...",
"page_age": "2024-05-04T06:45:04",
"profile": {
"name": "GitHub",
"url": "https://github.com/asdf-vm/asdf/issues/1740",
"long_name": "github.com",
"img": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "software",
"meta_url": {
"scheme": "https",
"netloc": "github.com",
"hostname": "github.com",
"favicon": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw",
"path": " asdf-vm asdf issues 1740"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/KrLW5s_2n4jyP8XLbc3ZPVBaLD963tQgWzG9EWPZlQs/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9vcGVu/Z3JhcGguZ2l0aHVi/YXNzZXRzLmNvbS81/MTE0ZTdkOGIwODM2/YmQ2MTY3NzQ1ZGI4/MmZjMGE3OGUyMjcw/MGFlY2ZjMWZkODBl/MDYzZTNiN2ZjOWNj/NzYyL2FzZGYtdm0v/YXNkZi9pc3N1ZXMv/MTc0MA",
"original": "https://opengraph.githubassets.com/5114e7d8b0836bd6167745db82fc0a78e22700aecfc1fd80e063e3b7fc9cc762/asdf-vm/asdf/issues/1740",
"logo": false
},
"age": "1 day ago",
"extra_snippets": [
"==> Starting build()... No preset version installed for command python Please install a version by running one of the following: asdf install python 3.8 or add one of the following versions in your config file at /home/ferret/.tool-versions python 3.11.0 python 3.12.1 python 3.12.3 ==> ERROR: A failure occurred in build(). Aborting...",
"-> error making: tlpui-exit status 4 -> Failed to install the following packages. Manual intervention is required: tlpui - exit status 4 ferret@FX505DT in ~ $ cat /home/ferret/.tool-versions nodejs 21.6.0 python 3.12.3 ferret@FX505DT in ~ $ python -V Python 3.12.3 ferret@FX505DT in ~ $ which python /home/ferret/.asdf/shims/python",
"Describe the Bug I am not sure why this is happening, I am trying to install tlpui from AUR and it fails, here are some logs to help: ==> Making package: tlpui 2:1.6.5-1 (Mi 10 apr 2024 23:19:15 +0300) ==> Retrieving sources... -> Found ..."
]
},
{
"title": "What are python.exe and python3.exe, and why do they appear to point to App Installer? | Windows 11 Forum",
"url": "https://www.elevenforum.com/t/what-are-python-exe-and-python3-exe-and-why-do-they-appear-to-point-to-app-installer.24886/",
"is_source_local": false,
"is_source_both": false,
"description": "I was looking at App execution aliases (Settings &gt; Apps &gt; Advanced app settings &gt; App execution aliases) on my new computer -- my first Windows 11 computer. Why are <strong>python</strong>.exe and python3.exe listed as App Installer? I assume that App Installer refers to installation of Microsoft Store / UWP...",
"page_age": "2024-05-03T17:30:04",
"profile": {
"name": "Windows 11 Forum",
"url": "https://www.elevenforum.com/t/what-are-python-exe-and-python3-exe-and-why-do-they-appear-to-point-to-app-installer.24886/",
"long_name": "elevenforum.com",
"img": "https://imgs.search.brave.com/XVRAYMEj6Im8i7jV5RxeTwpiRPtY9IWg4wRIuh-WhEw/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZjk5MDZkMDIw/M2U1OWIwNjM5Y2U1/M2U2NzNiNzVkNTA5/NzA5OTI1ZTFmOTc4/MzU3OTlhYzU5OTVi/ZGNjNTY4MS93d3cu/ZWxldmVuZm9ydW0u/Y29tLw"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "elevenforum.com",
"hostname": "www.elevenforum.com",
"favicon": "https://imgs.search.brave.com/XVRAYMEj6Im8i7jV5RxeTwpiRPtY9IWg4wRIuh-WhEw/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZjk5MDZkMDIw/M2U1OWIwNjM5Y2U1/M2U2NzNiNzVkNTA5/NzA5OTI1ZTFmOTc4/MzU3OTlhYzU5OTVi/ZGNjNTY4MS93d3cu/ZWxldmVuZm9ydW0u/Y29tLw",
"path": " windows support forums apps and software"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/DVoFcE6d_-lx3BVGNS-RZK_lZzxQ8VhwZVf3AVqEJFA/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/ZWxldmVuZm9ydW0u/Y29tL2RhdGEvYXNz/ZXRzL2xvZ28vbWV0/YTEtMjAxLnBuZw",
"original": "https://www.elevenforum.com/data/assets/logo/meta1-201.png",
"logo": true
},
"age": "2 days ago",
"extra_snippets": [
"Why are python.exe and python3.exe listed as App Installer? I assume that App Installer refers to installation of Microsoft Store / UWP apps, but if that's the case, then why are they called python.exe and python3.exe? Or are python.exe and python3.exe simply serving as aliases / pointers pointing to App Installer, which is itself a Microsoft Store App?",
"Or are python.exe and python3.exe simply serving as aliases / pointers pointing to App Installer, which is itself a Microsoft Store App? I wish to soon install Python, along with an integrated development editor (IDE), on my machine, so that I can code in Python.",
"I wish to soon install Python, along with an integrated development editor (IDE), on my machine, so that I can code in Python. But is a Python interpreter already on my computer as suggested, if obliquely, by the presence of python.exe and python3.exe? I kind of doubt it."
]
},
{
"title": "How to Watermark Your Images Using Python OpenCV in ...",
"url": "https://medium.com/@daily_data_prep/how-to-watermark-your-images-using-python-opencv-in-bulk-e472085389a1",
"is_source_local": false,
"is_source_both": false,
"description": "Medium is an open platform where readers find dynamic thinking, and where expert and undiscovered voices can share their writing on any topic.",
"page_age": "2024-05-03T14:05:06",
"profile": {
"name": "Medium",
"url": "https://medium.com/@daily_data_prep/how-to-watermark-your-images-using-python-opencv-in-bulk-e472085389a1",
"long_name": "medium.com",
"img": "https://imgs.search.brave.com/qvE2kIQCiAsnPv2C6P9xM5J2VVWdm55g-A-2Q_yIJ0g/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTZhYmQ1N2Q4/NDg4ZDcyODIyMDZi/MzFmOWNhNjE3Y2E4/Y2YzMThjNjljNDIx/ZjllZmNhYTcwODhl/YTcwNDEzYy9tZWRp/dW0uY29tLw"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "medium.com",
"hostname": "medium.com",
"favicon": "https://imgs.search.brave.com/qvE2kIQCiAsnPv2C6P9xM5J2VVWdm55g-A-2Q_yIJ0g/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTZhYmQ1N2Q4/NDg4ZDcyODIyMDZi/MzFmOWNhNjE3Y2E4/Y2YzMThjNjljNDIx/ZjllZmNhYTcwODhl/YTcwNDEzYy9tZWRp/dW0uY29tLw",
"path": " @daily_data_prep how-to-watermark-your-images-using-python-opencv-in-bulk-e472085389a1"
},
"age": "2 days ago"
},
{
"title": "Increment and Decrement Operators in Python?",
"url": "https://www.tutorialspoint.com/increment-and-decrement-operators-in-python",
"is_source_local": false,
"is_source_both": false,
"description": "Increment and Decrement Operators in <strong>Python</strong> - <strong>Python</strong> does not have unary increment/decrement operator (++/--). Instead to increment a value, usea += 1to decrement a value, use a -= 1Example&gt;&gt;&gt; a = 0 &gt;&gt;&gt; &gt;&gt;&gt; #Increment &gt;&gt;&gt; a +=1 &gt;&gt;&gt; &gt;&gt;&gt; #Decrement &gt;&gt;&gt; a -= 1 &gt;&gt;&gt; &gt;&gt;&gt; #value of a &gt;&gt;&gt; a 0Python ...",
"page_age": "2023-08-23T00:00:00",
"profile": {
"name": "Tutorialspoint",
"url": "https://www.tutorialspoint.com/increment-and-decrement-operators-in-python",
"long_name": "tutorialspoint.com",
"img": "https://imgs.search.brave.com/Wt8BSkivPlFwcU5yBtf7YzuvTuRExyd_502cdABCS5c/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjcyYjAzYmVl/ODU4MzZiMjJiYTFh/MjJhZDNmNWE4YzA5/MDgyYTZhMDg3NTYw/M2NiY2NiZTUxN2I5/MjU1MWFmMS93d3cu/dHV0b3JpYWxzcG9p/bnQuY29tLw"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "tutorialspoint.com",
"hostname": "www.tutorialspoint.com",
"favicon": "https://imgs.search.brave.com/Wt8BSkivPlFwcU5yBtf7YzuvTuRExyd_502cdABCS5c/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjcyYjAzYmVl/ODU4MzZiMjJiYTFh/MjJhZDNmNWE4YzA5/MDgyYTZhMDg3NTYw/M2NiY2NiZTUxN2I5/MjU1MWFmMS93d3cu/dHV0b3JpYWxzcG9p/bnQuY29tLw",
"path": " increment-and-decrement-operators-in-python"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/ddG5vyZGLVudvecEbQJPeG8tGuaZ7g3Xz6Gyjdl5WA8/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/dHV0b3JpYWxzcG9p/bnQuY29tL2ltYWdl/cy90cF9sb2dvXzQz/Ni5wbmc",
"original": "https://www.tutorialspoint.com/images/tp_logo_436.png",
"logo": true
},
"age": "August 23, 2023",
"extra_snippets": [
"Increment and Decrement Operators in Python - Python does not have unary increment/decrement operator (++/--). Instead to increment a value, usea += 1to decrement a value, use a -= 1Example>>> a = 0 >>> >>> #Increment >>> a +=1 >>> >>> #Decrement >>> a -= 1 >>> >>> #value of a >>> a 0Python does not provide multiple ways to do the same thing",
"So what above statement means in python is: create an object of type int having value 1 and give the name a to it. The object is an instance of int having value 1 and the name a refers to it. The assigned name a and the object to which it refers are distinct.",
"Python does not provide multiple ways to do the same thing .",
"However, be careful if you are coming from a language like C, Python doesnt have \"variables\" in the sense that C does, instead python uses names and objects and in python integers (ints) are immutable."
]
},
{
"title": "Gumroad How not to suck at Python / SideFX Houdini | CG Persia",
"url": "https://cgpersia.com/2024/05/gumroad-how-not-to-suck-at-python-sidefx-houdini-195370.html",
"is_source_local": false,
"is_source_both": false,
"description": "Info: This course is made for artists or TD (technical director) willing to learn <strong>Python</strong> to improve their workflows inside SideFX Houdini, get faster in production and develop all the tools you always wished you had.",
"page_age": "2024-05-03T08:35:03",
"profile": {
"name": "Cgpersia",
"url": "https://cgpersia.com/2024/05/gumroad-how-not-to-suck-at-python-sidefx-houdini-195370.html",
"long_name": "cgpersia.com",
"img": "https://imgs.search.brave.com/VjyaopAm-M9sWvM7n-KnGZ3T5swIOwwE80iF5QVqQPg/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYmE0MzQ4NmI2/NjFhMTA1ZDBiN2Iw/ZWNiNDUxNjUwYjdh/MGE5ZjQ0ZjIxNzll/NmVkZDE2YzYyMDBh/NDNiMDgwMy9jZ3Bl/cnNpYS5jb20v"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "cgpersia.com",
"hostname": "cgpersia.com",
"favicon": "https://imgs.search.brave.com/VjyaopAm-M9sWvM7n-KnGZ3T5swIOwwE80iF5QVqQPg/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYmE0MzQ4NmI2/NjFhMTA1ZDBiN2Iw/ZWNiNDUxNjUwYjdh/MGE5ZjQ0ZjIxNzll/NmVkZDE2YzYyMDBh/NDNiMDgwMy9jZ3Bl/cnNpYS5jb20v",
"path": " 2024 05 gumroad-how-not-to-suck-at-python-sidefx-houdini-195370.html"
},
"age": "2 days ago",
"extra_snippets": [
"Posted in: 2D, CG Releases, Downloads, Learning, Tutorials, Videos. Tagged: Gumroad, Python, Sidefx. Leave a Comment",
"01 Python Fundamentals Get the Fundamentals of python before starting the fun stuff ! 02 Python Construction Part02 digging further into python concepts 03 Houdini Python Basics Applying some basic python in Houdini and starting to make tools !",
"02 Python Construction Part02 digging further into python concepts 03 Houdini Python Basics Applying some basic python in Houdini and starting to make tools ! 04 Houdini Python Intermediate Applying some more advanced python in Houdini to make tools ! 05 Houdini Python Expert Using QtDesigner in combinaison with Houdini Python/Pyside to create advanced tools."
]
},
{
"title": "How to install Python: The complete Python programmers guide",
"url": "https://www.pluralsight.com/resources/blog/software-development/python-installation-guide",
"is_source_local": false,
"is_source_both": false,
"description": "An easy guide on how set up your operating system so you can program in <strong>Python</strong>, and how to update or uninstall it. For Linux, Windows, and macOS.",
"page_age": "2024-05-02T07:30:02",
"profile": {
"name": "Pluralsight",
"url": "https://www.pluralsight.com/resources/blog/software-development/python-installation-guide",
"long_name": "pluralsight.com",
"img": "https://imgs.search.brave.com/zvwQNSVu9-jR2CRlNcsTzxjaXKPlXNuh-Jo9-0yA1OE/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMTNkNWQyNjk3/M2Q0NzYyMmUyNDc3/ZjYwMWFlZDI5YTI4/ODhmYzc2MDkzMjAy/MjNkMWY1MDE3NTQw/MzI5NWVkZS93d3cu/cGx1cmFsc2lnaHQu/Y29tLw"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "pluralsight.com",
"hostname": "www.pluralsight.com",
"favicon": "https://imgs.search.brave.com/zvwQNSVu9-jR2CRlNcsTzxjaXKPlXNuh-Jo9-0yA1OE/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMTNkNWQyNjk3/M2Q0NzYyMmUyNDc3/ZjYwMWFlZDI5YTI4/ODhmYzc2MDkzMjAy/MjNkMWY1MDE3NTQw/MzI5NWVkZS93d3cu/cGx1cmFsc2lnaHQu/Y29tLw",
"path": " blog blog"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/xrv5PHH2Bzmq2rcIYzk__8h5RqCj6kS3I6SGCNw5dZM/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/cGx1cmFsc2lnaHQu/Y29tL2NvbnRlbnQv/ZGFtL3BzL2ltYWdl/cy9yZXNvdXJjZS1j/ZW50ZXIvYmxvZy9o/ZWFkZXItaGVyby1p/bWFnZXMvUHl0aG9u/LndlYnA",
"original": "https://www.pluralsight.com/content/dam/ps/images/resource-center/blog/header-hero-images/Python.webp",
"logo": false
},
"age": "3 days ago",
"extra_snippets": [
"Whether its your first time programming or youre a seasoned programmer, youll have to install or update Python every now and then --- or if necessary, uninstall it. In this article, you'll learn how to do just that.",
"Some systems come with Python, so to start off, well first check to see if its installed on your system before we proceed. To do that, well need to open a terminal. Since you might be new to programming, lets go over how to open a terminal for Linux, Windows, and macOS.",
"Before we dive into setting up your system so you can program in Python, lets talk terminal basics and benefits.",
"However, lets focus on why we need it for working with Python. We use a terminal, or command line, to:"
]
}
],
"family_friendly": true
}
}

View File

@@ -0,0 +1,442 @@
{
"kind": "customsearch#search",
"url": {
"type": "application/json",
"template": "https://www.googleapis.com/customsearch/v1?q={searchTerms}&num={count?}&start={startIndex?}&lr={language?}&safe={safe?}&cx={cx?}&sort={sort?}&filter={filter?}&gl={gl?}&cr={cr?}&googlehost={googleHost?}&c2coff={disableCnTwTranslation?}&hq={hq?}&hl={hl?}&siteSearch={siteSearch?}&siteSearchFilter={siteSearchFilter?}&exactTerms={exactTerms?}&excludeTerms={excludeTerms?}&linkSite={linkSite?}&orTerms={orTerms?}&dateRestrict={dateRestrict?}&lowRange={lowRange?}&highRange={highRange?}&searchType={searchType}&fileType={fileType?}&rights={rights?}&imgSize={imgSize?}&imgType={imgType?}&imgColorType={imgColorType?}&imgDominantColor={imgDominantColor?}&alt=json"
},
"queries": {
"request": [
{
"title": "Google Custom Search - lectures",
"totalResults": "2450000000",
"searchTerms": "lectures",
"count": 10,
"startIndex": 1,
"inputEncoding": "utf8",
"outputEncoding": "utf8",
"safe": "off",
"cx": "0473ef98502d44e18"
}
],
"nextPage": [
{
"title": "Google Custom Search - lectures",
"totalResults": "2450000000",
"searchTerms": "lectures",
"count": 10,
"startIndex": 11,
"inputEncoding": "utf8",
"outputEncoding": "utf8",
"safe": "off",
"cx": "0473ef98502d44e18"
}
]
},
"context": {
"title": "LLM Search"
},
"searchInformation": {
"searchTime": 0.445959,
"formattedSearchTime": "0.45",
"totalResults": "2450000000",
"formattedTotalResults": "2,450,000,000"
},
"items": [
{
"kind": "customsearch#result",
"title": "The Feynman Lectures on Physics",
"htmlTitle": "The Feynman \u003cb\u003eLectures\u003c/b\u003e on Physics",
"link": "https://www.feynmanlectures.caltech.edu/",
"displayLink": "www.feynmanlectures.caltech.edu",
"snippet": "This edition has been designed for ease of reading on devices of any size or shape; text, figures and equations can all be zoomed without degradation.",
"htmlSnippet": "This edition has been designed for ease of reading on devices of any size or shape; text, figures and equations can all be zoomed without degradation.",
"cacheId": "CyXMWYWs9UEJ",
"formattedUrl": "https://www.feynmanlectures.caltech.edu/",
"htmlFormattedUrl": "https://www.feynman\u003cb\u003electures\u003c/b\u003e.caltech.edu/",
"pagemap": {
"metatags": [
{
"viewport": "width=device-width, initial-scale=1.0"
}
]
}
},
{
"kind": "customsearch#result",
"title": "Video Lectures",
"htmlTitle": "Video \u003cb\u003eLectures\u003c/b\u003e",
"link": "https://www.reddit.com/r/lectures/",
"displayLink": "www.reddit.com",
"snippet": "r/lectures: This subreddit is all about video lectures, talks and interesting public speeches. The topics include mathematics, physics, computer…",
"htmlSnippet": "r/\u003cb\u003electures\u003c/b\u003e: This subreddit is all about video \u003cb\u003electures\u003c/b\u003e, talks and interesting public speeches. The topics include mathematics, physics, computer…",
"formattedUrl": "https://www.reddit.com/r/lectures/",
"htmlFormattedUrl": "https://www.reddit.com/r/\u003cb\u003electures\u003c/b\u003e/",
"pagemap": {
"cse_thumbnail": [
{
"src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTZtOjhfkgUKQbL3DZxe5F6OVsgeDNffleObjJ7n9RllKQTSsimax7VIaY&s",
"width": "192",
"height": "192"
}
],
"metatags": [
{
"og:image": "https://www.redditstatic.com/shreddit/assets/favicon/192x192.png",
"theme-color": "#000000",
"og:image:width": "256",
"og:type": "website",
"twitter:card": "summary",
"twitter:title": "r/lectures",
"og:site_name": "Reddit",
"og:title": "r/lectures",
"og:image:height": "256",
"bingbot": "noarchive",
"msapplication-navbutton-color": "#000000",
"og:description": "This subreddit is all about video lectures, talks and interesting public speeches.\n\nThe topics include mathematics, physics, computer science, programming, engineering, biology, medicine, economics, politics, social sciences, and any other subjects!",
"twitter:image": "https://www.redditstatic.com/shreddit/assets/favicon/192x192.png",
"apple-mobile-web-app-status-bar-style": "black",
"twitter:site": "@reddit",
"viewport": "width=device-width, initial-scale=1, viewport-fit=cover",
"apple-mobile-web-app-capable": "yes",
"og:ttl": "600",
"og:url": "https://www.reddit.com/r/lectures/"
}
],
"cse_image": [
{
"src": "https://www.redditstatic.com/shreddit/assets/favicon/192x192.png"
}
]
}
},
{
"kind": "customsearch#result",
"title": "Lectures & Discussions | Flint Institute of Arts",
"htmlTitle": "\u003cb\u003eLectures\u003c/b\u003e &amp; Discussions | Flint Institute of Arts",
"link": "https://flintarts.org/events/lectures",
"displayLink": "flintarts.org",
"snippet": "It will trace the intricate relationship between jewelry, attire, and the expression of personal identity, social hierarchy, and spiritual belief systems that ...",
"htmlSnippet": "It will trace the intricate relationship between jewelry, attire, and the expression of personal identity, social hierarchy, and spiritual belief systems that&nbsp;...",
"cacheId": "jvpb9DxrfxoJ",
"formattedUrl": "https://flintarts.org/events/lectures",
"htmlFormattedUrl": "https://flintarts.org/events/\u003cb\u003electures\u003c/b\u003e",
"pagemap": {
"cse_thumbnail": [
{
"src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS23tMtAeNhJbOWdGxShYsmnyzFdzOC9Hb7lRykA9Pw72z1IlKTkjTdZw&s",
"width": "447",
"height": "113"
}
],
"metatags": [
{
"og:image": "https://flintarts.org/uploads/images/page-headers/_headerImage/nightshot.jpg",
"og:type": "website",
"viewport": "width=device-width, initial-scale=1",
"og:title": "Lectures & Discussions | Flint Institute of Arts",
"og:description": "The Flint Institute of Arts is the second largest art museum in Michigan and one of the largest museum art schools in the nation."
}
],
"cse_image": [
{
"src": "https://flintarts.org/uploads/images/page-headers/_headerImage/nightshot.jpg"
}
]
}
},
{
"kind": "customsearch#result",
"title": "Mandel Lectures | Mandel Center for the Humanities ... - Waltham",
"htmlTitle": "Mandel \u003cb\u003eLectures\u003c/b\u003e | Mandel Center for the Humanities ... - Waltham",
"link": "https://www.brandeis.edu/mandel-center-humanities/mandel-lectures.html",
"displayLink": "www.brandeis.edu",
"snippet": "Past Lectures · Lecture 1: \"Invisible Music: The Sonic Idea of Black Revolution From Captivity to Reconstruction\" · Lecture 2: \"Solidarity in Sound: Grassroots ...",
"htmlSnippet": "Past \u003cb\u003eLectures\u003c/b\u003e &middot; \u003cb\u003eLecture\u003c/b\u003e 1: &quot;Invisible Music: The Sonic Idea of Black Revolution From Captivity to Reconstruction&quot; &middot; \u003cb\u003eLecture\u003c/b\u003e 2: &quot;Solidarity in Sound: Grassroots&nbsp;...",
"cacheId": "cQLOZr0kgEEJ",
"formattedUrl": "https://www.brandeis.edu/mandel-center-humanities/mandel-lectures.html",
"htmlFormattedUrl": "https://www.brandeis.edu/mandel-center-humanities/mandel-\u003cb\u003electures\u003c/b\u003e.html",
"pagemap": {
"cse_thumbnail": [
{
"src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQWlU7bcJ5pIHk7RBCk2QKE-48ejF7hyPV0pr-20_cBt2BGdfKtiYXBuyw&s",
"width": "275",
"height": "183"
}
],
"metatags": [
{
"og:image": "https://www.brandeis.edu/mandel-center-humanities/events/events-images/mlhzumba",
"twitter:card": "summary_large_image",
"viewport": "width=device-width,initial-scale=1,minimum-scale=1",
"og:title": "Mandel Lectures in the Humanities",
"og:url": "https://www.brandeis.edu/mandel-center-humanities/mandel-lectures.html",
"og:description": "Annual Lecture Series",
"twitter:image": "https://www.brandeis.edu/mandel-center-humanities/events/events-images/mlhzumba"
}
],
"cse_image": [
{
"src": "https://www.brandeis.edu/mandel-center-humanities/events/events-images/mlhzumba"
}
]
}
},
{
"kind": "customsearch#result",
"title": "Brian Douglas - YouTube",
"htmlTitle": "Brian Douglas - YouTube",
"link": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
"displayLink": "www.youtube.com",
"snippet": "Welcome to Control Systems Lectures! This collection of videos is intended to supplement a first year controls class, not replace it.",
"htmlSnippet": "Welcome to Control Systems \u003cb\u003eLectures\u003c/b\u003e! This collection of videos is intended to supplement a first year controls class, not replace it.",
"cacheId": "NEROyBHolL0J",
"formattedUrl": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
"htmlFormattedUrl": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
"pagemap": {
"hcard": [
{
"fn": "Brian Douglas",
"url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg"
}
],
"cse_thumbnail": [
{
"src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR7G0CeCBz_wVTZgjnhEr2QbiKP7f3uYzKitZYn74Mi32cDmVxvsegJoLI&s",
"width": "225",
"height": "225"
}
],
"imageobject": [
{
"width": "900",
"url": "https://yt3.googleusercontent.com/ytc/AIdro_nLo68wetImbwGUYP3stve_iKmAEccjhqB-q4o79xdInN4=s900-c-k-c0x00ffffff-no-rj",
"height": "900"
}
],
"person": [
{
"name": "Brian Douglas",
"url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg"
}
],
"metatags": [
{
"apple-itunes-app": "app-id=544007664, app-argument=https://m.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg?referring_app=com.apple.mobilesafari-smartbanner, affiliate-data=ct=smart_app_banner_polymer&pt=9008",
"og:image": "https://yt3.googleusercontent.com/ytc/AIdro_nLo68wetImbwGUYP3stve_iKmAEccjhqB-q4o79xdInN4=s900-c-k-c0x00ffffff-no-rj",
"twitter:app:url:iphone": "vnd.youtube://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
"twitter:app:id:googleplay": "com.google.android.youtube",
"theme-color": "rgb(255, 255, 255)",
"og:image:width": "900",
"twitter:card": "summary",
"og:site_name": "YouTube",
"twitter:url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
"twitter:app:url:ipad": "vnd.youtube://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
"al:android:package": "com.google.android.youtube",
"twitter:app:name:googleplay": "YouTube",
"al:ios:url": "vnd.youtube://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
"twitter:app:id:iphone": "544007664",
"og:description": "Welcome to Control Systems Lectures! This collection of videos is intended to supplement a first year controls class, not replace it. My goal is to take specific concepts in controls and expand on them in order to provide an intuitive understanding which will ultimately make you a better controls engineer. \n\nI'm glad you made it to my channel and I hope you find it useful.\n\nShoot me a message at controlsystemlectures@gmail.com, leave a comment or question and I'll get back to you if I can. Don't forget to subscribe!\n \nTwitter: @BrianBDouglas for engineering tweets and announcement of new videos.\nWebpage: http://engineeringmedia.com\n\nHere is the hardware/software I use: http://www.youtube.com/watch?v=m-M5_mIyHe4\n\nHere's a list of my favorite references: http://bit.ly/2skvmWd\n\n--Brian",
"al:ios:app_store_id": "544007664",
"twitter:image": "https://yt3.googleusercontent.com/ytc/AIdro_nLo68wetImbwGUYP3stve_iKmAEccjhqB-q4o79xdInN4=s900-c-k-c0x00ffffff-no-rj",
"twitter:site": "@youtube",
"og:type": "profile",
"twitter:title": "Brian Douglas",
"al:ios:app_name": "YouTube",
"og:title": "Brian Douglas",
"og:image:height": "900",
"twitter:app:id:ipad": "544007664",
"al:web:url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg?feature=applinks",
"al:android:url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg?feature=applinks",
"fb:app_id": "87741124305",
"twitter:app:url:googleplay": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
"twitter:app:name:ipad": "YouTube",
"viewport": "width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no,",
"twitter:description": "Welcome to Control Systems Lectures! This collection of videos is intended to supplement a first year controls class, not replace it. My goal is to take specific concepts in controls and expand on them in order to provide an intuitive understanding which will ultimately make you a better controls engineer. \n\nI'm glad you made it to my channel and I hope you find it useful.\n\nShoot me a message at controlsystemlectures@gmail.com, leave a comment or question and I'll get back to you if I can. Don't forget to subscribe!\n \nTwitter: @BrianBDouglas for engineering tweets and announcement of new videos.\nWebpage: http://engineeringmedia.com\n\nHere is the hardware/software I use: http://www.youtube.com/watch?v=m-M5_mIyHe4\n\nHere's a list of my favorite references: http://bit.ly/2skvmWd\n\n--Brian",
"og:url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
"al:android:app_name": "YouTube",
"twitter:app:name:iphone": "YouTube"
}
],
"cse_image": [
{
"src": "https://yt3.googleusercontent.com/ytc/AIdro_nLo68wetImbwGUYP3stve_iKmAEccjhqB-q4o79xdInN4=s900-c-k-c0x00ffffff-no-rj"
}
]
}
},
{
"kind": "customsearch#result",
"title": "Lecture - Wikipedia",
"htmlTitle": "\u003cb\u003eLecture\u003c/b\u003e - Wikipedia",
"link": "https://en.wikipedia.org/wiki/Lecture",
"displayLink": "en.wikipedia.org",
"snippet": "Lecture ... For the academic rank, see Lecturer. A lecture (from Latin: lēctūra 'reading') is an oral presentation intended to present information or teach people ...",
"htmlSnippet": "\u003cb\u003eLecture\u003c/b\u003e ... For the academic rank, see \u003cb\u003eLecturer\u003c/b\u003e. A \u003cb\u003electure\u003c/b\u003e (from Latin: lēctūra &#39;reading&#39;) is an oral presentation intended to present information or teach people&nbsp;...",
"cacheId": "d9Pjta02fmgJ",
"formattedUrl": "https://en.wikipedia.org/wiki/Lecture",
"htmlFormattedUrl": "https://en.wikipedia.org/wiki/Lecture",
"pagemap": {
"metatags": [
{
"referrer": "origin",
"og:image": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/26/ADFA_Lecture_Theatres.jpg/1200px-ADFA_Lecture_Theatres.jpg",
"theme-color": "#eaecf0",
"og:image:width": "1200",
"og:type": "website",
"viewport": "width=device-width, initial-scale=1.0, user-scalable=yes, minimum-scale=0.25, maximum-scale=5.0",
"og:title": "Lecture - Wikipedia",
"og:image:height": "799",
"format-detection": "telephone=no"
}
]
}
},
{
"kind": "customsearch#result",
"title": "Mount Wilson Observatory | Lectures",
"htmlTitle": "Mount Wilson Observatory | \u003cb\u003eLectures\u003c/b\u003e",
"link": "https://www.mtwilson.edu/lectures/",
"displayLink": "www.mtwilson.edu",
"snippet": "Talks & Telescopes: August 24, 2024 Panel: The Triumph of Hubble ... Compelling talks followed by picnicking and convivial stargazing through both the big ...",
"htmlSnippet": "Talks &amp; Telescopes: August 24, 2024 Panel: The Triumph of Hubble ... Compelling talks followed by picnicking and convivial stargazing through both the big&nbsp;...",
"cacheId": "wdXI0azqx5UJ",
"formattedUrl": "https://www.mtwilson.edu/lectures/",
"htmlFormattedUrl": "https://www.mtwilson.edu/\u003cb\u003electures\u003c/b\u003e/",
"pagemap": {
"metatags": [
{
"viewport": "width=device-width,initial-scale=1,user-scalable=no"
}
],
"webpage": [
{
"image": "http://www.mtwilson.edu/wp-content/uploads/2016/09/Logo.jpg",
"url": "https://www.facebook.com/WilsonObs"
}
]
}
},
{
"kind": "customsearch#result",
"title": "Lectures | NBER",
"htmlTitle": "\u003cb\u003eLectures\u003c/b\u003e | NBER",
"link": "https://www.nber.org/research/lectures",
"displayLink": "www.nber.org",
"snippet": "Results 1 - 50 of 354 ... Among featured events at the NBER Summer Institute are the Martin Feldstein Lecture, which examines a current issue involving economic ...",
"htmlSnippet": "Results 1 - 50 of 354 \u003cb\u003e...\u003c/b\u003e Among featured events at the NBER Summer Institute are the Martin Feldstein \u003cb\u003eLecture\u003c/b\u003e, which examines a current issue involving economic&nbsp;...",
"cacheId": "CvvP3U3nb44J",
"formattedUrl": "https://www.nber.org/research/lectures",
"htmlFormattedUrl": "https://www.nber.org/research/\u003cb\u003electures\u003c/b\u003e",
"pagemap": {
"cse_thumbnail": [
{
"src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTmeViEZyV1YmFEFLhcA6WdgAG3v3RV6tB93ncyxSJ5JPst_p2aWrL7D1k&s",
"width": "310",
"height": "163"
}
],
"metatags": [
{
"og:image": "https://www.nber.org/sites/default/files/2022-06/NBER-FB-Share-Tile-1200.jpg",
"og:site_name": "NBER",
"handheldfriendly": "true",
"viewport": "width=device-width, initial-scale=1.0",
"og:title": "Lectures",
"mobileoptimized": "width",
"og:url": "https://www.nber.org/research/lectures"
}
],
"cse_image": [
{
"src": "https://www.nber.org/sites/default/files/2022-06/NBER-FB-Share-Tile-1200.jpg"
}
]
}
},
{
"kind": "customsearch#result",
"title": "STUDENTS CANNOT ACCESS RECORDED LECTURES ... - Solved",
"htmlTitle": "STUDENTS CANNOT ACCESS RECORDED LECTURES ... - Solved",
"link": "https://community.canvaslms.com/t5/Canvas-Question-Forum/STUDENTS-CANNOT-ACCESS-RECORDED-LECTURES/td-p/190358",
"displayLink": "community.canvaslms.com",
"snippet": "Mar 19, 2020 ... I believe the issue is that students were not invited. Are you trying to capture your screen? If not, there is an option to just record your web ...",
"htmlSnippet": "Mar 19, 2020 \u003cb\u003e...\u003c/b\u003e I believe the issue is that students were not invited. Are you trying to capture your screen? If not, there is an option to just record your web&nbsp;...",
"cacheId": "wqrynQXX61sJ",
"formattedUrl": "https://community.canvaslms.com/t5/Canvas...LECTURES/td-p/190358",
"htmlFormattedUrl": "https://community.canvaslms.com/t5/Canvas...\u003cb\u003eLECTURES\u003c/b\u003e/td-p/190358",
"pagemap": {
"cse_thumbnail": [
{
"src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRUqXau3N8LfKgSD7OJOvV7xzGarLKRU-ckWXy1ZQ1p4CLPsedvLKmLMhk&s",
"width": "310",
"height": "163"
}
],
"metatags": [
{
"og:image": "https://community.canvaslms.com/html/@6A1FDD4D5FF35E4BBB4083A1022FA0DB/assets/CommunityPreview23.png",
"og:type": "article",
"article:section": "Canvas Question Forum",
"article:published_time": "2020-03-19T15:50:03.409Z",
"og:site_name": "Instructure Community",
"article:modified_time": "2020-03-19T13:55:53-07:00",
"viewport": "width=device-width, initial-scale=1.0, user-scalable=yes",
"og:title": "STUDENTS CANNOT ACCESS RECORDED LECTURES",
"og:url": "https://community.canvaslms.com/t5/Canvas-Question-Forum/STUDENTS-CANNOT-ACCESS-RECORDED-LECTURES/m-p/190358#M93667",
"og:description": "I can access and see my recorded lectures but my students can't. They have an error message when they try to open the recorded presentation or notes.",
"article:author": "https://community.canvaslms.com/t5/user/viewprofilepage/user-id/794287",
"twitter:image": "https://community.canvaslms.com/html/@6A1FDD4D5FF35E4BBB4083A1022FA0DB/assets/CommunityPreview23.png"
}
],
"cse_image": [
{
"src": "https://community.canvaslms.com/html/@6A1FDD4D5FF35E4BBB4083A1022FA0DB/assets/CommunityPreview23.png"
}
]
}
},
{
"kind": "customsearch#result",
"title": "Public Lecture Series - Sam Fox School of Design & Visual Arts",
"htmlTitle": "Public \u003cb\u003eLecture\u003c/b\u003e Series - Sam Fox School of Design &amp; Visual Arts",
"link": "https://samfoxschool.wustl.edu/calendar/series/2-public-lecture-series",
"displayLink": "samfoxschool.wustl.edu",
"snippet": "The Sam Fox School's Spring 2024 Public Lecture Series highlights design and art as catalysts for change. Renowned speakers will delve into themes like ...",
"htmlSnippet": "The Sam Fox School&#39;s Spring 2024 Public \u003cb\u003eLecture\u003c/b\u003e Series highlights design and art as catalysts for change. Renowned speakers will delve into themes like&nbsp;...",
"cacheId": "B-cgQG0j6tUJ",
"formattedUrl": "https://samfoxschool.wustl.edu/calendar/series/2-public-lecture-series",
"htmlFormattedUrl": "https://samfoxschool.wustl.edu/calendar/series/2-public-lecture-series",
"pagemap": {
"cse_thumbnail": [
{
"src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQSmHaGianm-64m-qauYjkPK_Q0JKWe-7yom4m1ogFYTmpWArA7k6dmk0sR&s",
"width": "307",
"height": "164"
}
],
"website": [
{
"name": "Public Lecture Series - Sam Fox School of Design & Visual Arts — Washington University in St. Louis"
}
],
"metatags": [
{
"og:image": "https://dvsp0hlm0xrn3.cloudfront.net/assets/default_og_image-44e73dee4b9d1e2c6a6295901371270c8ec5899eaed48ee8167a9b12f1b0f8b3.jpg",
"og:type": "website",
"og:site_name": "Sam Fox School of Design & Visual Arts — Washington University in St. Louis",
"viewport": "width=device-width, initial-scale=1.0",
"og:title": "Public Lecture Series - Sam Fox School of Design & Visual Arts — Washington University in St. Louis",
"csrf-token": "jBQsfZGY3RH8NVs0-KVDBYB-2N2kib4UYZHYdrShfTdLkvzfSvGeOaMrRKTRdYBPRKzdcGIuP7zwm9etqX_uvg",
"csrf-param": "authenticity_token",
"og:description": "The Sam Fox School's Spring 2024 Public Lecture Series highlights design and art as catalysts for change. Renowned speakers will delve into themes like social equity, resilient cities, and the impact of emerging technologies on contemporary life. Speakers include artists, architects, designers, and critics of the highest caliber, widely recognized for their research-based practices and multidisciplinary approaches to their fields."
}
],
"cse_image": [
{
"src": "https://dvsp0hlm0xrn3.cloudfront.net/assets/default_og_image-44e73dee4b9d1e2c6a6295901371270c8ec5899eaed48ee8167a9b12f1b0f8b3.jpg"
}
]
}
}
]
}

View File

@@ -0,0 +1,476 @@
{
"query": "python",
"number_of_results": 116000000,
"results": [
{
"url": "https://www.python.org/",
"title": "Welcome to Python.org",
"content": "Python is a versatile and powerful language that lets you work quickly and integrate systems more effectively. Learn how to get started, download the latest version, access documentation, find jobs, and join the Python community.",
"engine": "bing",
"parsed_url": ["https", "www.python.org", "/", "", "", ""],
"template": "default.html",
"engines": ["bing", "qwant", "duckduckgo"],
"positions": [1, 1, 1],
"score": 9.0,
"category": "general"
},
{
"url": "https://wiki.nerdvpn.de/wiki/Python_(programming_language)",
"title": "Python (programming language) - Wikipedia",
"content": "Python is a high-level, general-purpose programming language. Its design philosophy emphasizes code readability with the use of significant indentation. Python is dynamically typed and garbage-collected. It supports multiple programming paradigms, including structured (particularly procedural), object-oriented and functional programming.",
"engine": "bing",
"parsed_url": ["https", "wiki.nerdvpn.de", "/wiki/Python_(programming_language)", "", "", ""],
"template": "default.html",
"engines": ["bing", "qwant", "duckduckgo"],
"positions": [4, 3, 2],
"score": 3.25,
"category": "general"
},
{
"url": "https://docs.python.org/3/tutorial/index.html",
"title": "The Python Tutorial \u2014 Python 3.12.3 documentation",
"content": "3 days ago \u00b7 Python is an easy to learn, powerful programming language. It has efficient high-level data structures and a simple but effective approach to object-oriented programming. Python\u2019s elegant syntax and dynamic typing, together with its interpreted nature, make it an ideal language for scripting and rapid application development in many \u2026",
"engine": "bing",
"parsed_url": ["https", "docs.python.org", "/3/tutorial/index.html", "", "", ""],
"template": "default.html",
"engines": ["bing", "qwant", "duckduckgo"],
"positions": [5, 5, 3],
"score": 2.2,
"category": "general"
},
{
"url": "https://www.python.org/downloads/",
"title": "Download Python | Python.org",
"content": "Python is a popular programming language for various purposes. Find the latest version of Python for different operating systems, download release notes, and learn about the development process.",
"engine": "bing",
"parsed_url": ["https", "www.python.org", "/downloads/", "", "", ""],
"template": "default.html",
"engines": ["bing", "duckduckgo"],
"positions": [2, 2],
"score": 2.0,
"category": "general"
},
{
"url": "https://www.python.org/about/gettingstarted/",
"title": "Python For Beginners | Python.org",
"content": "Learn the basics of Python, a popular and easy-to-use programming language, from installing it to using it for various purposes. Find out how to access online documentation, tutorials, books, code samples, and more resources to help you get started with Python.",
"engine": "bing",
"parsed_url": ["https", "www.python.org", "/about/gettingstarted/", "", "", ""],
"template": "default.html",
"engines": ["bing", "qwant", "duckduckgo"],
"positions": [9, 4, 4],
"score": 1.8333333333333333,
"category": "general"
},
{
"url": "https://www.python.org/shell/",
"title": "Welcome to Python.org",
"content": "Python is a versatile and easy-to-use programming language that lets you work quickly. Learn more about Python, download the latest version, access documentation, find jobs, and join the community.",
"engine": "bing",
"parsed_url": ["https", "www.python.org", "/shell/", "", "", ""],
"template": "default.html",
"engines": ["bing", "qwant", "duckduckgo"],
"positions": [3, 10, 8],
"score": 1.675,
"category": "general"
},
{
"url": "https://realpython.com/",
"title": "Python Tutorials \u2013 Real Python",
"content": "Real Python offers comprehensive and up-to-date tutorials, books, and courses for Python developers of all skill levels. Whether you want to learn Python basics, web development, data science, machine learning, or more, you can find clear and practical guides and code examples here.",
"engine": "bing",
"parsed_url": ["https", "realpython.com", "/", "", "", ""],
"template": "default.html",
"engines": ["bing", "qwant", "duckduckgo"],
"positions": [6, 6, 5],
"score": 1.6,
"category": "general"
},
{
"url": "https://wiki.nerdvpn.de/wiki/Python",
"title": "Python",
"content": "Topics referred to by the same term",
"engine": "wikipedia",
"parsed_url": ["https", "wiki.nerdvpn.de", "/wiki/Python", "", "", ""],
"template": "default.html",
"engines": ["wikipedia"],
"positions": [1],
"score": 1.0,
"category": "general"
},
{
"title": "Online Python - IDE, Editor, Compiler, Interpreter",
"content": "Online Python IDE is a free online tool that lets you write, execute, and share Python code in the web browser. Learn about Python, its features, and its popularity as a general-purpose programming language for web development, data science, and more.",
"url": "https://www.online-python.com/",
"engine": "duckduckgo",
"parsed_url": ["https", "www.online-python.com", "/", "", "", ""],
"template": "default.html",
"engines": ["qwant", "duckduckgo"],
"positions": [8, 6],
"score": 0.5833333333333333,
"category": "general"
},
{
"url": "https://micropython.org/",
"title": "MicroPython - Python for microcontrollers",
"content": "MicroPython is a full Python compiler and runtime that runs on the bare-metal. You get an interactive prompt (the REPL) to execute commands immediately, along ...",
"img_src": null,
"engine": "google",
"parsed_url": ["https", "micropython.org", "/", "", "", ""],
"template": "default.html",
"engines": ["google"],
"positions": [1],
"score": 1.0,
"category": "general"
},
{
"url": "https://dictionary.cambridge.org/uk/dictionary/english/python",
"title": "PYTHON | \u0417\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0432 \u0430\u043d\u0433\u043b\u0456\u0439\u0441\u044c\u043a\u0456\u0439 \u043c\u043e\u0432\u0456 - Cambridge Dictionary",
"content": "Apr 17, 2024 \u2014 \u0412\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f PYTHON: 1. a very large snake that kills animals for food by wrapping itself around them and crushing them\u2026. \u0414\u0456\u0437\u043d\u0430\u0439\u0442\u0435\u0441\u044f \u0431\u0456\u043b\u044c\u0448\u0435.",
"img_src": null,
"engine": "google",
"parsed_url": [
"https",
"dictionary.cambridge.org",
"/uk/dictionary/english/python",
"",
"",
""
],
"template": "default.html",
"engines": ["google"],
"positions": [2],
"score": 0.5,
"category": "general"
},
{
"url": "https://www.codetoday.co.uk/code",
"title": "Web-based Python Editor (with Turtle graphics)",
"content": "Quick way of starting to write Python code, including drawing with Turtle, provided by CodeToday using Trinket.io Ideal for young children to start ...",
"img_src": null,
"engine": "google",
"parsed_url": ["https", "www.codetoday.co.uk", "/code", "", "", ""],
"template": "default.html",
"engines": ["google"],
"positions": [3],
"score": 0.3333333333333333,
"category": "general"
},
{
"url": "https://snapcraft.io/docs/python-plugin",
"title": "The python plugin | Snapcraft documentation",
"content": "The python plugin can be used by either Python 2 or Python 3 based parts using a setup.py script for building the project, or using a package published to ...",
"img_src": null,
"engine": "google",
"parsed_url": ["https", "snapcraft.io", "/docs/python-plugin", "", "", ""],
"template": "default.html",
"engines": ["google"],
"positions": [4],
"score": 0.25,
"category": "general"
},
{
"url": "https://www.developer-tech.com/categories/developer-languages/developer-languages-python/",
"title": "Latest Python Developer News",
"content": "Python's status as the primary language for AI and machine learning projects, from its extensive data-handling capabilities to its flexibility and ...",
"img_src": null,
"engine": "google",
"parsed_url": [
"https",
"www.developer-tech.com",
"/categories/developer-languages/developer-languages-python/",
"",
"",
""
],
"template": "default.html",
"engines": ["google"],
"positions": [5],
"score": 0.2,
"category": "general"
},
{
"url": "https://subjectguides.york.ac.uk/coding/python",
"title": "Coding: a Practical Guide - Python - Subject Guides",
"content": "Python is a coding language used for a wide range of things, including working with data, building systems and software, and even creating games.",
"img_src": null,
"engine": "google",
"parsed_url": ["https", "subjectguides.york.ac.uk", "/coding/python", "", "", ""],
"template": "default.html",
"engines": ["google"],
"positions": [6],
"score": 0.16666666666666666,
"category": "general"
},
{
"url": "https://hub.salford.ac.uk/psytech/python/getting-started-python/",
"title": "Getting Started - Python - Salford PsyTech Home - The Hub",
"content": "Python in itself is a very friendly programming language, when we get to grips with writing code, once you grasp the logic, it will become very intuitive.",
"img_src": null,
"engine": "google",
"parsed_url": [
"https",
"hub.salford.ac.uk",
"/psytech/python/getting-started-python/",
"",
"",
""
],
"template": "default.html",
"engines": ["google"],
"positions": [7],
"score": 0.14285714285714285,
"category": "general"
},
{
"url": "https://snapcraft.io/docs/python-apps",
"title": "Python apps | Snapcraft documentation",
"content": "Snapcraft can be used to package and distribute Python applications in a way that enables convenient installation by users. The process of creating a snap ...",
"img_src": null,
"engine": "google",
"parsed_url": ["https", "snapcraft.io", "/docs/python-apps", "", "", ""],
"template": "default.html",
"engines": ["google"],
"positions": [8],
"score": 0.125,
"category": "general"
},
{
"url": "https://anvil.works/",
"title": "Anvil | Build Web Apps with Nothing but Python",
"content": "Anvil is a free Python-based drag-and-drop web app builder.\u200eSign Up \u00b7 \u200eSign in \u00b7 \u200ePricing \u00b7 \u200eForum",
"img_src": null,
"engine": "google",
"parsed_url": ["https", "anvil.works", "/", "", "", ""],
"template": "default.html",
"engines": ["google"],
"positions": [9],
"score": 0.1111111111111111,
"category": "general"
},
{
"url": "https://docs.python.org/",
"title": "Python 3.12.3 documentation",
"content": "3 days ago \u00b7 This is the official documentation for Python 3.12.3. Documentation sections: What's new in Python 3.12? Or all \"What's new\" documents since Python 2.0. Tutorial. Start here: a tour of Python's syntax and features. Library reference. Standard library and builtins. Language reference.",
"engine": "bing",
"parsed_url": ["https", "docs.python.org", "/", "", "", ""],
"template": "default.html",
"engines": ["bing", "duckduckgo"],
"positions": [7, 13],
"score": 0.43956043956043955,
"category": "general"
},
{
"title": "How to Use Python: Your First Steps - Real Python",
"content": "Learn the basics of Python syntax, installation, error handling, and more in this tutorial. You'll also code your first Python program and test your knowledge with a quiz.",
"url": "https://realpython.com/python-first-steps/",
"engine": "duckduckgo",
"parsed_url": ["https", "realpython.com", "/python-first-steps/", "", "", ""],
"template": "default.html",
"engines": ["qwant", "duckduckgo"],
"positions": [14, 7],
"score": 0.42857142857142855,
"category": "general"
},
{
"title": "The Python Tutorial \u2014 Python 3.11.8 documentation",
"content": "This tutorial introduces the reader informally to the basic concepts and features of the Python language and system. It helps to have a Python interpreter handy for hands-on experience, but all examples are self-contained, so the tutorial can be read off-line as well. For a description of standard objects and modules, see The Python Standard ...",
"url": "https://docs.python.org/3.11/tutorial/",
"engine": "duckduckgo",
"parsed_url": ["https", "docs.python.org", "/3.11/tutorial/", "", "", ""],
"template": "default.html",
"engines": ["duckduckgo"],
"positions": [7],
"score": 0.14285714285714285,
"category": "general"
},
{
"url": "https://realpython.com/python-introduction/",
"title": "Introduction to Python 3 \u2013 Real Python",
"content": "Python programming language, including a brief history of the development of Python and reasons why you might select Python as your language of choice.",
"engine": "bing",
"parsed_url": ["https", "realpython.com", "/python-introduction/", "", "", ""],
"template": "default.html",
"engines": ["bing"],
"positions": [8],
"score": 0.125,
"category": "general"
},
{
"title": "Our Documentation | Python.org",
"content": "Find online or download Python's documentation, tutorials, and guides for beginners and advanced users. Learn how to port from Python 2 to Python 3, contribute to Python, and access Python videos and books.",
"url": "https://www.python.org/doc/",
"engine": "duckduckgo",
"parsed_url": ["https", "www.python.org", "/doc/", "", "", ""],
"template": "default.html",
"engines": ["duckduckgo"],
"positions": [9],
"score": 0.1111111111111111,
"category": "general"
},
{
"title": "Welcome to Python.org",
"url": "http://www.get-python.org/shell/",
"content": "The mission of the Python Software Foundation is to promote, protect, and advance the Python programming language, and to support and facilitate the growth of a diverse and international community of Python programmers. Learn more. Become a Member Donate to the PSF.",
"engine": "qwant",
"parsed_url": ["http", "www.get-python.org", "/shell/", "", "", ""],
"template": "default.html",
"engines": ["qwant"],
"positions": [9],
"score": 0.1111111111111111,
"category": "general"
},
{
"title": "About Python\u2122 | Python.org",
"content": "Python is a powerful, fast, and versatile programming language that runs on various platforms and is easy to learn. Learn how to get started, explore the applications, and join the community of Python programmers and users.",
"url": "https://www.python.org/about/",
"engine": "duckduckgo",
"parsed_url": ["https", "www.python.org", "/about/", "", "", ""],
"template": "default.html",
"engines": ["duckduckgo"],
"positions": [11],
"score": 0.09090909090909091,
"category": "general"
},
{
"title": "Online Python Compiler (Interpreter) - Programiz",
"content": "Write and run Python code using this online tool. You can use Python Shell like IDLE, and take inputs from the user in our Python compiler.",
"url": "https://www.programiz.com/python-programming/online-compiler/",
"engine": "duckduckgo",
"parsed_url": [
"https",
"www.programiz.com",
"/python-programming/online-compiler/",
"",
"",
""
],
"template": "default.html",
"engines": ["duckduckgo"],
"positions": [12],
"score": 0.08333333333333333,
"category": "general"
},
{
"title": "Welcome to Python.org",
"content": "Python is a versatile and powerful language that lets you work quickly and integrate systems more effectively. Download the latest version, read the documentation, find jobs, events, success stories, and more on Python.org.",
"url": "https://www.python.org/?downloads",
"engine": "duckduckgo",
"parsed_url": ["https", "www.python.org", "/", "", "downloads", ""],
"template": "default.html",
"engines": ["duckduckgo"],
"positions": [15],
"score": 0.06666666666666667,
"category": "general"
},
{
"url": "https://www.matillion.com/blog/the-importance-of-python-and-its-growing-influence-on-data-productivty-a-matillion-perspective",
"title": "The Importance of Python and its Growing Influence on ...",
"content": "Jan 30, 2024 \u2014 The synergy of low-code functionality with Python's versatility empowers data professionals to orchestrate complex transformations seamlessly.",
"img_src": null,
"engine": "google",
"parsed_url": [
"https",
"www.matillion.com",
"/blog/the-importance-of-python-and-its-growing-influence-on-data-productivty-a-matillion-perspective",
"",
"",
""
],
"template": "default.html",
"engines": ["google"],
"positions": [10],
"score": 0.1,
"category": "general"
},
{
"title": "BeginnersGuide - Python Wiki",
"content": "This is the program that reads Python programs and carries out their instructions; you need it before you can do any Python programming. Mac and Linux distributions may include an outdated version of Python (Python 2), but you should install an updated one (Python 3). See BeginnersGuide/Download for instructions to download the correct version ...",
"url": "https://wiki.python.org/moin/BeginnersGuide",
"engine": "duckduckgo",
"parsed_url": ["https", "wiki.python.org", "/moin/BeginnersGuide", "", "", ""],
"template": "default.html",
"engines": ["duckduckgo"],
"positions": [16],
"score": 0.0625,
"category": "general"
},
{
"title": "Learn Python - Free Interactive Python Tutorial",
"content": "Learn Python from scratch or improve your skills with this website that offers tutorials, exercises, tests and certification. Explore topics such as basics, data science, advanced features and more with DataCamp.",
"url": "https://www.learnpython.org/",
"engine": "duckduckgo",
"parsed_url": ["https", "www.learnpython.org", "/", "", "", ""],
"template": "default.html",
"engines": ["duckduckgo"],
"positions": [17],
"score": 0.058823529411764705,
"category": "general"
}
],
"answers": [],
"corrections": [],
"infoboxes": [
{
"infobox": "Python",
"id": "https://en.wikipedia.org/wiki/Python_(programming_language)",
"content": "general-purpose programming language",
"img_src": "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6f/.PY_file_recreation.png/500px-.PY_file_recreation.png",
"urls": [
{
"title": "Official website",
"url": "https://www.python.org/",
"official": true
},
{
"title": "Wikipedia (en)",
"url": "https://en.wikipedia.org/wiki/Python_(programming_language)"
},
{
"title": "Wikidata",
"url": "http://www.wikidata.org/entity/Q28865"
}
],
"attributes": [
{
"label": "Inception",
"value": "Wednesday, February 20, 1991",
"entity": "P571"
},
{
"label": "Developer",
"value": "Python Software Foundation, Guido van Rossum",
"entity": "P178"
},
{
"label": "Copyright license",
"value": "Python Software Foundation License",
"entity": "P275"
},
{
"label": "Programmed in",
"value": "C, Python",
"entity": "P277"
},
{
"label": "Software version identifier",
"value": "3.12.3, 3.13.0a6",
"entity": "P348"
}
],
"engine": "wikidata",
"engines": ["wikidata"]
}
],
"suggestions": [
"python turtle",
"micro python tutorial",
"python docs",
"python compiler",
"snapcraft python",
"micropython vs python",
"python online",
"python download"
],
"unresponsive_engines": []
}

View File

@@ -0,0 +1,190 @@
{
"searchParameters": {
"q": "apple inc",
"gl": "us",
"hl": "en",
"autocorrect": true,
"page": 1,
"type": "search"
},
"knowledgeGraph": {
"title": "Apple",
"type": "Technology company",
"website": "http://www.apple.com/",
"imageUrl": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQwGQRv5TjjkycpctY66mOg_e2-npacrmjAb6_jAWhzlzkFE3OTjxyzbA&s=0",
"description": "Apple Inc. is an American multinational technology company specializing in consumer electronics, software and online services headquartered in Cupertino, California, United States.",
"descriptionSource": "Wikipedia",
"descriptionLink": "https://en.wikipedia.org/wiki/Apple_Inc.",
"attributes": {
"Headquarters": "Cupertino, CA",
"CEO": "Tim Cook (Aug 24, 2011)",
"Founded": "April 1, 1976, Los Altos, CA",
"Sales": "1 (800) 692-7753",
"Products": "iPhone, Apple Watch, iPad, and more",
"Founders": "Steve Jobs, Steve Wozniak, and Ronald Wayne",
"Subsidiaries": "Apple Store, Beats Electronics, Beddit, and more"
}
},
"organic": [
{
"title": "Apple",
"link": "https://www.apple.com/",
"snippet": "Discover the innovative world of Apple and shop everything iPhone, iPad, Apple Watch, Mac, and Apple TV, plus explore accessories, entertainment, ...",
"sitelinks": [
{
"title": "Support",
"link": "https://support.apple.com/"
},
{
"title": "iPhone",
"link": "https://www.apple.com/iphone/"
},
{
"title": "Apple makes business better.",
"link": "https://www.apple.com/business/"
},
{
"title": "Mac",
"link": "https://www.apple.com/mac/"
}
],
"position": 1
},
{
"title": "Apple Inc. - Wikipedia",
"link": "https://en.wikipedia.org/wiki/Apple_Inc.",
"snippet": "Apple Inc. is an American multinational technology company specializing in consumer electronics, software and online services headquartered in Cupertino, ...",
"attributes": {
"Products": "AirPods; Apple Watch; iPad; iPhone; Mac",
"Founders": "Steve Jobs; Steve Wozniak; Ronald Wayne",
"Founded": "April 1, 1976; 46 years ago in Los Altos, California, U.S",
"Industry": "Consumer electronics; Software services; Online services"
},
"sitelinks": [
{
"title": "History",
"link": "https://en.wikipedia.org/wiki/History_of_Apple_Inc."
},
{
"title": "Timeline of Apple Inc. products",
"link": "https://en.wikipedia.org/wiki/Timeline_of_Apple_Inc._products"
},
{
"title": "List of software by Apple Inc.",
"link": "https://en.wikipedia.org/wiki/List_of_software_by_Apple_Inc."
},
{
"title": "Apple Store",
"link": "https://en.wikipedia.org/wiki/Apple_Store"
}
],
"position": 2
},
{
"title": "Apple Inc. | History, Products, Headquarters, & Facts | Britannica",
"link": "https://www.britannica.com/topic/Apple-Inc",
"snippet": "Apple Inc., formerly Apple Computer, Inc., American manufacturer of personal computers, smartphones, tablet computers, computer peripherals, ...",
"date": "Aug 31, 2022",
"attributes": {
"Related People": "Steve Jobs Steve Wozniak Jony Ive Tim Cook Angela Ahrendts",
"Date": "1976 - present",
"Areas Of Involvement": "peripheral device"
},
"position": 3
},
{
"title": "AAPL: Apple Inc Stock Price Quote - NASDAQ GS - Bloomberg.com",
"link": "https://www.bloomberg.com/quote/AAPL:US",
"snippet": "Stock analysis for Apple Inc (AAPL:NASDAQ GS) including stock price, stock chart, company news, key statistics, fundamentals and company profile.",
"position": 4
},
{
"title": "Apple Inc. (AAPL) Company Profile & Facts - Yahoo Finance",
"link": "https://finance.yahoo.com/quote/AAPL/profile/",
"snippet": "Apple Inc. designs, manufactures, and markets smartphones, personal computers, tablets, wearables, and accessories worldwide. It also sells various related ...",
"position": 5
},
{
"title": "AAPL | Apple Inc. Stock Price & News - WSJ",
"link": "https://www.wsj.com/market-data/quotes/AAPL",
"snippet": "Apple, Inc. engages in the design, manufacture, and sale of smartphones, personal computers, tablets, wearables and accessories, and other varieties of ...",
"position": 6
},
{
"title": "Apple Inc Company Profile - Apple Inc Overview - GlobalData",
"link": "https://www.globaldata.com/company-profile/apple-inc/",
"snippet": "Apple Inc (Apple) designs, manufactures, and markets smartphones, tablets, personal computers (PCs), portable and wearable devices. The company also offers ...",
"position": 7
},
{
"title": "Apple Inc (AAPL) Stock Price & News - Google Finance",
"link": "https://www.google.com/finance/quote/AAPL:NASDAQ?hl=en",
"snippet": "Get the latest Apple Inc (AAPL) real-time quote, historical performance, charts, and other financial information to help you make more informed trading and ...",
"position": 8
}
],
"peopleAlsoAsk": [
{
"question": "What does Apple Inc mean?",
"snippet": "Apple Inc., formerly Apple Computer, Inc., American manufacturer of personal\ncomputers, smartphones, tablet computers, computer peripherals, and computer\nsoftware. It was the first successful personal computer company and the\npopularizer of the graphical user interface.\nAug 31, 2022",
"title": "Apple Inc. | History, Products, Headquarters, & Facts | Britannica",
"link": "https://www.britannica.com/topic/Apple-Inc"
},
{
"question": "Is Apple and Apple Inc same?",
"snippet": "Apple was founded as Apple Computer Company on April 1, 1976, by Steve Jobs,\nSteve Wozniak and Ronald Wayne to develop and sell Wozniak's Apple I personal\ncomputer. It was incorporated by Jobs and Wozniak as Apple Computer, Inc.",
"title": "Apple Inc. - Wikipedia",
"link": "https://en.wikipedia.org/wiki/Apple_Inc."
},
{
"question": "Who owns Apple Inc?",
"snippet": "Apple Inc. is owned by two main institutional investors (Vanguard Group and\nBlackRock, Inc). While its major individual shareholders comprise people like\nArt Levinson, Tim Cook, Bruce Sewell, Al Gore, Johny Sroujli, and others.",
"title": "Who Owns Apple In 2022? - FourWeekMBA",
"link": "https://fourweekmba.com/who-owns-apple/"
},
{
"question": "What products does Apple Inc offer?",
"snippet": "APPLE FOOTER\nStore.\nMac.\niPad.\niPhone.\nWatch.\nAirPods.\nTV & Home.\nAirTag.",
"title": "More items...",
"link": "https://www.apple.com/business/"
}
],
"relatedSearches": [
{
"query": "Who invented the iPhone"
},
{
"query": "Apple Inc competitors"
},
{
"query": "Apple iPad"
},
{
"query": "iPhones"
},
{
"query": "Apple Inc us"
},
{
"query": "Apple company history"
},
{
"query": "Apple Store"
},
{
"query": "Apple customer service"
},
{
"query": "Apple Watch"
},
{
"query": "Apple Inc Industry"
},
{
"query": "Apple Inc registered address"
},
{
"query": "Apple Inc Bloomberg"
}
]
}

View File

@@ -0,0 +1,206 @@
{
"ads": [],
"ads_count": 0,
"answers": [],
"results": [
{
"title": "Apple",
"link": "https://www.apple.com/",
"description": "Discover the innovative world of Apple and shop everything iPhone, iPad, Apple Watch, Mac, and Apple TV, plus explore accessories, entertainment, ...",
"additional_links": [
{
"text": "AppleApplehttps://www.apple.com",
"href": "https://www.apple.com/"
}
],
"cite": {},
"subdomains": [
{
"title": "Support",
"link": "https://support.apple.com/",
"description": "SupportContact - iPhone Support - Billing and Subscriptions - Apple Repair"
},
{
"title": "Store",
"link": "https://www.apple.com/store",
"description": "StoreShop iPhone - Shop iPad - App Store - Shop Mac - ..."
},
{
"title": "Mac",
"link": "https://www.apple.com/mac/",
"description": "MacMacBook Air - MacBook Pro - iMac - Compare Mac models - Mac mini"
},
{
"title": "iPad",
"link": "https://www.apple.com/ipad/",
"description": "iPadShop iPad - iPad Pro - iPad Air - Compare iPad models - ..."
},
{
"title": "Watch",
"link": "https://www.apple.com/watch/",
"description": "WatchShop Apple Watch - Series 9 - SE - Ultra 2 - Nike - Hermès - ..."
}
],
"realPosition": 1
},
{
"title": "Apple",
"link": "https://www.apple.com/",
"description": "Discover the innovative world of Apple and shop everything iPhone, iPad, Apple Watch, Mac, and Apple TV, plus explore accessories, entertainment, ...",
"additional_links": [
{
"text": "AppleApplehttps://www.apple.com",
"href": "https://www.apple.com/"
}
],
"cite": {},
"realPosition": 2
},
{
"title": "Apple Inc.",
"link": "https://en.wikipedia.org/wiki/Apple_Inc.",
"description": "Apple Inc. (formerly Apple Computer, Inc.) is an American multinational corporation and technology company headquartered in Cupertino, California, ...",
"additional_links": [
{
"text": "Apple Inc.Wikipediahttps://en.wikipedia.org wiki Apple_Inc",
"href": "https://en.wikipedia.org/wiki/Apple_Inc."
},
{
"text": "",
"href": "https://en.wikipedia.org/wiki/Apple_Inc."
},
{
"text": "History",
"href": "https://en.wikipedia.org/wiki/History_of_Apple_Inc."
},
{
"text": "List of Apple products",
"href": "https://en.wikipedia.org/wiki/List_of_Apple_products"
},
{
"text": "Litigation involving Apple Inc.",
"href": "https://en.wikipedia.org/wiki/Litigation_involving_Apple_Inc."
},
{
"text": "Apple Park",
"href": "https://en.wikipedia.org/wiki/Apple_Park"
}
],
"cite": {
"domain": "https://en.wikipedia.org wiki Apple_Inc",
"span": " wiki Apple_Inc"
},
"realPosition": 3
},
{
"title": "Apple Inc. (AAPL) Company Profile & Facts",
"link": "https://finance.yahoo.com/quote/AAPL/profile/",
"description": "Apple Inc. designs, manufactures, and markets smartphones, personal computers, tablets, wearables, and accessories worldwide. The company offers iPhone, a line ...",
"additional_links": [
{
"text": "Apple Inc. (AAPL) Company Profile & FactsYahoo Financehttps://finance.yahoo.com quote AAPL profile",
"href": "https://finance.yahoo.com/quote/AAPL/profile/"
}
],
"cite": {
"domain": "https://finance.yahoo.com quote AAPL profile",
"span": " quote AAPL profile"
},
"realPosition": 4
},
{
"title": "Apple Inc - Company Profile and News",
"link": "https://www.bloomberg.com/profile/company/AAPL:US",
"description": "Apple Inc. Apple Inc. designs, manufactures, and markets smartphones, personal computers, tablets, wearables and accessories, and sells a variety of related ...",
"additional_links": [
{
"text": "Apple Inc - Company Profile and NewsBloomberghttps://www.bloomberg.com company AAPL:US",
"href": "https://www.bloomberg.com/profile/company/AAPL:US"
},
{
"text": "",
"href": "https://www.bloomberg.com/profile/company/AAPL:US"
}
],
"cite": {
"domain": "https://www.bloomberg.com company AAPL:US",
"span": " company AAPL:US"
},
"realPosition": 5
},
{
"title": "Apple Inc. | History, Products, Headquarters, & Facts",
"link": "https://www.britannica.com/money/Apple-Inc",
"description": "May 22, 2024 — Apple Inc. is an American multinational technology company that revolutionized the technology sector through its innovation of computer ...",
"additional_links": [
{
"text": "Apple Inc. | History, Products, Headquarters, & FactsBritannicahttps://www.britannica.com money Apple-Inc",
"href": "https://www.britannica.com/money/Apple-Inc"
},
{
"text": "",
"href": "https://www.britannica.com/money/Apple-Inc"
}
],
"cite": {
"domain": "https://www.britannica.com money Apple-Inc",
"span": " money Apple-Inc"
},
"realPosition": 6
}
],
"shopping_ads": [],
"places": [
{
"title": "Apple Inc."
},
{
"title": "Apple Inc"
},
{
"title": "Apple Inc"
}
],
"related_searches": {
"images": [],
"text": [
{
"title": "apple inc full form",
"link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+Inc+full+form&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhPEAE"
},
{
"title": "apple company history",
"link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+company+history&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhOEAE"
},
{
"title": "apple store",
"link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+Store&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhQEAE"
},
{
"title": "apple id",
"link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+id&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhSEAE"
},
{
"title": "apple inc industry",
"link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+Inc+industry&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhREAE"
},
{
"title": "apple login",
"link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+login&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhTEAE"
}
]
},
"image_results": [],
"carousel": [],
"total": 2450000000,
"knowledge_graph": "",
"related_questions": [
"What does the Apple Inc do?",
"Why did Apple change to Apple Inc?",
"Who owns Apple Inc.?",
"What is Apple Inc best known for?"
],
"carousel_count": 0,
"ts": 2.491065263748169,
"device_type": null
}

View File

@@ -0,0 +1,276 @@
{
"request": {
"success": true,
"total_time_taken": 3.4,
"processed_timestamp": 1714968442,
"search_url": "http://www.google.com/search?q=mcdonalds\u0026gl=us\u0026hl=en\u0026safe=0\u0026num=10"
},
"search_parameters": {
"engine": "google",
"type": "web",
"device": "desktop",
"auto_location": "1",
"google_domain": "google.com",
"gl": "us",
"hl": "en",
"safe": "0",
"news_type": "all",
"exclude_autocorrected_results": "0",
"images_color": "any",
"page": "1",
"num": "10",
"output": "json",
"csv_fields": "search_parameters.query,organic_results.position,organic_results.title,organic_results.url,organic_results.domain",
"query": "mcdonalds",
"action": "search",
"access_key": "aac48e007e15c532bb94ffb34532a4b2",
"error": {}
},
"search_information": {
"total_results": 1170000000,
"time_taken_displayed": 0.49,
"detected_location": {},
"did_you_mean": {},
"no_results_for_original_query": false,
"showing_results_for": {}
},
"organic_results": [
{
"position": 1,
"title": "Our Full McDonald\u0027s Food Menu",
"snippet": "",
"prerender": false,
"cached_page_url": {},
"related_pages_url": {},
"url": "https://www.mcdonalds.com/us/en-us/full-menu.html",
"domain": "www.mcdonalds.com",
"displayed_url": "https://www.mcdonalds.com \u203a en-us \u203a full-menu"
},
{
"position": 2,
"title": "McDonald\u0027s",
"snippet": "McDonald\u0027s is the world\u0027s largest fast food restaurant chain, serving over 69 million customers daily in over 100 countries in more than 40,000 outlets as of\u00a0...",
"prerender": false,
"cached_page_url": {},
"related_pages_url": {},
"url": "https://en.wikipedia.org/wiki/McDonald%27s",
"domain": "en.wikipedia.org",
"displayed_url": "https://en.wikipedia.org \u203a wiki \u203a McDonald\u0027s"
},
{
"position": 3,
"title": "Restaurants Near Me: Nearby McDonald\u0027s Locations",
"snippet": "",
"prerender": false,
"cached_page_url": {},
"related_pages_url": {},
"url": "https://www.mcdonalds.com/us/en-us/restaurant-locator.html",
"domain": "www.mcdonalds.com",
"displayed_url": "https://www.mcdonalds.com \u203a en-us \u203a restaurant-locator"
},
{
"position": 4,
"title": "Download the McDonald\u0027s App: Deals, Promotions \u0026 ...",
"snippet": "Download the McDonald\u0027s app for Mobile Order \u0026 Pay, exclusive deals and coupons, menu information and special promotions.",
"prerender": false,
"cached_page_url": {},
"related_pages_url": {},
"url": "https://www.mcdonalds.com/us/en-us/download-app.html",
"domain": "www.mcdonalds.com",
"displayed_url": "https://www.mcdonalds.com \u203a en-us \u203a download-app"
},
{
"position": 5,
"title": "McDonald\u0027s Restaurant Careers in the US",
"snippet": "McDonald\u0027s restaurant jobs are one-of-a-kind \u2013 just like you. Restaurants are hiring across all levels, from Crew team to Management. Apply today!",
"prerender": false,
"cached_page_url": {},
"related_pages_url": {},
"url": "https://jobs.mchire.com/",
"domain": "jobs.mchire.com",
"displayed_url": "https://jobs.mchire.com"
}
],
"inline_images": [
{
"image_url": "https://serpstack-assets.apilayer.net/2418910010831954152.png",
"title": ""
}
],
"local_results": [
{
"position": 1,
"title": "McDonald\u0027s",
"coordinates": {
"latitude": 0,
"longitude": 0
},
"address": "",
"rating": 0,
"reviews": 0,
"type": "",
"price": {},
"url": 0
},
{
"position": 2,
"title": "McDonald\u0027s",
"coordinates": {
"latitude": 0,
"longitude": 0
},
"address": "",
"rating": 0,
"reviews": 0,
"type": "",
"price": {},
"url": 0
},
{
"position": 3,
"title": "McDonald\u0027s",
"coordinates": {
"latitude": 0,
"longitude": 0
},
"address": "",
"rating": 0,
"reviews": 0,
"type": "",
"price": {},
"url": 0
}
],
"top_stories": [
{
"block_position": 1,
"title": "Menu nutrition",
"url": "/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=mcdonald%27s+double+quarter+pounder+with+cheese\u0026stick=H4sIAAAAAAAAAONgFuLUz9U3ME-vLDBX4tVP1zc0TCsuNE0ytjTTUs5OttJPy89P0c9NzSuNLyjKL8tMSS2yAvNS80qKMlOLF7Hq5ian5Ocl5qSoFyuk5Jcm5aQqFJYmFpWkFikU5JfmATUolGeWZCgkZ6SmFqcCAM4ilJtxAAAA\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4Qri56BAh0EAM",
"source": "",
"uploaded": "",
"uploaded_utc": "2024-05-06T04:07:22.082Z"
},
{
"block_position": 2,
"title": "Profiles",
"url": "https://www.instagram.com/McDonalds",
"source": "",
"uploaded": "",
"uploaded_utc": "2024-05-06T04:07:22.082Z"
},
{
"block_position": 3,
"title": "People also search for",
"url": "/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026si=ACC90nzx_D3_zUKRnpAjmO0UBLNxnt7EyN4YYdru6U3bxLI-L5Wg8IL2sxPFxxcDEhVbocy-LJPZIvZySijw0ho2hfZ-KtV-sSEEJ9lw7JuEkXHDnRK5y4Dm8aqbiLwugbLbslwjG3hO_gpDTFZK2VoUGZPy2nrmOBCy0G3PoOfoiEtct2GSZlUz0uufG-xP8emtNzQKQpvjkAm5Zmi57iVZueiD62upz7-x2N3dAbwtm6FkInAPRw1yR91zuT7F3lEaPblTW3LaRwCDC0bvaRCh9x4N9zHgY1OOQa_rzts2jf5WpXcuw4Y%3D\u0026q=Burger+King\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4Qs9oBKAB6BAhzEAI",
"source": "",
"uploaded": "",
"uploaded_utc": "2024-05-06T04:07:22.082Z"
}
],
"related_questions": [
{
"question": "What\u0027s a number 7 at McDonald\u0027s?What\u0027s a number 7 at McDonald\u0027s?What\u0027s a number 7 at McDonald\u0027s?",
"answer": "",
"title": "",
"displayed_url": ""
},
{
"question": "Why is McDonald\u0027s changing their name?Why is McDonald\u0027s changing their name?Why is McDonald\u0027s changing their name?",
"answer": "",
"title": "",
"displayed_url": ""
},
{
"question": "What is the oldest still running Mcdonalds?What is the oldest still running Mcdonalds?What is the oldest still running Mcdonalds?",
"answer": "",
"title": "",
"displayed_url": ""
},
{
"question": "Why is McDonald\u0027s now WcDonald\u0027s?Why is McDonald\u0027s now WcDonald\u0027s?Why is McDonald\u0027s now WcDonald\u0027s?",
"answer": "",
"title": "",
"displayed_url": ""
}
],
"knowledge_graph": {
"title": "",
"type": "Fast-food restaurant company",
"image_urls": ["https://serpstack-assets.apilayer.net/2418910010831954152.png"],
"description": "McDonald\u0027s Corporation is an American multinational fast food chain, founded in 1940 as a restaurant operated by Richard and Maurice McDonald, in San Bernardino, California, United States.",
"source": {
"name": "Wikipedia",
"url": "https://en.wikipedia.org/wiki/McDonald\u0027s"
},
"people_also_search_for": [],
"known_attributes": [
{
"attribute": "kc:/business/business_operation:founder",
"link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Ray+Kroc\u0026si=ACC90nzx_D3_zUKRnpAjmO0UBLNxnt7EyN4YYdru6U3bxLI-LxARWRdbk5SkoY2sDn5Qq7yOmqYGei6qZ7sfJhsjZXBPgjMlLbS7824rpJOm69GzqVWMdoNIZiFX2T4A2td14sZOn4a1BexZLtZXHU7NZdF6VsWbGMVuiSYtXdev7uaUjEJKumiwlqTAATTebOriYTEBuSzC\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECHgQAg",
"name": "Founder: ",
"value": "Ray Kroc"
},
{
"attribute": "kc:/organization/organization:ceo",
"link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Chris+Kempczinski\u0026si=ACC90nwLLwns5sISZcdzuISy7t-NHozt8Cbt6G3WNQfC9ekAgKFbjdEFCDgxLbt57EDZGosYDGiZuq1AcBhA6IhTOSZxfVSySuGQ3VDwmmTA7Z93n3K3596jAuZH9VVv5h8PyvKJSuGuSsQWviJTl3eKj2UL1ZIWuDgkjyVMnC47rN7j0G9PlHRCCLdQF7VDQ1gubTiC4onXqLRBTbwAj6a--PD6Jv_NoA%3D%3D\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECHUQAg",
"name": "CEO: ",
"value": "Chris Kempczinski (Nov 1, 2019\u2013)"
},
{
"attribute": "kc:/business/employer:revenue",
"link": "",
"name": "Revenue: ",
"value": "25.49\u00a0billion USD (2023)"
},
{
"attribute": "kc:/organization/organization:founded",
"link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Des+Plaines\u0026si=ACC90nyvvWro6QmnyY1IfSdgk5wwjB1r8BGd_IWRjXqmKPQqm_yqLtI_DBi5PXGOtg_Z3qrzzEP6mcih1nN7h5A7v6OefnEJiC7a8dBR-v9LxlRubfyR6vlMr3fZ3TmVKWwz9FRpvZb1eYNt-RM7KIDKQlwGEIgINvzhxjUrv6uxSmceduzxd8W7Pkz71XGwxF0F8OlSzHlx\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECG4QAg",
"name": "Founded: ",
"value": "April 15, 1955, Des Plaines, IL"
},
{
"attribute": "kc:/organization/organization:headquarters",
"link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Chicago\u0026si=ACC90nyvvWro6QmnyY1IfSdgk5wwjB1r8BGd_IWRjXqmKPQqm-46AEJ_kJbUIEvsvEEZqteiYJvXVXs2ScRNDvFFpjfeAaW3dxtpTGCgcsf5RMdi6IdzOdtjJMN3ZaFwqZOmdi7tC6r0Mh1O9bnP3HrVDB9hH02m7aA6f70dCAfTdpOFnGxDU6wVMAI5MxWBE3wTugtUDOK-\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECHYQAg",
"name": "Headquarters: ",
"value": "Chicago, IL"
},
{
"attribute": "kc:/organization/organization:president",
"link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Chris+Kempczinski\u0026si=ACC90nwLLwns5sISZcdzuISy7t-NHozt8Cbt6G3WNQfC9ekAgKFbjdEFCDgxLbt57EDZGosYDGiZuq1AcBhA6IhTOSZxfVSySuGQ3VDwmmTA7Z93n3K3596jAuZH9VVv5h8PyvKJSuGuSsQWviJTl3eKj2UL1ZIWuDgkjyVMnC47rN7j0G9PlHRCCLdQF7VDQ1gubTiC4onXqLRBTbwAj6a--PD6Jv_NoA%3D%3D\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECHEQAg",
"name": "President: ",
"value": "Chris Kempczinski"
}
],
"website": "https://www.mcdonalds.com/us/en-us.html",
"profiles": [
{
"name": "Instagram",
"url": "https://www.instagram.com/McDonalds"
},
{
"name": "X (Twitter)",
"url": "https://twitter.com/McDonalds"
},
{
"name": "Facebook",
"url": "https://www.facebook.com/McDonaldsUS"
},
{
"name": "YouTube",
"url": "https://www.youtube.com/user/McDonaldsUS"
},
{
"name": "Pinterest",
"url": "https://www.pinterest.com/mcdonalds"
}
],
"founded": "April 15, 1955, Des Plaines, IL",
"headquarters": "Chicago, IL",
"founders": [
{
"name": "Ray Kroc",
"link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Ray+Kroc\u0026si=ACC90nzx_D3_zUKRnpAjmO0UBLNxnt7EyN4YYdru6U3bxLI-LxARWRdbk5SkoY2sDn5Qq7yOmqYGei6qZ7sfJhsjZXBPgjMlLbS7824rpJOm69GzqVWMdoNIZiFX2T4A2td14sZOn4a1BexZLtZXHU7NZdF6VsWbGMVuiSYtXdev7uaUjEJKumiwlqTAATTebOriYTEBuSzC\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECHgQAg"
}
]
}
}

View File

@@ -2,7 +2,7 @@ import os
import logging
import requests
from typing import List
from typing import List, Union
from apps.ollama.main import (
generate_ollama_embeddings,
@@ -19,8 +19,9 @@ from langchain.retrievers import (
)
from typing import Optional
from config import SRC_LOG_LEVELS, CHROMA_CLIENT
from utils.misc import get_last_user_message, add_or_update_system_message
from config import SRC_LOG_LEVELS, CHROMA_CLIENT
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
@@ -198,6 +199,7 @@ def get_embedding_function(
embedding_function,
openai_key,
openai_url,
batch_size,
):
if embedding_engine == "":
return lambda query: embedding_function.encode(query).tolist()
@@ -221,70 +223,51 @@ def get_embedding_function(
def generate_multiple(query, f):
if isinstance(query, list):
return [f(q) for q in query]
if embedding_engine == "openai":
embeddings = []
for i in range(0, len(query), batch_size):
embeddings.extend(f(query[i : i + batch_size]))
return embeddings
else:
return [f(q) for q in query]
else:
return f(query)
return lambda query: generate_multiple(query, func)
def rag_messages(
docs,
def get_rag_context(
files,
messages,
template,
embedding_function,
k,
reranking_function,
r,
hybrid_search,
):
log.debug(f"docs: {docs} {messages} {embedding_function} {reranking_function}")
last_user_message_idx = None
for i in range(len(messages) - 1, -1, -1):
if messages[i]["role"] == "user":
last_user_message_idx = i
break
user_message = messages[last_user_message_idx]
if isinstance(user_message["content"], list):
# Handle list content input
content_type = "list"
query = ""
for content_item in user_message["content"]:
if content_item["type"] == "text":
query = content_item["text"]
break
elif isinstance(user_message["content"], str):
# Handle text content input
content_type = "text"
query = user_message["content"]
else:
# Fallback in case the input does not match expected types
content_type = None
query = ""
log.debug(f"files: {files} {messages} {embedding_function} {reranking_function}")
query = get_last_user_message(messages)
extracted_collections = []
relevant_contexts = []
for doc in docs:
for file in files:
context = None
collection_names = (
doc["collection_names"]
if doc["type"] == "collection"
else [doc["collection_name"]]
file["collection_names"]
if file["type"] == "collection"
else [file["collection_name"]]
)
collection_names = set(collection_names).difference(extracted_collections)
if not collection_names:
log.debug(f"skipping {doc} as it has already been extracted")
log.debug(f"skipping {file} as it has already been extracted")
continue
try:
if doc["type"] == "text":
context = doc["content"]
if file["type"] == "text":
context = file["content"]
else:
if hybrid_search:
context = query_collection_with_hybrid_search(
@@ -307,18 +290,20 @@ def rag_messages(
context = None
if context:
relevant_contexts.append({**context, "source": doc})
relevant_contexts.append({**context, "source": file})
extracted_collections.extend(collection_names)
context_string = ""
contexts = []
citations = []
for context in relevant_contexts:
try:
if "documents" in context:
context_string += "\n\n".join(
[text for text in context["documents"][0] if text is not None]
contexts.append(
"\n\n".join(
[text for text in context["documents"][0] if text is not None]
)
)
if "metadatas" in context:
@@ -332,35 +317,7 @@ def rag_messages(
except Exception as e:
log.exception(e)
context_string = context_string.strip()
ra_content = rag_template(
template=template,
context=context_string,
query=query,
)
log.debug(f"ra_content: {ra_content}")
if content_type == "list":
new_content = []
for content_item in user_message["content"]:
if content_item["type"] == "text":
# Update the text item's content with ra_content
new_content.append({"type": "text", "text": ra_content})
else:
# Keep other types of content as they are
new_content.append(content_item)
new_user_message = {**user_message, "content": new_content}
else:
new_user_message = {
**user_message,
"content": ra_content,
}
messages[last_user_message_idx] = new_user_message
return messages, citations
return contexts, citations
def get_model_path(model: str, update_model: bool = False):
@@ -402,8 +359,22 @@ def get_model_path(model: str, update_model: bool = False):
def generate_openai_embeddings(
model: str, text: str, key: str, url: str = "https://api.openai.com/v1"
model: str,
text: Union[str, list[str]],
key: str,
url: str = "https://api.openai.com/v1",
):
if isinstance(text, list):
embeddings = generate_openai_batch_embeddings(model, text, key, url)
else:
embeddings = generate_openai_batch_embeddings(model, [text], key, url)
return embeddings[0] if isinstance(text, str) else embeddings
def generate_openai_batch_embeddings(
model: str, texts: list[str], key: str, url: str = "https://api.openai.com/v1"
) -> Optional[list[list[float]]]:
try:
r = requests.post(
f"{url}/embeddings",
@@ -411,12 +382,12 @@ def generate_openai_embeddings(
"Content-Type": "application/json",
"Authorization": f"Bearer {key}",
},
json={"input": text, "model": model},
json={"input": texts, "model": model},
)
r.raise_for_status()
data = r.json()
if "data" in data:
return data["data"][0]["embedding"]
return [elem["embedding"] for elem in data["data"]]
else:
raise "Something went wrong :/"
except Exception as e:
@@ -471,8 +442,6 @@ from langchain_core.documents import BaseDocumentCompressor, Document
from langchain_core.callbacks import Callbacks
from langchain_core.pydantic_v1 import Extra
from sentence_transformers import util
class RerankCompressor(BaseDocumentCompressor):
embedding_function: Any
@@ -497,6 +466,8 @@ class RerankCompressor(BaseDocumentCompressor):
[(query, doc.page_content) for doc in documents]
)
else:
from sentence_transformers import util
query_embedding = self.embedding_function(query)
document_embedding = self.embedding_function(
[doc.page_content for doc in documents]

139
backend/apps/socket/main.py Normal file
View File

@@ -0,0 +1,139 @@
import socketio
import asyncio
from apps.webui.models.users import Users
from utils.utils import decode_token
sio = socketio.AsyncServer(cors_allowed_origins=[], async_mode="asgi")
app = socketio.ASGIApp(sio, socketio_path="/ws/socket.io")
# Dictionary to maintain the user pool
SESSION_POOL = {}
USER_POOL = {}
USAGE_POOL = {}
# Timeout duration in seconds
TIMEOUT_DURATION = 3
@sio.event
async def connect(sid, environ, auth):
user = None
if auth and "token" in auth:
data = decode_token(auth["token"])
if data is not None and "id" in data:
user = Users.get_user_by_id(data["id"])
if user:
SESSION_POOL[sid] = user.id
if user.id in USER_POOL:
USER_POOL[user.id].append(sid)
else:
USER_POOL[user.id] = [sid]
print(f"user {user.name}({user.id}) connected with session ID {sid}")
await sio.emit("user-count", {"count": len(set(USER_POOL))})
await sio.emit("usage", {"models": get_models_in_use()})
@sio.on("user-join")
async def user_join(sid, data):
print("user-join", sid, data)
auth = data["auth"] if "auth" in data else None
if auth and "token" in auth:
data = decode_token(auth["token"])
if data is not None and "id" in data:
user = Users.get_user_by_id(data["id"])
if user:
SESSION_POOL[sid] = user.id
if user.id in USER_POOL:
USER_POOL[user.id].append(sid)
else:
USER_POOL[user.id] = [sid]
print(f"user {user.name}({user.id}) connected with session ID {sid}")
await sio.emit("user-count", {"count": len(set(USER_POOL))})
@sio.on("user-count")
async def user_count(sid):
await sio.emit("user-count", {"count": len(set(USER_POOL))})
def get_models_in_use():
# Aggregate all models in use
models_in_use = []
for model_id, data in USAGE_POOL.items():
models_in_use.append(model_id)
return models_in_use
@sio.on("usage")
async def usage(sid, data):
model_id = data["model"]
# Cancel previous callback if there is one
if model_id in USAGE_POOL:
USAGE_POOL[model_id]["callback"].cancel()
# Store the new usage data and task
if model_id in USAGE_POOL:
USAGE_POOL[model_id]["sids"].append(sid)
USAGE_POOL[model_id]["sids"] = list(set(USAGE_POOL[model_id]["sids"]))
else:
USAGE_POOL[model_id] = {"sids": [sid]}
# Schedule a task to remove the usage data after TIMEOUT_DURATION
USAGE_POOL[model_id]["callback"] = asyncio.create_task(
remove_after_timeout(sid, model_id)
)
# Broadcast the usage data to all clients
await sio.emit("usage", {"models": get_models_in_use()})
async def remove_after_timeout(sid, model_id):
try:
await asyncio.sleep(TIMEOUT_DURATION)
if model_id in USAGE_POOL:
print(USAGE_POOL[model_id]["sids"])
USAGE_POOL[model_id]["sids"].remove(sid)
USAGE_POOL[model_id]["sids"] = list(set(USAGE_POOL[model_id]["sids"]))
if len(USAGE_POOL[model_id]["sids"]) == 0:
del USAGE_POOL[model_id]
# Broadcast the usage data to all clients
await sio.emit("usage", {"models": get_models_in_use()})
except asyncio.CancelledError:
# Task was cancelled due to new 'usage' event
pass
@sio.event
async def disconnect(sid):
if sid in SESSION_POOL:
user_id = SESSION_POOL[sid]
del SESSION_POOL[sid]
USER_POOL[user_id].remove(sid)
if len(USER_POOL[user_id]) == 0:
del USER_POOL[user_id]
await sio.emit("user-count", {"count": len(USER_POOL)})
else:
print(f"Unknown session ID {sid} disconnected")

View File

@@ -1,23 +0,0 @@
from peewee import *
from peewee_migrate import Router
from playhouse.db_url import connect
from config import SRC_LOG_LEVELS, DATA_DIR, DATABASE_URL
import os
import logging
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["DB"])
# Check if the file exists
if os.path.exists(f"{DATA_DIR}/ollama.db"):
# Rename the file
os.rename(f"{DATA_DIR}/ollama.db", f"{DATA_DIR}/webui.db")
log.info("Database migrated from Ollama-WebUI successfully.")
else:
pass
DB = connect(DATABASE_URL)
log.info(f"Connected to a {DB.__class__.__name__} database.")
router = Router(DB, migrate_dir="apps/web/internal/migrations", logger=log)
router.run()
DB.connect(reuse_if_open=True)

View File

@@ -1,21 +0,0 @@
# Database Migrations
This directory contains all the database migrations for the web app.
Migrations are done using the [`peewee-migrate`](https://github.com/klen/peewee_migrate) library.
Migrations are automatically ran at app startup.
## Creating a migration
Have you made a change to the schema of an existing model?
You will need to create a migration file to ensure that existing databases are updated for backwards compatibility.
1. Have a database file (`webui.db`) that has the old schema prior to any of your changes.
2. Make your changes to the models.
3. From the `backend` directory, run the following command:
```bash
pw_migrate create --auto --auto-source apps.web.models --database sqlite:///${SQLITE_DB} --directory apps/web/internal/migrations ${MIGRATION_NAME}
```
- `$SQLITE_DB` should be the path to the database file.
- `$MIGRATION_NAME` should be a descriptive name for the migration.
4. The migration file will be created in the `apps/web/internal/migrations` directory.

View File

@@ -1,75 +0,0 @@
from fastapi import FastAPI, Depends
from fastapi.routing import APIRoute
from fastapi.middleware.cors import CORSMiddleware
from apps.web.routers import (
auths,
users,
chats,
documents,
modelfiles,
prompts,
configs,
memories,
utils,
)
from config import (
WEBUI_VERSION,
WEBUI_AUTH,
DEFAULT_MODELS,
DEFAULT_PROMPT_SUGGESTIONS,
DEFAULT_USER_ROLE,
ENABLE_SIGNUP,
USER_PERMISSIONS,
WEBHOOK_URL,
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
JWT_EXPIRES_IN,
AppConfig,
)
app = FastAPI()
origins = ["*"]
app.state.config = AppConfig()
app.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP
app.state.config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
app.state.config.DEFAULT_MODELS = DEFAULT_MODELS
app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
app.state.config.USER_PERMISSIONS = USER_PERMISSIONS
app.state.config.WEBHOOK_URL = WEBHOOK_URL
app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(auths.router, prefix="/auths", tags=["auths"])
app.include_router(users.router, prefix="/users", tags=["users"])
app.include_router(chats.router, prefix="/chats", tags=["chats"])
app.include_router(documents.router, prefix="/documents", tags=["documents"])
app.include_router(modelfiles.router, prefix="/modelfiles", tags=["modelfiles"])
app.include_router(prompts.router, prefix="/prompts", tags=["prompts"])
app.include_router(memories.router, prefix="/memories", tags=["memories"])
app.include_router(configs.router, prefix="/configs", tags=["configs"])
app.include_router(utils.router, prefix="/utils", tags=["utils"])
@app.get("/")
async def get_status():
return {
"status": True,
"auth": WEBUI_AUTH,
"default_models": app.state.config.DEFAULT_MODELS,
"default_prompt_suggestions": app.state.config.DEFAULT_PROMPT_SUGGESTIONS,
}

View File

@@ -1,318 +0,0 @@
from pydantic import BaseModel
from typing import List, Union, Optional
from peewee import *
from playhouse.shortcuts import model_to_dict
import json
import uuid
import time
from apps.web.internal.db import DB
####################
# Chat DB Schema
####################
class Chat(Model):
id = CharField(unique=True)
user_id = CharField()
title = TextField()
chat = TextField() # Save Chat JSON as Text
created_at = BigIntegerField()
updated_at = BigIntegerField()
share_id = CharField(null=True, unique=True)
archived = BooleanField(default=False)
class Meta:
database = DB
class ChatModel(BaseModel):
id: str
user_id: str
title: str
chat: str
created_at: int # timestamp in epoch
updated_at: int # timestamp in epoch
share_id: Optional[str] = None
archived: bool = False
####################
# Forms
####################
class ChatForm(BaseModel):
chat: dict
class ChatTitleForm(BaseModel):
title: str
class ChatResponse(BaseModel):
id: str
user_id: str
title: str
chat: dict
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
share_id: Optional[str] = None # id of the chat to be shared
archived: bool
class ChatTitleIdResponse(BaseModel):
id: str
title: str
updated_at: int
created_at: int
class ChatTable:
def __init__(self, db):
self.db = db
db.create_tables([Chat])
def insert_new_chat(self, user_id: str, form_data: ChatForm) -> Optional[ChatModel]:
id = str(uuid.uuid4())
chat = ChatModel(
**{
"id": id,
"user_id": user_id,
"title": (
form_data.chat["title"] if "title" in form_data.chat else "New Chat"
),
"chat": json.dumps(form_data.chat),
"created_at": int(time.time()),
"updated_at": int(time.time()),
}
)
result = Chat.create(**chat.model_dump())
return chat if result else None
def update_chat_by_id(self, id: str, chat: dict) -> Optional[ChatModel]:
try:
query = Chat.update(
chat=json.dumps(chat),
title=chat["title"] if "title" in chat else "New Chat",
updated_at=int(time.time()),
).where(Chat.id == id)
query.execute()
chat = Chat.get(Chat.id == id)
return ChatModel(**model_to_dict(chat))
except:
return None
def insert_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]:
# Get the existing chat to share
chat = Chat.get(Chat.id == chat_id)
# Check if the chat is already shared
if chat.share_id:
return self.get_chat_by_id_and_user_id(chat.share_id, "shared")
# Create a new chat with the same data, but with a new ID
shared_chat = ChatModel(
**{
"id": str(uuid.uuid4()),
"user_id": f"shared-{chat_id}",
"title": chat.title,
"chat": chat.chat,
"created_at": chat.created_at,
"updated_at": int(time.time()),
}
)
shared_result = Chat.create(**shared_chat.model_dump())
# Update the original chat with the share_id
result = (
Chat.update(share_id=shared_chat.id).where(Chat.id == chat_id).execute()
)
return shared_chat if (shared_result and result) else None
def update_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]:
try:
print("update_shared_chat_by_id")
chat = Chat.get(Chat.id == chat_id)
print(chat)
query = Chat.update(
title=chat.title,
chat=chat.chat,
).where(Chat.id == chat.share_id)
query.execute()
chat = Chat.get(Chat.id == chat.share_id)
return ChatModel(**model_to_dict(chat))
except:
return None
def delete_shared_chat_by_chat_id(self, chat_id: str) -> bool:
try:
query = Chat.delete().where(Chat.user_id == f"shared-{chat_id}")
query.execute() # Remove the rows, return number of rows removed.
return True
except:
return False
def update_chat_share_id_by_id(
self, id: str, share_id: Optional[str]
) -> Optional[ChatModel]:
try:
query = Chat.update(
share_id=share_id,
).where(Chat.id == id)
query.execute()
chat = Chat.get(Chat.id == id)
return ChatModel(**model_to_dict(chat))
except:
return None
def toggle_chat_archive_by_id(self, id: str) -> Optional[ChatModel]:
try:
chat = self.get_chat_by_id(id)
query = Chat.update(
archived=(not chat.archived),
).where(Chat.id == id)
query.execute()
chat = Chat.get(Chat.id == id)
return ChatModel(**model_to_dict(chat))
except:
return None
def get_archived_chat_list_by_user_id(
self, user_id: str, skip: int = 0, limit: int = 50
) -> List[ChatModel]:
return [
ChatModel(**model_to_dict(chat))
for chat in Chat.select()
.where(Chat.archived == True)
.where(Chat.user_id == user_id)
.order_by(Chat.updated_at.desc())
# .limit(limit)
# .offset(skip)
]
def get_chat_list_by_user_id(
self, user_id: str, skip: int = 0, limit: int = 50
) -> List[ChatModel]:
return [
ChatModel(**model_to_dict(chat))
for chat in Chat.select()
.where(Chat.archived == False)
.where(Chat.user_id == user_id)
.order_by(Chat.updated_at.desc())
# .limit(limit)
# .offset(skip)
]
def get_chat_list_by_chat_ids(
self, chat_ids: List[str], skip: int = 0, limit: int = 50
) -> List[ChatModel]:
return [
ChatModel(**model_to_dict(chat))
for chat in Chat.select()
.where(Chat.archived == False)
.where(Chat.id.in_(chat_ids))
.order_by(Chat.updated_at.desc())
]
def get_chat_by_id(self, id: str) -> Optional[ChatModel]:
try:
chat = Chat.get(Chat.id == id)
return ChatModel(**model_to_dict(chat))
except:
return None
def get_chat_by_share_id(self, id: str) -> Optional[ChatModel]:
try:
chat = Chat.get(Chat.share_id == id)
if chat:
chat = Chat.get(Chat.id == id)
return ChatModel(**model_to_dict(chat))
else:
return None
except:
return None
def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]:
try:
chat = Chat.get(Chat.id == id, Chat.user_id == user_id)
return ChatModel(**model_to_dict(chat))
except:
return None
def get_chats(self, skip: int = 0, limit: int = 50) -> List[ChatModel]:
return [
ChatModel(**model_to_dict(chat))
for chat in Chat.select().order_by(Chat.updated_at.desc())
# .limit(limit).offset(skip)
]
def get_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
return [
ChatModel(**model_to_dict(chat))
for chat in Chat.select()
.where(Chat.user_id == user_id)
.order_by(Chat.updated_at.desc())
# .limit(limit).offset(skip)
]
def delete_chat_by_id(self, id: str) -> bool:
try:
query = Chat.delete().where((Chat.id == id))
query.execute() # Remove the rows, return number of rows removed.
return True and self.delete_shared_chat_by_chat_id(id)
except:
return False
def delete_chat_by_id_and_user_id(self, id: str, user_id: str) -> bool:
try:
query = Chat.delete().where((Chat.id == id) & (Chat.user_id == user_id))
query.execute() # Remove the rows, return number of rows removed.
return True and self.delete_shared_chat_by_chat_id(id)
except:
return False
def delete_chats_by_user_id(self, user_id: str) -> bool:
try:
self.delete_shared_chats_by_user_id(user_id)
query = Chat.delete().where(Chat.user_id == user_id)
query.execute() # Remove the rows, return number of rows removed.
return True
except:
return False
def delete_shared_chats_by_user_id(self, user_id: str) -> bool:
try:
shared_chat_ids = [
f"shared-{chat.id}"
for chat in Chat.select().where(Chat.user_id == user_id)
]
query = Chat.delete().where(Chat.user_id << shared_chat_ids)
query.execute() # Remove the rows, return number of rows removed.
return True
except:
return False
Chats = ChatTable(DB)

View File

@@ -1,160 +0,0 @@
from pydantic import BaseModel
from peewee import *
from playhouse.shortcuts import model_to_dict
from typing import List, Union, Optional
import time
import logging
from utils.utils import decode_token
from utils.misc import get_gravatar_url
from apps.web.internal.db import DB
import json
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
# Documents DB Schema
####################
class Document(Model):
collection_name = CharField(unique=True)
name = CharField(unique=True)
title = TextField()
filename = TextField()
content = TextField(null=True)
user_id = CharField()
timestamp = BigIntegerField()
class Meta:
database = DB
class DocumentModel(BaseModel):
collection_name: str
name: str
title: str
filename: str
content: Optional[str] = None
user_id: str
timestamp: int # timestamp in epoch
####################
# Forms
####################
class DocumentResponse(BaseModel):
collection_name: str
name: str
title: str
filename: str
content: Optional[dict] = None
user_id: str
timestamp: int # timestamp in epoch
class DocumentUpdateForm(BaseModel):
name: str
title: str
class DocumentForm(DocumentUpdateForm):
collection_name: str
filename: str
content: Optional[str] = None
class DocumentsTable:
def __init__(self, db):
self.db = db
self.db.create_tables([Document])
def insert_new_doc(
self, user_id: str, form_data: DocumentForm
) -> Optional[DocumentModel]:
document = DocumentModel(
**{
**form_data.model_dump(),
"user_id": user_id,
"timestamp": int(time.time()),
}
)
try:
result = Document.create(**document.model_dump())
if result:
return document
else:
return None
except:
return None
def get_doc_by_name(self, name: str) -> Optional[DocumentModel]:
try:
document = Document.get(Document.name == name)
return DocumentModel(**model_to_dict(document))
except:
return None
def get_docs(self) -> List[DocumentModel]:
return [
DocumentModel(**model_to_dict(doc))
for doc in Document.select()
# .limit(limit).offset(skip)
]
def update_doc_by_name(
self, name: str, form_data: DocumentUpdateForm
) -> Optional[DocumentModel]:
try:
query = Document.update(
title=form_data.title,
name=form_data.name,
timestamp=int(time.time()),
).where(Document.name == name)
query.execute()
doc = Document.get(Document.name == form_data.name)
return DocumentModel(**model_to_dict(doc))
except Exception as e:
log.exception(e)
return None
def update_doc_content_by_name(
self, name: str, updated: dict
) -> Optional[DocumentModel]:
try:
doc = self.get_doc_by_name(name)
doc_content = json.loads(doc.content if doc.content else "{}")
doc_content = {**doc_content, **updated}
query = Document.update(
content=json.dumps(doc_content),
timestamp=int(time.time()),
).where(Document.name == name)
query.execute()
doc = Document.get(Document.name == name)
return DocumentModel(**model_to_dict(doc))
except Exception as e:
log.exception(e)
return None
def delete_doc_by_name(self, name: str) -> bool:
try:
query = Document.delete().where((Document.name == name))
query.execute() # Remove the rows, return number of rows removed.
return True
except:
return False
Documents = DocumentsTable(DB)

View File

@@ -1,118 +0,0 @@
from pydantic import BaseModel
from peewee import *
from playhouse.shortcuts import model_to_dict
from typing import List, Union, Optional
from apps.web.internal.db import DB
from apps.web.models.chats import Chats
import time
import uuid
####################
# Memory DB Schema
####################
class Memory(Model):
id = CharField(unique=True)
user_id = CharField()
content = TextField()
updated_at = BigIntegerField()
created_at = BigIntegerField()
class Meta:
database = DB
class MemoryModel(BaseModel):
id: str
user_id: str
content: str
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
####################
# Forms
####################
class MemoriesTable:
def __init__(self, db):
self.db = db
self.db.create_tables([Memory])
def insert_new_memory(
self,
user_id: str,
content: str,
) -> Optional[MemoryModel]:
id = str(uuid.uuid4())
memory = MemoryModel(
**{
"id": id,
"user_id": user_id,
"content": content,
"created_at": int(time.time()),
"updated_at": int(time.time()),
}
)
result = Memory.create(**memory.model_dump())
if result:
return memory
else:
return None
def get_memories(self) -> List[MemoryModel]:
try:
memories = Memory.select()
return [MemoryModel(**model_to_dict(memory)) for memory in memories]
except:
return None
def get_memories_by_user_id(self, user_id: str) -> List[MemoryModel]:
try:
memories = Memory.select().where(Memory.user_id == user_id)
return [MemoryModel(**model_to_dict(memory)) for memory in memories]
except:
return None
def get_memory_by_id(self, id) -> Optional[MemoryModel]:
try:
memory = Memory.get(Memory.id == id)
return MemoryModel(**model_to_dict(memory))
except:
return None
def delete_memory_by_id(self, id: str) -> bool:
try:
query = Memory.delete().where(Memory.id == id)
query.execute() # Remove the rows, return number of rows removed.
return True
except:
return False
def delete_memories_by_user_id(self, user_id: str) -> bool:
try:
query = Memory.delete().where(Memory.user_id == user_id)
query.execute()
return True
except:
return False
def delete_memory_by_id_and_user_id(self, id: str, user_id: str) -> bool:
try:
query = Memory.delete().where(Memory.id == id, Memory.user_id == user_id)
query.execute()
return True
except:
return False
Memories = MemoriesTable(DB)

View File

@@ -1,136 +0,0 @@
from pydantic import BaseModel
from peewee import *
from playhouse.shortcuts import model_to_dict
from typing import List, Union, Optional
import time
from utils.utils import decode_token
from utils.misc import get_gravatar_url
from apps.web.internal.db import DB
import json
####################
# Modelfile DB Schema
####################
class Modelfile(Model):
tag_name = CharField(unique=True)
user_id = CharField()
modelfile = TextField()
timestamp = BigIntegerField()
class Meta:
database = DB
class ModelfileModel(BaseModel):
tag_name: str
user_id: str
modelfile: str
timestamp: int # timestamp in epoch
####################
# Forms
####################
class ModelfileForm(BaseModel):
modelfile: dict
class ModelfileTagNameForm(BaseModel):
tag_name: str
class ModelfileUpdateForm(ModelfileForm, ModelfileTagNameForm):
pass
class ModelfileResponse(BaseModel):
tag_name: str
user_id: str
modelfile: dict
timestamp: int # timestamp in epoch
class ModelfilesTable:
def __init__(self, db):
self.db = db
self.db.create_tables([Modelfile])
def insert_new_modelfile(
self, user_id: str, form_data: ModelfileForm
) -> Optional[ModelfileModel]:
if "tagName" in form_data.modelfile:
modelfile = ModelfileModel(
**{
"user_id": user_id,
"tag_name": form_data.modelfile["tagName"],
"modelfile": json.dumps(form_data.modelfile),
"timestamp": int(time.time()),
}
)
try:
result = Modelfile.create(**modelfile.model_dump())
if result:
return modelfile
else:
return None
except:
return None
else:
return None
def get_modelfile_by_tag_name(self, tag_name: str) -> Optional[ModelfileModel]:
try:
modelfile = Modelfile.get(Modelfile.tag_name == tag_name)
return ModelfileModel(**model_to_dict(modelfile))
except:
return None
def get_modelfiles(self, skip: int = 0, limit: int = 50) -> List[ModelfileResponse]:
return [
ModelfileResponse(
**{
**model_to_dict(modelfile),
"modelfile": json.loads(modelfile.modelfile),
}
)
for modelfile in Modelfile.select()
# .limit(limit).offset(skip)
]
def update_modelfile_by_tag_name(
self, tag_name: str, modelfile: dict
) -> Optional[ModelfileModel]:
try:
query = Modelfile.update(
modelfile=json.dumps(modelfile),
timestamp=int(time.time()),
).where(Modelfile.tag_name == tag_name)
query.execute()
modelfile = Modelfile.get(Modelfile.tag_name == tag_name)
return ModelfileModel(**model_to_dict(modelfile))
except:
return None
def delete_modelfile_by_tag_name(self, tag_name: str) -> bool:
try:
query = Modelfile.delete().where((Modelfile.tag_name == tag_name))
query.execute() # Remove the rows, return number of rows removed.
return True
except:
return False
Modelfiles = ModelfilesTable(DB)

View File

@@ -1,118 +0,0 @@
from pydantic import BaseModel
from peewee import *
from playhouse.shortcuts import model_to_dict
from typing import List, Union, Optional
import time
from utils.utils import decode_token
from utils.misc import get_gravatar_url
from apps.web.internal.db import DB
import json
####################
# Prompts DB Schema
####################
class Prompt(Model):
command = CharField(unique=True)
user_id = CharField()
title = TextField()
content = TextField()
timestamp = BigIntegerField()
class Meta:
database = DB
class PromptModel(BaseModel):
command: str
user_id: str
title: str
content: str
timestamp: int # timestamp in epoch
####################
# Forms
####################
class PromptForm(BaseModel):
command: str
title: str
content: str
class PromptsTable:
def __init__(self, db):
self.db = db
self.db.create_tables([Prompt])
def insert_new_prompt(
self, user_id: str, form_data: PromptForm
) -> Optional[PromptModel]:
prompt = PromptModel(
**{
"user_id": user_id,
"command": form_data.command,
"title": form_data.title,
"content": form_data.content,
"timestamp": int(time.time()),
}
)
try:
result = Prompt.create(**prompt.model_dump())
if result:
return prompt
else:
return None
except:
return None
def get_prompt_by_command(self, command: str) -> Optional[PromptModel]:
try:
prompt = Prompt.get(Prompt.command == command)
return PromptModel(**model_to_dict(prompt))
except:
return None
def get_prompts(self) -> List[PromptModel]:
return [
PromptModel(**model_to_dict(prompt))
for prompt in Prompt.select()
# .limit(limit).offset(skip)
]
def update_prompt_by_command(
self, command: str, form_data: PromptForm
) -> Optional[PromptModel]:
try:
query = Prompt.update(
title=form_data.title,
content=form_data.content,
timestamp=int(time.time()),
).where(Prompt.command == command)
query.execute()
prompt = Prompt.get(Prompt.command == command)
return PromptModel(**model_to_dict(prompt))
except:
return None
def delete_prompt_by_command(self, command: str) -> bool:
try:
query = Prompt.delete().where((Prompt.command == command))
query.execute() # Remove the rows, return number of rows removed.
return True
except:
return False
Prompts = PromptsTable(DB)

View File

@@ -1,237 +0,0 @@
from pydantic import BaseModel
from typing import List, Union, Optional
from peewee import *
from playhouse.shortcuts import model_to_dict
import json
import uuid
import time
import logging
from apps.web.internal.db import DB
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
# Tag DB Schema
####################
class Tag(Model):
id = CharField(unique=True)
name = CharField()
user_id = CharField()
data = TextField(null=True)
class Meta:
database = DB
class ChatIdTag(Model):
id = CharField(unique=True)
tag_name = CharField()
chat_id = CharField()
user_id = CharField()
timestamp = BigIntegerField()
class Meta:
database = DB
class TagModel(BaseModel):
id: str
name: str
user_id: str
data: Optional[str] = None
class ChatIdTagModel(BaseModel):
id: str
tag_name: str
chat_id: str
user_id: str
timestamp: int
####################
# Forms
####################
class ChatIdTagForm(BaseModel):
tag_name: str
chat_id: str
class TagChatIdsResponse(BaseModel):
chat_ids: List[str]
class ChatTagsResponse(BaseModel):
tags: List[str]
class TagTable:
def __init__(self, db):
self.db = db
db.create_tables([Tag, ChatIdTag])
def insert_new_tag(self, name: str, user_id: str) -> Optional[TagModel]:
id = str(uuid.uuid4())
tag = TagModel(**{"id": id, "user_id": user_id, "name": name})
try:
result = Tag.create(**tag.model_dump())
if result:
return tag
else:
return None
except Exception as e:
return None
def get_tag_by_name_and_user_id(
self, name: str, user_id: str
) -> Optional[TagModel]:
try:
tag = Tag.get(Tag.name == name, Tag.user_id == user_id)
return TagModel(**model_to_dict(tag))
except Exception as e:
return None
def add_tag_to_chat(
self, user_id: str, form_data: ChatIdTagForm
) -> Optional[ChatIdTagModel]:
tag = self.get_tag_by_name_and_user_id(form_data.tag_name, user_id)
if tag == None:
tag = self.insert_new_tag(form_data.tag_name, user_id)
id = str(uuid.uuid4())
chatIdTag = ChatIdTagModel(
**{
"id": id,
"user_id": user_id,
"chat_id": form_data.chat_id,
"tag_name": tag.name,
"timestamp": int(time.time()),
}
)
try:
result = ChatIdTag.create(**chatIdTag.model_dump())
if result:
return chatIdTag
else:
return None
except:
return None
def get_tags_by_user_id(self, user_id: str) -> List[TagModel]:
tag_names = [
ChatIdTagModel(**model_to_dict(chat_id_tag)).tag_name
for chat_id_tag in ChatIdTag.select()
.where(ChatIdTag.user_id == user_id)
.order_by(ChatIdTag.timestamp.desc())
]
return [
TagModel(**model_to_dict(tag))
for tag in Tag.select()
.where(Tag.user_id == user_id)
.where(Tag.name.in_(tag_names))
]
def get_tags_by_chat_id_and_user_id(
self, chat_id: str, user_id: str
) -> List[TagModel]:
tag_names = [
ChatIdTagModel(**model_to_dict(chat_id_tag)).tag_name
for chat_id_tag in ChatIdTag.select()
.where((ChatIdTag.user_id == user_id) & (ChatIdTag.chat_id == chat_id))
.order_by(ChatIdTag.timestamp.desc())
]
return [
TagModel(**model_to_dict(tag))
for tag in Tag.select()
.where(Tag.user_id == user_id)
.where(Tag.name.in_(tag_names))
]
def get_chat_ids_by_tag_name_and_user_id(
self, tag_name: str, user_id: str
) -> Optional[ChatIdTagModel]:
return [
ChatIdTagModel(**model_to_dict(chat_id_tag))
for chat_id_tag in ChatIdTag.select()
.where((ChatIdTag.user_id == user_id) & (ChatIdTag.tag_name == tag_name))
.order_by(ChatIdTag.timestamp.desc())
]
def count_chat_ids_by_tag_name_and_user_id(
self, tag_name: str, user_id: str
) -> int:
return (
ChatIdTag.select()
.where((ChatIdTag.tag_name == tag_name) & (ChatIdTag.user_id == user_id))
.count()
)
def delete_tag_by_tag_name_and_user_id(self, tag_name: str, user_id: str) -> bool:
try:
query = ChatIdTag.delete().where(
(ChatIdTag.tag_name == tag_name) & (ChatIdTag.user_id == user_id)
)
res = query.execute() # Remove the rows, return number of rows removed.
log.debug(f"res: {res}")
tag_count = self.count_chat_ids_by_tag_name_and_user_id(tag_name, user_id)
if tag_count == 0:
# Remove tag item from Tag col as well
query = Tag.delete().where(
(Tag.name == tag_name) & (Tag.user_id == user_id)
)
query.execute() # Remove the rows, return number of rows removed.
return True
except Exception as e:
log.error(f"delete_tag: {e}")
return False
def delete_tag_by_tag_name_and_chat_id_and_user_id(
self, tag_name: str, chat_id: str, user_id: str
) -> bool:
try:
query = ChatIdTag.delete().where(
(ChatIdTag.tag_name == tag_name)
& (ChatIdTag.chat_id == chat_id)
& (ChatIdTag.user_id == user_id)
)
res = query.execute() # Remove the rows, return number of rows removed.
log.debug(f"res: {res}")
tag_count = self.count_chat_ids_by_tag_name_and_user_id(tag_name, user_id)
if tag_count == 0:
# Remove tag item from Tag col as well
query = Tag.delete().where(
(Tag.name == tag_name) & (Tag.user_id == user_id)
)
query.execute() # Remove the rows, return number of rows removed.
return True
except Exception as e:
log.error(f"delete_tag: {e}")
return False
def delete_tags_by_chat_id_and_user_id(self, chat_id: str, user_id: str) -> bool:
tags = self.get_tags_by_chat_id_and_user_id(chat_id, user_id)
for tag in tags:
self.delete_tag_by_tag_name_and_chat_id_and_user_id(
tag.tag_name, chat_id, user_id
)
return True
Tags = TagTable(DB)

View File

@@ -1,210 +0,0 @@
from pydantic import BaseModel
from peewee import *
from playhouse.shortcuts import model_to_dict
from typing import List, Union, Optional
import time
from utils.misc import get_gravatar_url
from apps.web.internal.db import DB
from apps.web.models.chats import Chats
####################
# User DB Schema
####################
class User(Model):
id = CharField(unique=True)
name = CharField()
email = CharField()
role = CharField()
profile_image_url = TextField()
last_active_at = BigIntegerField()
updated_at = BigIntegerField()
created_at = BigIntegerField()
api_key = CharField(null=True, unique=True)
class Meta:
database = DB
class UserModel(BaseModel):
id: str
name: str
email: str
role: str = "pending"
profile_image_url: str
last_active_at: int # timestamp in epoch
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
api_key: Optional[str] = None
####################
# Forms
####################
class UserRoleUpdateForm(BaseModel):
id: str
role: str
class UserUpdateForm(BaseModel):
name: str
email: str
profile_image_url: str
password: Optional[str] = None
class UsersTable:
def __init__(self, db):
self.db = db
self.db.create_tables([User])
def insert_new_user(
self,
id: str,
name: str,
email: str,
profile_image_url: str = "/user.png",
role: str = "pending",
) -> Optional[UserModel]:
user = UserModel(
**{
"id": id,
"name": name,
"email": email,
"role": role,
"profile_image_url": profile_image_url,
"last_active_at": int(time.time()),
"created_at": int(time.time()),
"updated_at": int(time.time()),
}
)
result = User.create(**user.model_dump())
if result:
return user
else:
return None
def get_user_by_id(self, id: str) -> Optional[UserModel]:
try:
user = User.get(User.id == id)
return UserModel(**model_to_dict(user))
except:
return None
def get_user_by_api_key(self, api_key: str) -> Optional[UserModel]:
try:
user = User.get(User.api_key == api_key)
return UserModel(**model_to_dict(user))
except:
return None
def get_user_by_email(self, email: str) -> Optional[UserModel]:
try:
user = User.get(User.email == email)
return UserModel(**model_to_dict(user))
except:
return None
def get_users(self, skip: int = 0, limit: int = 50) -> List[UserModel]:
return [
UserModel(**model_to_dict(user))
for user in User.select()
# .limit(limit).offset(skip)
]
def get_num_users(self) -> Optional[int]:
return User.select().count()
def get_first_user(self) -> UserModel:
try:
user = User.select().order_by(User.created_at).first()
return UserModel(**model_to_dict(user))
except:
return None
def update_user_role_by_id(self, id: str, role: str) -> Optional[UserModel]:
try:
query = User.update(role=role).where(User.id == id)
query.execute()
user = User.get(User.id == id)
return UserModel(**model_to_dict(user))
except:
return None
def update_user_profile_image_url_by_id(
self, id: str, profile_image_url: str
) -> Optional[UserModel]:
try:
query = User.update(profile_image_url=profile_image_url).where(
User.id == id
)
query.execute()
user = User.get(User.id == id)
return UserModel(**model_to_dict(user))
except:
return None
def update_user_last_active_by_id(self, id: str) -> Optional[UserModel]:
try:
query = User.update(last_active_at=int(time.time())).where(User.id == id)
query.execute()
user = User.get(User.id == id)
return UserModel(**model_to_dict(user))
except:
return None
def update_user_by_id(self, id: str, updated: dict) -> Optional[UserModel]:
try:
query = User.update(**updated).where(User.id == id)
query.execute()
user = User.get(User.id == id)
return UserModel(**model_to_dict(user))
except:
return None
def delete_user_by_id(self, id: str) -> bool:
try:
# Delete User Chats
result = Chats.delete_chats_by_user_id(id)
if result:
# Delete User
query = User.delete().where(User.id == id)
query.execute() # Remove the rows, return number of rows removed.
return True
else:
return False
except:
return False
def update_user_api_key_by_id(self, id: str, api_key: str) -> str:
try:
query = User.update(api_key=api_key).where(User.id == id)
result = query.execute()
return True if result == 1 else False
except:
return False
def get_user_api_key_by_id(self, id: str) -> Optional[str]:
try:
user = User.get(User.id == id)
return user.api_key
except:
return None
Users = UsersTable(DB)

View File

@@ -1,124 +0,0 @@
from fastapi import Depends, FastAPI, HTTPException, status
from datetime import datetime, timedelta
from typing import List, Union, Optional
from fastapi import APIRouter
from pydantic import BaseModel
import json
from apps.web.models.modelfiles import (
Modelfiles,
ModelfileForm,
ModelfileTagNameForm,
ModelfileUpdateForm,
ModelfileResponse,
)
from utils.utils import get_current_user, get_admin_user
from constants import ERROR_MESSAGES
router = APIRouter()
############################
# GetModelfiles
############################
@router.get("/", response_model=List[ModelfileResponse])
async def get_modelfiles(
skip: int = 0, limit: int = 50, user=Depends(get_current_user)
):
return Modelfiles.get_modelfiles(skip, limit)
############################
# CreateNewModelfile
############################
@router.post("/create", response_model=Optional[ModelfileResponse])
async def create_new_modelfile(form_data: ModelfileForm, user=Depends(get_admin_user)):
modelfile = Modelfiles.insert_new_modelfile(user.id, form_data)
if modelfile:
return ModelfileResponse(
**{
**modelfile.model_dump(),
"modelfile": json.loads(modelfile.modelfile),
}
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.DEFAULT(),
)
############################
# GetModelfileByTagName
############################
@router.post("/", response_model=Optional[ModelfileResponse])
async def get_modelfile_by_tag_name(
form_data: ModelfileTagNameForm, user=Depends(get_current_user)
):
modelfile = Modelfiles.get_modelfile_by_tag_name(form_data.tag_name)
if modelfile:
return ModelfileResponse(
**{
**modelfile.model_dump(),
"modelfile": json.loads(modelfile.modelfile),
}
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# UpdateModelfileByTagName
############################
@router.post("/update", response_model=Optional[ModelfileResponse])
async def update_modelfile_by_tag_name(
form_data: ModelfileUpdateForm, user=Depends(get_admin_user)
):
modelfile = Modelfiles.get_modelfile_by_tag_name(form_data.tag_name)
if modelfile:
updated_modelfile = {
**json.loads(modelfile.modelfile),
**form_data.modelfile,
}
modelfile = Modelfiles.update_modelfile_by_tag_name(
form_data.tag_name, updated_modelfile
)
return ModelfileResponse(
**{
**modelfile.model_dump(),
"modelfile": json.loads(modelfile.modelfile),
}
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
############################
# DeleteModelfileByTagName
############################
@router.delete("/delete", response_model=bool)
async def delete_modelfile_by_tag_name(
form_data: ModelfileTagNameForm, user=Depends(get_admin_user)
):
result = Modelfiles.delete_modelfile_by_tag_name(form_data.tag_name)
return result

View File

@@ -0,0 +1,110 @@
import os
import logging
import json
from contextlib import contextmanager
from peewee_migrate import Router
from apps.webui.internal.wrappers import register_connection
from typing import Optional, Any
from typing_extensions import Self
from sqlalchemy import create_engine, types, Dialect
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy.sql.type_api import _T
from config import SRC_LOG_LEVELS, DATA_DIR, DATABASE_URL, BACKEND_DIR
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["DB"])
class JSONField(types.TypeDecorator):
impl = types.Text
cache_ok = True
def process_bind_param(self, value: Optional[_T], dialect: Dialect) -> Any:
return json.dumps(value)
def process_result_value(self, value: Optional[_T], dialect: Dialect) -> Any:
if value is not None:
return json.loads(value)
def copy(self, **kw: Any) -> Self:
return JSONField(self.impl.length)
def db_value(self, value):
return json.dumps(value)
def python_value(self, value):
if value is not None:
return json.loads(value)
# Check if the file exists
if os.path.exists(f"{DATA_DIR}/ollama.db"):
# Rename the file
os.rename(f"{DATA_DIR}/ollama.db", f"{DATA_DIR}/webui.db")
log.info("Database migrated from Ollama-WebUI successfully.")
else:
pass
# Workaround to handle the peewee migration
# This is required to ensure the peewee migration is handled before the alembic migration
def handle_peewee_migration(DATABASE_URL):
try:
# Replace the postgresql:// with postgres:// and %40 with @ in the DATABASE_URL
db = register_connection(
DATABASE_URL.replace("postgresql://", "postgres://").replace("%40", "@")
)
migrate_dir = BACKEND_DIR / "apps" / "webui" / "internal" / "migrations"
router = Router(db, logger=log, migrate_dir=migrate_dir)
router.run()
db.close()
# check if db connection has been closed
except Exception as e:
log.error(f"Failed to initialize the database connection: {e}")
raise
finally:
# Properly closing the database connection
if db and not db.is_closed():
db.close()
# Assert if db connection has been closed
assert db.is_closed(), "Database connection is still open."
handle_peewee_migration(DATABASE_URL)
SQLALCHEMY_DATABASE_URL = DATABASE_URL
if "sqlite" in SQLALCHEMY_DATABASE_URL:
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
else:
engine = create_engine(SQLALCHEMY_DATABASE_URL, pool_pre_ping=True)
SessionLocal = sessionmaker(
autocommit=False, autoflush=False, bind=engine, expire_on_commit=False
)
Base = declarative_base()
Session = scoped_session(SessionLocal)
# Dependency
def get_session():
db = SessionLocal()
try:
yield db
finally:
db.close()
get_db = contextmanager(get_session)

View File

@@ -0,0 +1,61 @@
"""Peewee migrations -- 009_add_models.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
@migrator.create_model
class Model(pw.Model):
id = pw.TextField(unique=True)
user_id = pw.TextField()
base_model_id = pw.TextField(null=True)
name = pw.TextField()
meta = pw.TextField()
params = pw.TextField()
created_at = pw.BigIntegerField(null=False)
updated_at = pw.BigIntegerField(null=False)
class Meta:
table_name = "model"
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_model("model")

View File

@@ -0,0 +1,130 @@
"""Peewee migrations -- 009_add_models.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
import json
from utils.misc import parse_ollama_modelfile
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
# Fetch data from 'modelfile' table and insert into 'model' table
migrate_modelfile_to_model(migrator, database)
# Drop the 'modelfile' table
migrator.remove_model("modelfile")
def migrate_modelfile_to_model(migrator: Migrator, database: pw.Database):
ModelFile = migrator.orm["modelfile"]
Model = migrator.orm["model"]
modelfiles = ModelFile.select()
for modelfile in modelfiles:
# Extract and transform data in Python
modelfile.modelfile = json.loads(modelfile.modelfile)
meta = json.dumps(
{
"description": modelfile.modelfile.get("desc"),
"profile_image_url": modelfile.modelfile.get("imageUrl"),
"ollama": {"modelfile": modelfile.modelfile.get("content")},
"suggestion_prompts": modelfile.modelfile.get("suggestionPrompts"),
"categories": modelfile.modelfile.get("categories"),
"user": {**modelfile.modelfile.get("user", {}), "community": True},
}
)
info = parse_ollama_modelfile(modelfile.modelfile.get("content"))
# Insert the processed data into the 'model' table
Model.create(
id=f"ollama-{modelfile.tag_name}",
user_id=modelfile.user_id,
base_model_id=info.get("base_model_id"),
name=modelfile.modelfile.get("title"),
meta=meta,
params=json.dumps(info.get("params", {})),
created_at=modelfile.timestamp,
updated_at=modelfile.timestamp,
)
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
recreate_modelfile_table(migrator, database)
move_data_back_to_modelfile(migrator, database)
migrator.remove_model("model")
def recreate_modelfile_table(migrator: Migrator, database: pw.Database):
query = """
CREATE TABLE IF NOT EXISTS modelfile (
user_id TEXT,
tag_name TEXT,
modelfile JSON,
timestamp BIGINT
)
"""
migrator.sql(query)
def move_data_back_to_modelfile(migrator: Migrator, database: pw.Database):
Model = migrator.orm["model"]
Modelfile = migrator.orm["modelfile"]
models = Model.select()
for model in models:
# Extract and transform data in Python
meta = json.loads(model.meta)
modelfile_data = {
"title": model.name,
"desc": meta.get("description"),
"imageUrl": meta.get("profile_image_url"),
"content": meta.get("ollama", {}).get("modelfile"),
"suggestionPrompts": meta.get("suggestion_prompts"),
"categories": meta.get("categories"),
"user": {k: v for k, v in meta.get("user", {}).items() if k != "community"},
}
# Insert the processed data back into the 'modelfile' table
Modelfile.create(
user_id=model.user_id,
tag_name=model.id,
modelfile=modelfile_data,
timestamp=model.created_at,
)

View File

@@ -0,0 +1,48 @@
"""Peewee migrations -- 002_add_local_sharing.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
# Adding fields settings to the 'user' table
migrator.add_fields("user", settings=pw.TextField(null=True))
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
# Remove the settings field
migrator.remove_fields("user", "settings")

View File

@@ -0,0 +1,61 @@
"""Peewee migrations -- 009_add_models.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
@migrator.create_model
class Tool(pw.Model):
id = pw.TextField(unique=True)
user_id = pw.TextField()
name = pw.TextField()
content = pw.TextField()
specs = pw.TextField()
meta = pw.TextField()
created_at = pw.BigIntegerField(null=False)
updated_at = pw.BigIntegerField(null=False)
class Meta:
table_name = "tool"
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_model("tool")

View File

@@ -0,0 +1,48 @@
"""Peewee migrations -- 002_add_local_sharing.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
# Adding fields info to the 'user' table
migrator.add_fields("user", info=pw.TextField(null=True))
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
# Remove the settings field
migrator.remove_fields("user", "info")

View File

@@ -0,0 +1,55 @@
"""Peewee migrations -- 009_add_models.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
@migrator.create_model
class File(pw.Model):
id = pw.TextField(unique=True)
user_id = pw.TextField()
filename = pw.TextField()
meta = pw.TextField()
created_at = pw.BigIntegerField(null=False)
class Meta:
table_name = "file"
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_model("file")

View File

@@ -0,0 +1,61 @@
"""Peewee migrations -- 009_add_models.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
@migrator.create_model
class Function(pw.Model):
id = pw.TextField(unique=True)
user_id = pw.TextField()
name = pw.TextField()
type = pw.TextField()
content = pw.TextField()
meta = pw.TextField()
created_at = pw.BigIntegerField(null=False)
updated_at = pw.BigIntegerField(null=False)
class Meta:
table_name = "function"
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_model("function")

View File

@@ -0,0 +1,50 @@
"""Peewee migrations -- 009_add_models.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
migrator.add_fields("tool", valves=pw.TextField(null=True))
migrator.add_fields("function", valves=pw.TextField(null=True))
migrator.add_fields("function", is_active=pw.BooleanField(default=False))
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_fields("tool", "valves")
migrator.remove_fields("function", "valves")
migrator.remove_fields("function", "is_active")

View File

@@ -0,0 +1,45 @@
"""Peewee migrations -- 017_add_user_oauth_sub.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
migrator.add_fields(
"user",
oauth_sub=pw.TextField(null=True, unique=True),
)
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_fields("user", "oauth_sub")

View File

@@ -0,0 +1,49 @@
"""Peewee migrations -- 017_add_user_oauth_sub.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
migrator.add_fields(
"function",
is_global=pw.BooleanField(default=False),
)
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_fields("function", "is_global")

View File

@@ -0,0 +1,72 @@
from contextvars import ContextVar
from peewee import *
from peewee import PostgresqlDatabase, InterfaceError as PeeWeeInterfaceError
import logging
from playhouse.db_url import connect, parse
from playhouse.shortcuts import ReconnectMixin
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["DB"])
db_state_default = {"closed": None, "conn": None, "ctx": None, "transactions": None}
db_state = ContextVar("db_state", default=db_state_default.copy())
class PeeweeConnectionState(object):
def __init__(self, **kwargs):
super().__setattr__("_state", db_state)
super().__init__(**kwargs)
def __setattr__(self, name, value):
self._state.get()[name] = value
def __getattr__(self, name):
value = self._state.get()[name]
return value
class CustomReconnectMixin(ReconnectMixin):
reconnect_errors = (
# psycopg2
(OperationalError, "termin"),
(InterfaceError, "closed"),
# peewee
(PeeWeeInterfaceError, "closed"),
)
class ReconnectingPostgresqlDatabase(CustomReconnectMixin, PostgresqlDatabase):
pass
def register_connection(db_url):
db = connect(db_url)
if isinstance(db, PostgresqlDatabase):
# Enable autoconnect for SQLite databases, managed by Peewee
db.autoconnect = True
db.reuse_if_open = True
log.info("Connected to PostgreSQL database")
# Get the connection details
connection = parse(db_url)
# Use our custom database class that supports reconnection
db = ReconnectingPostgresqlDatabase(
connection["database"],
user=connection["user"],
password=connection["password"],
host=connection["host"],
port=connection["port"],
)
db.connect(reuse_if_open=True)
elif isinstance(db, SqliteDatabase):
# Enable autoconnect for SQLite databases, managed by Peewee
db.autoconnect = True
db.reuse_if_open = True
log.info("Connected to SQLite database")
else:
raise ValueError("Unsupported database connection")
return db

420
backend/apps/webui/main.py Normal file
View File

@@ -0,0 +1,420 @@
from fastapi import FastAPI, Depends
from fastapi.routing import APIRoute
from fastapi.responses import StreamingResponse
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.sessions import SessionMiddleware
from sqlalchemy.orm import Session
from apps.webui.routers import (
auths,
users,
chats,
documents,
tools,
models,
prompts,
configs,
memories,
utils,
files,
functions,
)
from apps.webui.models.functions import Functions
from apps.webui.models.models import Models
from apps.webui.utils import load_function_module_by_id
from utils.misc import stream_message_template
from utils.task import prompt_template
from config import (
WEBUI_BUILD_HASH,
SHOW_ADMIN_DETAILS,
ADMIN_EMAIL,
WEBUI_AUTH,
DEFAULT_MODELS,
DEFAULT_PROMPT_SUGGESTIONS,
DEFAULT_USER_ROLE,
ENABLE_SIGNUP,
USER_PERMISSIONS,
WEBHOOK_URL,
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
WEBUI_AUTH_TRUSTED_NAME_HEADER,
JWT_EXPIRES_IN,
WEBUI_BANNERS,
ENABLE_COMMUNITY_SHARING,
AppConfig,
OAUTH_USERNAME_CLAIM,
OAUTH_PICTURE_CLAIM,
)
import inspect
import uuid
import time
import json
from typing import Iterator, Generator
from pydantic import BaseModel
app = FastAPI()
origins = ["*"]
app.state.config = AppConfig()
app.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP
app.state.config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
app.state.AUTH_TRUSTED_NAME_HEADER = WEBUI_AUTH_TRUSTED_NAME_HEADER
app.state.config.SHOW_ADMIN_DETAILS = SHOW_ADMIN_DETAILS
app.state.config.ADMIN_EMAIL = ADMIN_EMAIL
app.state.config.DEFAULT_MODELS = DEFAULT_MODELS
app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
app.state.config.USER_PERMISSIONS = USER_PERMISSIONS
app.state.config.WEBHOOK_URL = WEBHOOK_URL
app.state.config.BANNERS = WEBUI_BANNERS
app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
app.state.config.OAUTH_USERNAME_CLAIM = OAUTH_USERNAME_CLAIM
app.state.config.OAUTH_PICTURE_CLAIM = OAUTH_PICTURE_CLAIM
app.state.MODELS = {}
app.state.TOOLS = {}
app.state.FUNCTIONS = {}
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(configs.router, prefix="/configs", tags=["configs"])
app.include_router(auths.router, prefix="/auths", tags=["auths"])
app.include_router(users.router, prefix="/users", tags=["users"])
app.include_router(chats.router, prefix="/chats", tags=["chats"])
app.include_router(documents.router, prefix="/documents", tags=["documents"])
app.include_router(models.router, prefix="/models", tags=["models"])
app.include_router(prompts.router, prefix="/prompts", tags=["prompts"])
app.include_router(memories.router, prefix="/memories", tags=["memories"])
app.include_router(files.router, prefix="/files", tags=["files"])
app.include_router(tools.router, prefix="/tools", tags=["tools"])
app.include_router(functions.router, prefix="/functions", tags=["functions"])
app.include_router(utils.router, prefix="/utils", tags=["utils"])
@app.get("/")
async def get_status():
return {
"status": True,
"auth": WEBUI_AUTH,
"default_models": app.state.config.DEFAULT_MODELS,
"default_prompt_suggestions": app.state.config.DEFAULT_PROMPT_SUGGESTIONS,
}
async def get_pipe_models():
pipes = Functions.get_functions_by_type("pipe", active_only=True)
pipe_models = []
for pipe in pipes:
# Check if function is already loaded
if pipe.id not in app.state.FUNCTIONS:
function_module, function_type, frontmatter = load_function_module_by_id(
pipe.id
)
app.state.FUNCTIONS[pipe.id] = function_module
else:
function_module = app.state.FUNCTIONS[pipe.id]
if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
valves = Functions.get_function_valves_by_id(pipe.id)
function_module.valves = function_module.Valves(
**(valves if valves else {})
)
# Check if function is a manifold
if hasattr(function_module, "type"):
if function_module.type == "manifold":
manifold_pipes = []
# Check if pipes is a function or a list
if callable(function_module.pipes):
manifold_pipes = function_module.pipes()
else:
manifold_pipes = function_module.pipes
for p in manifold_pipes:
manifold_pipe_id = f'{pipe.id}.{p["id"]}'
manifold_pipe_name = p["name"]
if hasattr(function_module, "name"):
manifold_pipe_name = (
f"{function_module.name}{manifold_pipe_name}"
)
pipe_models.append(
{
"id": manifold_pipe_id,
"name": manifold_pipe_name,
"object": "model",
"created": pipe.created_at,
"owned_by": "openai",
"pipe": {"type": pipe.type},
}
)
else:
pipe_models.append(
{
"id": pipe.id,
"name": pipe.name,
"object": "model",
"created": pipe.created_at,
"owned_by": "openai",
"pipe": {"type": "pipe"},
}
)
return pipe_models
async def generate_function_chat_completion(form_data, user):
model_id = form_data.get("model")
model_info = Models.get_model_by_id(model_id)
if model_info:
if model_info.base_model_id:
form_data["model"] = model_info.base_model_id
model_info.params = model_info.params.model_dump()
if model_info.params:
if model_info.params.get("temperature", None) is not None:
form_data["temperature"] = float(model_info.params.get("temperature"))
if model_info.params.get("top_p", None):
form_data["top_p"] = int(model_info.params.get("top_p", None))
if model_info.params.get("max_tokens", None):
form_data["max_tokens"] = int(model_info.params.get("max_tokens", None))
if model_info.params.get("frequency_penalty", None):
form_data["frequency_penalty"] = int(
model_info.params.get("frequency_penalty", None)
)
if model_info.params.get("seed", None):
form_data["seed"] = model_info.params.get("seed", None)
if model_info.params.get("stop", None):
form_data["stop"] = (
[
bytes(stop, "utf-8").decode("unicode_escape")
for stop in model_info.params["stop"]
]
if model_info.params.get("stop", None)
else None
)
system = model_info.params.get("system", None)
if system:
system = prompt_template(
system,
**(
{
"user_name": user.name,
"user_location": (
user.info.get("location") if user.info else None
),
}
if user
else {}
),
)
# Check if the payload already has a system message
# If not, add a system message to the payload
if form_data.get("messages"):
for message in form_data["messages"]:
if message.get("role") == "system":
message["content"] = system + message["content"]
break
else:
form_data["messages"].insert(
0,
{
"role": "system",
"content": system,
},
)
else:
pass
async def job():
pipe_id = form_data["model"]
if "." in pipe_id:
pipe_id, sub_pipe_id = pipe_id.split(".", 1)
print(pipe_id)
# Check if function is already loaded
if pipe_id not in app.state.FUNCTIONS:
function_module, function_type, frontmatter = load_function_module_by_id(
pipe_id
)
app.state.FUNCTIONS[pipe_id] = function_module
else:
function_module = app.state.FUNCTIONS[pipe_id]
if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
valves = Functions.get_function_valves_by_id(pipe_id)
function_module.valves = function_module.Valves(
**(valves if valves else {})
)
pipe = function_module.pipe
# Get the signature of the function
sig = inspect.signature(pipe)
params = {"body": form_data}
if "__user__" in sig.parameters:
__user__ = {
"id": user.id,
"email": user.email,
"name": user.name,
"role": user.role,
}
try:
if hasattr(function_module, "UserValves"):
__user__["valves"] = function_module.UserValves(
**Functions.get_user_valves_by_id_and_user_id(pipe_id, user.id)
)
except Exception as e:
print(e)
params = {**params, "__user__": __user__}
if form_data["stream"]:
async def stream_content():
try:
if inspect.iscoroutinefunction(pipe):
res = await pipe(**params)
else:
res = pipe(**params)
# Directly return if the response is a StreamingResponse
if isinstance(res, StreamingResponse):
async for data in res.body_iterator:
yield data
return
if isinstance(res, dict):
yield f"data: {json.dumps(res)}\n\n"
return
except Exception as e:
print(f"Error: {e}")
yield f"data: {json.dumps({'error': {'detail':str(e)}})}\n\n"
return
if isinstance(res, str):
message = stream_message_template(form_data["model"], res)
yield f"data: {json.dumps(message)}\n\n"
if isinstance(res, Iterator):
for line in res:
if isinstance(line, BaseModel):
line = line.model_dump_json()
line = f"data: {line}"
if isinstance(line, dict):
line = f"data: {json.dumps(line)}"
try:
line = line.decode("utf-8")
except:
pass
if line.startswith("data:"):
yield f"{line}\n\n"
else:
line = stream_message_template(form_data["model"], line)
yield f"data: {json.dumps(line)}\n\n"
if isinstance(res, str) or isinstance(res, Generator):
finish_message = {
"id": f"{form_data['model']}-{str(uuid.uuid4())}",
"object": "chat.completion.chunk",
"created": int(time.time()),
"model": form_data["model"],
"choices": [
{
"index": 0,
"delta": {},
"logprobs": None,
"finish_reason": "stop",
}
],
}
yield f"data: {json.dumps(finish_message)}\n\n"
yield f"data: [DONE]"
return StreamingResponse(stream_content(), media_type="text/event-stream")
else:
try:
if inspect.iscoroutinefunction(pipe):
res = await pipe(**params)
else:
res = pipe(**params)
if isinstance(res, StreamingResponse):
return res
except Exception as e:
print(f"Error: {e}")
return {"error": {"detail": str(e)}}
if isinstance(res, dict):
return res
elif isinstance(res, BaseModel):
return res.model_dump()
else:
message = ""
if isinstance(res, str):
message = res
if isinstance(res, Generator):
for stream in res:
message = f"{message}{stream}"
return {
"id": f"{form_data['model']}-{str(uuid.uuid4())}",
"object": "chat.completion",
"created": int(time.time()),
"model": form_data["model"],
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": message,
},
"logprobs": None,
"finish_reason": "stop",
}
],
}
return await job()

View File

@@ -1,14 +1,13 @@
from pydantic import BaseModel
from typing import List, Union, Optional
import time
from typing import Optional
import uuid
import logging
from peewee import *
from sqlalchemy import String, Column, Boolean, Text
from apps.web.models.users import UserModel, Users
from apps.webui.models.users import UserModel, Users
from utils.utils import verify_password
from apps.web.internal.db import DB
from apps.webui.internal.db import Base, get_db
from config import SRC_LOG_LEVELS
@@ -20,14 +19,13 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
class Auth(Model):
id = CharField(unique=True)
email = CharField()
password = TextField()
active = BooleanField()
class Auth(Base):
__tablename__ = "auth"
class Meta:
database = DB
id = Column(String, primary_key=True)
email = Column(String)
password = Column(Text)
active = Column(Boolean)
class AuthModel(BaseModel):
@@ -94,9 +92,6 @@ class AddUserForm(SignupForm):
class AuthsTable:
def __init__(self, db):
self.db = db
self.db.create_tables([Auth])
def insert_new_auth(
self,
@@ -105,35 +100,46 @@ class AuthsTable:
name: str,
profile_image_url: str = "/user.png",
role: str = "pending",
oauth_sub: Optional[str] = None,
) -> Optional[UserModel]:
log.info("insert_new_auth")
with get_db() as db:
id = str(uuid.uuid4())
log.info("insert_new_auth")
auth = AuthModel(
**{"id": id, "email": email, "password": password, "active": True}
)
result = Auth.create(**auth.model_dump())
id = str(uuid.uuid4())
user = Users.insert_new_user(id, name, email, profile_image_url, role)
auth = AuthModel(
**{"id": id, "email": email, "password": password, "active": True}
)
result = Auth(**auth.model_dump())
db.add(result)
if result and user:
return user
else:
return None
user = Users.insert_new_user(
id, name, email, profile_image_url, role, oauth_sub
)
db.commit()
db.refresh(result)
if result and user:
return user
else:
return None
def authenticate_user(self, email: str, password: str) -> Optional[UserModel]:
log.info(f"authenticate_user: {email}")
try:
auth = Auth.get(Auth.email == email, Auth.active == True)
if auth:
if verify_password(password, auth.password):
user = Users.get_user_by_id(auth.id)
return user
with get_db() as db:
auth = db.query(Auth).filter_by(email=email, active=True).first()
if auth:
if verify_password(password, auth.password):
user = Users.get_user_by_id(auth.id)
return user
else:
return None
else:
return None
else:
return None
except:
return None
@@ -152,46 +158,50 @@ class AuthsTable:
def authenticate_user_by_trusted_header(self, email: str) -> Optional[UserModel]:
log.info(f"authenticate_user_by_trusted_header: {email}")
try:
auth = Auth.get(Auth.email == email, Auth.active == True)
if auth:
user = Users.get_user_by_id(auth.id)
return user
with get_db() as db:
auth = db.query(Auth).filter(email=email, active=True).first()
if auth:
user = Users.get_user_by_id(auth.id)
return user
except:
return None
def update_user_password_by_id(self, id: str, new_password: str) -> bool:
try:
query = Auth.update(password=new_password).where(Auth.id == id)
result = query.execute()
return True if result == 1 else False
with get_db() as db:
result = (
db.query(Auth).filter_by(id=id).update({"password": new_password})
)
db.commit()
return True if result == 1 else False
except:
return False
def update_email_by_id(self, id: str, email: str) -> bool:
try:
query = Auth.update(email=email).where(Auth.id == id)
result = query.execute()
return True if result == 1 else False
with get_db() as db:
result = db.query(Auth).filter_by(id=id).update({"email": email})
db.commit()
return True if result == 1 else False
except:
return False
def delete_auth_by_id(self, id: str) -> bool:
try:
# Delete User
result = Users.delete_user_by_id(id)
with get_db() as db:
if result:
# Delete Auth
query = Auth.delete().where(Auth.id == id)
query.execute() # Remove the rows, return number of rows removed.
# Delete User
result = Users.delete_user_by_id(id)
return True
else:
return False
if result:
db.query(Auth).filter_by(id=id).delete()
db.commit()
return True
else:
return False
except:
return False
Auths = AuthsTable(DB)
Auths = AuthsTable()

View File

@@ -0,0 +1,374 @@
from pydantic import BaseModel, ConfigDict
from typing import List, Union, Optional
import json
import uuid
import time
from sqlalchemy import Column, String, BigInteger, Boolean, Text
from apps.webui.internal.db import Base, get_db
####################
# Chat DB Schema
####################
class Chat(Base):
__tablename__ = "chat"
id = Column(String, primary_key=True)
user_id = Column(String)
title = Column(Text)
chat = Column(Text) # Save Chat JSON as Text
created_at = Column(BigInteger)
updated_at = Column(BigInteger)
share_id = Column(Text, unique=True, nullable=True)
archived = Column(Boolean, default=False)
class ChatModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: str
user_id: str
title: str
chat: str
created_at: int # timestamp in epoch
updated_at: int # timestamp in epoch
share_id: Optional[str] = None
archived: bool = False
####################
# Forms
####################
class ChatForm(BaseModel):
chat: dict
class ChatTitleForm(BaseModel):
title: str
class ChatResponse(BaseModel):
id: str
user_id: str
title: str
chat: dict
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
share_id: Optional[str] = None # id of the chat to be shared
archived: bool
class ChatTitleIdResponse(BaseModel):
id: str
title: str
updated_at: int
created_at: int
class ChatTable:
def insert_new_chat(self, user_id: str, form_data: ChatForm) -> Optional[ChatModel]:
with get_db() as db:
id = str(uuid.uuid4())
chat = ChatModel(
**{
"id": id,
"user_id": user_id,
"title": (
form_data.chat["title"]
if "title" in form_data.chat
else "New Chat"
),
"chat": json.dumps(form_data.chat),
"created_at": int(time.time()),
"updated_at": int(time.time()),
}
)
result = Chat(**chat.model_dump())
db.add(result)
db.commit()
db.refresh(result)
return ChatModel.model_validate(result) if result else None
def update_chat_by_id(self, id: str, chat: dict) -> Optional[ChatModel]:
try:
with get_db() as db:
chat_obj = db.get(Chat, id)
chat_obj.chat = json.dumps(chat)
chat_obj.title = chat["title"] if "title" in chat else "New Chat"
chat_obj.updated_at = int(time.time())
db.commit()
db.refresh(chat_obj)
return ChatModel.model_validate(chat_obj)
except Exception as e:
return None
def insert_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]:
with get_db() as db:
# Get the existing chat to share
chat = db.get(Chat, chat_id)
# Check if the chat is already shared
if chat.share_id:
return self.get_chat_by_id_and_user_id(chat.share_id, "shared")
# Create a new chat with the same data, but with a new ID
shared_chat = ChatModel(
**{
"id": str(uuid.uuid4()),
"user_id": f"shared-{chat_id}",
"title": chat.title,
"chat": chat.chat,
"created_at": chat.created_at,
"updated_at": int(time.time()),
}
)
shared_result = Chat(**shared_chat.model_dump())
db.add(shared_result)
db.commit()
db.refresh(shared_result)
# Update the original chat with the share_id
result = (
db.query(Chat)
.filter_by(id=chat_id)
.update({"share_id": shared_chat.id})
)
db.commit()
return shared_chat if (shared_result and result) else None
def update_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]:
try:
with get_db() as db:
print("update_shared_chat_by_id")
chat = db.get(Chat, chat_id)
print(chat)
chat.title = chat.title
chat.chat = chat.chat
db.commit()
db.refresh(chat)
return self.get_chat_by_id(chat.share_id)
except:
return None
def delete_shared_chat_by_chat_id(self, chat_id: str) -> bool:
try:
with get_db() as db:
db.query(Chat).filter_by(user_id=f"shared-{chat_id}").delete()
db.commit()
return True
except:
return False
def update_chat_share_id_by_id(
self, id: str, share_id: Optional[str]
) -> Optional[ChatModel]:
try:
with get_db() as db:
chat = db.get(Chat, id)
chat.share_id = share_id
db.commit()
db.refresh(chat)
return ChatModel.model_validate(chat)
except:
return None
def toggle_chat_archive_by_id(self, id: str) -> Optional[ChatModel]:
try:
with get_db() as db:
chat = db.get(Chat, id)
chat.archived = not chat.archived
db.commit()
db.refresh(chat)
return ChatModel.model_validate(chat)
except:
return None
def archive_all_chats_by_user_id(self, user_id: str) -> bool:
try:
with get_db() as db:
db.query(Chat).filter_by(user_id=user_id).update({"archived": True})
db.commit()
return True
except:
return False
def get_archived_chat_list_by_user_id(
self, user_id: str, skip: int = 0, limit: int = 50
) -> List[ChatModel]:
with get_db() as db:
all_chats = (
db.query(Chat)
.filter_by(user_id=user_id, archived=True)
.order_by(Chat.updated_at.desc())
# .limit(limit).offset(skip)
.all()
)
return [ChatModel.model_validate(chat) for chat in all_chats]
def get_chat_list_by_user_id(
self,
user_id: str,
include_archived: bool = False,
skip: int = 0,
limit: int = 50,
) -> List[ChatModel]:
with get_db() as db:
query = db.query(Chat).filter_by(user_id=user_id)
if not include_archived:
query = query.filter_by(archived=False)
all_chats = (
query.order_by(Chat.updated_at.desc())
# .limit(limit).offset(skip)
.all()
)
return [ChatModel.model_validate(chat) for chat in all_chats]
def get_chat_list_by_chat_ids(
self, chat_ids: List[str], skip: int = 0, limit: int = 50
) -> List[ChatModel]:
with get_db() as db:
all_chats = (
db.query(Chat)
.filter(Chat.id.in_(chat_ids))
.filter_by(archived=False)
.order_by(Chat.updated_at.desc())
.all()
)
return [ChatModel.model_validate(chat) for chat in all_chats]
def get_chat_by_id(self, id: str) -> Optional[ChatModel]:
try:
with get_db() as db:
chat = db.get(Chat, id)
return ChatModel.model_validate(chat)
except:
return None
def get_chat_by_share_id(self, id: str) -> Optional[ChatModel]:
try:
with get_db() as db:
chat = db.query(Chat).filter_by(share_id=id).first()
if chat:
return self.get_chat_by_id(id)
else:
return None
except Exception as e:
return None
def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]:
try:
with get_db() as db:
chat = db.query(Chat).filter_by(id=id, user_id=user_id).first()
return ChatModel.model_validate(chat)
except:
return None
def get_chats(self, skip: int = 0, limit: int = 50) -> List[ChatModel]:
with get_db() as db:
all_chats = (
db.query(Chat)
# .limit(limit).offset(skip)
.order_by(Chat.updated_at.desc())
)
return [ChatModel.model_validate(chat) for chat in all_chats]
def get_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
with get_db() as db:
all_chats = (
db.query(Chat)
.filter_by(user_id=user_id)
.order_by(Chat.updated_at.desc())
)
return [ChatModel.model_validate(chat) for chat in all_chats]
def get_archived_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
with get_db() as db:
all_chats = (
db.query(Chat)
.filter_by(user_id=user_id, archived=True)
.order_by(Chat.updated_at.desc())
)
return [ChatModel.model_validate(chat) for chat in all_chats]
def delete_chat_by_id(self, id: str) -> bool:
try:
with get_db() as db:
db.query(Chat).filter_by(id=id).delete()
db.commit()
return True and self.delete_shared_chat_by_chat_id(id)
except:
return False
def delete_chat_by_id_and_user_id(self, id: str, user_id: str) -> bool:
try:
with get_db() as db:
db.query(Chat).filter_by(id=id, user_id=user_id).delete()
db.commit()
return True and self.delete_shared_chat_by_chat_id(id)
except:
return False
def delete_chats_by_user_id(self, user_id: str) -> bool:
try:
with get_db() as db:
self.delete_shared_chats_by_user_id(user_id)
db.query(Chat).filter_by(user_id=user_id).delete()
db.commit()
return True
except:
return False
def delete_shared_chats_by_user_id(self, user_id: str) -> bool:
try:
with get_db() as db:
chats_by_user = db.query(Chat).filter_by(user_id=user_id).all()
shared_chat_ids = [f"shared-{chat.id}" for chat in chats_by_user]
db.query(Chat).filter(Chat.user_id.in_(shared_chat_ids)).delete()
db.commit()
return True
except:
return False
Chats = ChatTable()

View File

@@ -0,0 +1,167 @@
from pydantic import BaseModel, ConfigDict
from typing import List, Optional
import time
import logging
from sqlalchemy import String, Column, BigInteger, Text
from apps.webui.internal.db import Base, get_db
import json
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
# Documents DB Schema
####################
class Document(Base):
__tablename__ = "document"
collection_name = Column(String, primary_key=True)
name = Column(String, unique=True)
title = Column(Text)
filename = Column(Text)
content = Column(Text, nullable=True)
user_id = Column(String)
timestamp = Column(BigInteger)
class DocumentModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
collection_name: str
name: str
title: str
filename: str
content: Optional[str] = None
user_id: str
timestamp: int # timestamp in epoch
####################
# Forms
####################
class DocumentResponse(BaseModel):
collection_name: str
name: str
title: str
filename: str
content: Optional[dict] = None
user_id: str
timestamp: int # timestamp in epoch
class DocumentUpdateForm(BaseModel):
name: str
title: str
class DocumentForm(DocumentUpdateForm):
collection_name: str
filename: str
content: Optional[str] = None
class DocumentsTable:
def insert_new_doc(
self, user_id: str, form_data: DocumentForm
) -> Optional[DocumentModel]:
with get_db() as db:
document = DocumentModel(
**{
**form_data.model_dump(),
"user_id": user_id,
"timestamp": int(time.time()),
}
)
try:
result = Document(**document.model_dump())
db.add(result)
db.commit()
db.refresh(result)
if result:
return DocumentModel.model_validate(result)
else:
return None
except:
return None
def get_doc_by_name(self, name: str) -> Optional[DocumentModel]:
try:
with get_db() as db:
document = db.query(Document).filter_by(name=name).first()
return DocumentModel.model_validate(document) if document else None
except:
return None
def get_docs(self) -> List[DocumentModel]:
with get_db() as db:
return [
DocumentModel.model_validate(doc) for doc in db.query(Document).all()
]
def update_doc_by_name(
self, name: str, form_data: DocumentUpdateForm
) -> Optional[DocumentModel]:
try:
with get_db() as db:
db.query(Document).filter_by(name=name).update(
{
"title": form_data.title,
"name": form_data.name,
"timestamp": int(time.time()),
}
)
db.commit()
return self.get_doc_by_name(form_data.name)
except Exception as e:
log.exception(e)
return None
def update_doc_content_by_name(
self, name: str, updated: dict
) -> Optional[DocumentModel]:
try:
doc = self.get_doc_by_name(name)
doc_content = json.loads(doc.content if doc.content else "{}")
doc_content = {**doc_content, **updated}
with get_db() as db:
db.query(Document).filter_by(name=name).update(
{
"content": json.dumps(doc_content),
"timestamp": int(time.time()),
}
)
db.commit()
return self.get_doc_by_name(name)
except Exception as e:
log.exception(e)
return None
def delete_doc_by_name(self, name: str) -> bool:
try:
with get_db() as db:
db.query(Document).filter_by(name=name).delete()
db.commit()
return True
except:
return False
Documents = DocumentsTable()

View File

@@ -0,0 +1,126 @@
from pydantic import BaseModel, ConfigDict
from typing import List, Union, Optional
import time
import logging
from sqlalchemy import Column, String, BigInteger, Text
from apps.webui.internal.db import JSONField, Base, get_db
import json
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
# Files DB Schema
####################
class File(Base):
__tablename__ = "file"
id = Column(String, primary_key=True)
user_id = Column(String)
filename = Column(Text)
meta = Column(JSONField)
created_at = Column(BigInteger)
class FileModel(BaseModel):
id: str
user_id: str
filename: str
meta: dict
created_at: int # timestamp in epoch
model_config = ConfigDict(from_attributes=True)
####################
# Forms
####################
class FileModelResponse(BaseModel):
id: str
user_id: str
filename: str
meta: dict
created_at: int # timestamp in epoch
class FileForm(BaseModel):
id: str
filename: str
meta: dict = {}
class FilesTable:
def insert_new_file(self, user_id: str, form_data: FileForm) -> Optional[FileModel]:
with get_db() as db:
file = FileModel(
**{
**form_data.model_dump(),
"user_id": user_id,
"created_at": int(time.time()),
}
)
try:
result = File(**file.model_dump())
db.add(result)
db.commit()
db.refresh(result)
if result:
return FileModel.model_validate(result)
else:
return None
except Exception as e:
print(f"Error creating tool: {e}")
return None
def get_file_by_id(self, id: str) -> Optional[FileModel]:
with get_db() as db:
try:
file = db.get(File, id)
return FileModel.model_validate(file)
except:
return None
def get_files(self) -> List[FileModel]:
with get_db() as db:
return [FileModel.model_validate(file) for file in db.query(File).all()]
def delete_file_by_id(self, id: str) -> bool:
with get_db() as db:
try:
db.query(File).filter_by(id=id).delete()
db.commit()
return True
except:
return False
def delete_all_files(self) -> bool:
with get_db() as db:
try:
db.query(File).delete()
db.commit()
return True
except:
return False
Files = FilesTable()

View File

@@ -0,0 +1,279 @@
from pydantic import BaseModel, ConfigDict
from typing import List, Union, Optional
import time
import logging
from sqlalchemy import Column, String, Text, BigInteger, Boolean
from apps.webui.internal.db import JSONField, Base, get_db
from apps.webui.models.users import Users
import json
import copy
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
# Functions DB Schema
####################
class Function(Base):
__tablename__ = "function"
id = Column(String, primary_key=True)
user_id = Column(String)
name = Column(Text)
type = Column(Text)
content = Column(Text)
meta = Column(JSONField)
valves = Column(JSONField)
is_active = Column(Boolean)
is_global = Column(Boolean)
updated_at = Column(BigInteger)
created_at = Column(BigInteger)
class FunctionMeta(BaseModel):
description: Optional[str] = None
manifest: Optional[dict] = {}
class FunctionModel(BaseModel):
id: str
user_id: str
name: str
type: str
content: str
meta: FunctionMeta
is_active: bool = False
is_global: bool = False
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
model_config = ConfigDict(from_attributes=True)
####################
# Forms
####################
class FunctionResponse(BaseModel):
id: str
user_id: str
type: str
name: str
meta: FunctionMeta
is_active: bool
is_global: bool
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
class FunctionForm(BaseModel):
id: str
name: str
content: str
meta: FunctionMeta
class FunctionValves(BaseModel):
valves: Optional[dict] = None
class FunctionsTable:
def insert_new_function(
self, user_id: str, type: str, form_data: FunctionForm
) -> Optional[FunctionModel]:
function = FunctionModel(
**{
**form_data.model_dump(),
"user_id": user_id,
"type": type,
"updated_at": int(time.time()),
"created_at": int(time.time()),
}
)
try:
with get_db() as db:
result = Function(**function.model_dump())
db.add(result)
db.commit()
db.refresh(result)
if result:
return FunctionModel.model_validate(result)
else:
return None
except Exception as e:
print(f"Error creating tool: {e}")
return None
def get_function_by_id(self, id: str) -> Optional[FunctionModel]:
try:
with get_db() as db:
function = db.get(Function, id)
return FunctionModel.model_validate(function)
except:
return None
def get_functions(self, active_only=False) -> List[FunctionModel]:
with get_db() as db:
if active_only:
return [
FunctionModel.model_validate(function)
for function in db.query(Function).filter_by(is_active=True).all()
]
else:
return [
FunctionModel.model_validate(function)
for function in db.query(Function).all()
]
def get_functions_by_type(
self, type: str, active_only=False
) -> List[FunctionModel]:
with get_db() as db:
if active_only:
return [
FunctionModel.model_validate(function)
for function in db.query(Function)
.filter_by(type=type, is_active=True)
.all()
]
else:
return [
FunctionModel.model_validate(function)
for function in db.query(Function).filter_by(type=type).all()
]
def get_global_filter_functions(self) -> List[FunctionModel]:
with get_db() as db:
return [
FunctionModel.model_validate(function)
for function in db.query(Function)
.filter_by(type="filter", is_active=True, is_global=True)
.all()
]
def get_function_valves_by_id(self, id: str) -> Optional[dict]:
with get_db() as db:
try:
function = db.get(Function, id)
return function.valves if function.valves else {}
except Exception as e:
print(f"An error occurred: {e}")
return None
def update_function_valves_by_id(
self, id: str, valves: dict
) -> Optional[FunctionValves]:
with get_db() as db:
try:
function = db.get(Function, id)
function.valves = valves
function.updated_at = int(time.time())
db.commit()
db.refresh(function)
return self.get_function_by_id(id)
except:
return None
def get_user_valves_by_id_and_user_id(
self, id: str, user_id: str
) -> Optional[dict]:
try:
user = Users.get_user_by_id(user_id)
user_settings = user.settings.model_dump() if user.settings else {}
# Check if user has "functions" and "valves" settings
if "functions" not in user_settings:
user_settings["functions"] = {}
if "valves" not in user_settings["functions"]:
user_settings["functions"]["valves"] = {}
return user_settings["functions"]["valves"].get(id, {})
except Exception as e:
print(f"An error occurred: {e}")
return None
def update_user_valves_by_id_and_user_id(
self, id: str, user_id: str, valves: dict
) -> Optional[dict]:
try:
user = Users.get_user_by_id(user_id)
user_settings = user.settings.model_dump() if user.settings else {}
# Check if user has "functions" and "valves" settings
if "functions" not in user_settings:
user_settings["functions"] = {}
if "valves" not in user_settings["functions"]:
user_settings["functions"]["valves"] = {}
user_settings["functions"]["valves"][id] = valves
# Update the user settings in the database
Users.update_user_by_id(user_id, {"settings": user_settings})
return user_settings["functions"]["valves"][id]
except Exception as e:
print(f"An error occurred: {e}")
return None
def update_function_by_id(self, id: str, updated: dict) -> Optional[FunctionModel]:
with get_db() as db:
try:
db.query(Function).filter_by(id=id).update(
{
**updated,
"updated_at": int(time.time()),
}
)
db.commit()
return self.get_function_by_id(id)
except:
return None
def deactivate_all_functions(self) -> Optional[bool]:
with get_db() as db:
try:
db.query(Function).update(
{
"is_active": False,
"updated_at": int(time.time()),
}
)
db.commit()
return True
except:
return None
def delete_function_by_id(self, id: str) -> bool:
with get_db() as db:
try:
db.query(Function).filter_by(id=id).delete()
db.commit()
return True
except:
return False
Functions = FunctionsTable()

View File

@@ -0,0 +1,148 @@
from pydantic import BaseModel, ConfigDict
from typing import List, Union, Optional
from sqlalchemy import Column, String, BigInteger, Text
from apps.webui.internal.db import Base, get_db
import time
import uuid
####################
# Memory DB Schema
####################
class Memory(Base):
__tablename__ = "memory"
id = Column(String, primary_key=True)
user_id = Column(String)
content = Column(Text)
updated_at = Column(BigInteger)
created_at = Column(BigInteger)
class MemoryModel(BaseModel):
id: str
user_id: str
content: str
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
model_config = ConfigDict(from_attributes=True)
####################
# Forms
####################
class MemoriesTable:
def insert_new_memory(
self,
user_id: str,
content: str,
) -> Optional[MemoryModel]:
with get_db() as db:
id = str(uuid.uuid4())
memory = MemoryModel(
**{
"id": id,
"user_id": user_id,
"content": content,
"created_at": int(time.time()),
"updated_at": int(time.time()),
}
)
result = Memory(**memory.model_dump())
db.add(result)
db.commit()
db.refresh(result)
if result:
return MemoryModel.model_validate(result)
else:
return None
def update_memory_by_id(
self,
id: str,
content: str,
) -> Optional[MemoryModel]:
with get_db() as db:
try:
db.query(Memory).filter_by(id=id).update(
{"content": content, "updated_at": int(time.time())}
)
db.commit()
return self.get_memory_by_id(id)
except:
return None
def get_memories(self) -> List[MemoryModel]:
with get_db() as db:
try:
memories = db.query(Memory).all()
return [MemoryModel.model_validate(memory) for memory in memories]
except:
return None
def get_memories_by_user_id(self, user_id: str) -> List[MemoryModel]:
with get_db() as db:
try:
memories = db.query(Memory).filter_by(user_id=user_id).all()
return [MemoryModel.model_validate(memory) for memory in memories]
except:
return None
def get_memory_by_id(self, id: str) -> Optional[MemoryModel]:
with get_db() as db:
try:
memory = db.get(Memory, id)
return MemoryModel.model_validate(memory)
except:
return None
def delete_memory_by_id(self, id: str) -> bool:
with get_db() as db:
try:
db.query(Memory).filter_by(id=id).delete()
db.commit()
return True
except:
return False
def delete_memories_by_user_id(self, user_id: str) -> bool:
with get_db() as db:
try:
db.query(Memory).filter_by(user_id=user_id).delete()
db.commit()
return True
except:
return False
def delete_memory_by_id_and_user_id(self, id: str, user_id: str) -> bool:
with get_db() as db:
try:
db.query(Memory).filter_by(id=id, user_id=user_id).delete()
db.commit()
return True
except:
return False
Memories = MemoriesTable()

View File

@@ -0,0 +1,190 @@
import json
import logging
from typing import Optional
from pydantic import BaseModel, ConfigDict
from sqlalchemy import String, Column, BigInteger, Text
from apps.webui.internal.db import Base, JSONField, get_db
from typing import List, Union, Optional
from config import SRC_LOG_LEVELS
import time
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
# Models DB Schema
####################
# ModelParams is a model for the data stored in the params field of the Model table
class ModelParams(BaseModel):
model_config = ConfigDict(extra="allow")
pass
# ModelMeta is a model for the data stored in the meta field of the Model table
class ModelMeta(BaseModel):
profile_image_url: Optional[str] = "/static/favicon.png"
description: Optional[str] = None
"""
User-facing description of the model.
"""
capabilities: Optional[dict] = None
model_config = ConfigDict(extra="allow")
pass
class Model(Base):
__tablename__ = "model"
id = Column(Text, primary_key=True)
"""
The model's id as used in the API. If set to an existing model, it will override the model.
"""
user_id = Column(Text)
base_model_id = Column(Text, nullable=True)
"""
An optional pointer to the actual model that should be used when proxying requests.
"""
name = Column(Text)
"""
The human-readable display name of the model.
"""
params = Column(JSONField)
"""
Holds a JSON encoded blob of parameters, see `ModelParams`.
"""
meta = Column(JSONField)
"""
Holds a JSON encoded blob of metadata, see `ModelMeta`.
"""
updated_at = Column(BigInteger)
created_at = Column(BigInteger)
class ModelModel(BaseModel):
id: str
user_id: str
base_model_id: Optional[str] = None
name: str
params: ModelParams
meta: ModelMeta
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
model_config = ConfigDict(from_attributes=True)
####################
# Forms
####################
class ModelResponse(BaseModel):
id: str
name: str
meta: ModelMeta
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
class ModelForm(BaseModel):
id: str
base_model_id: Optional[str] = None
name: str
meta: ModelMeta
params: ModelParams
class ModelsTable:
def insert_new_model(
self, form_data: ModelForm, user_id: str
) -> Optional[ModelModel]:
model = ModelModel(
**{
**form_data.model_dump(),
"user_id": user_id,
"created_at": int(time.time()),
"updated_at": int(time.time()),
}
)
try:
with get_db() as db:
result = Model(**model.model_dump())
db.add(result)
db.commit()
db.refresh(result)
if result:
return ModelModel.model_validate(result)
else:
return None
except Exception as e:
print(e)
return None
def get_all_models(self) -> List[ModelModel]:
with get_db() as db:
return [ModelModel.model_validate(model) for model in db.query(Model).all()]
def get_model_by_id(self, id: str) -> Optional[ModelModel]:
try:
with get_db() as db:
model = db.get(Model, id)
return ModelModel.model_validate(model)
except:
return None
def update_model_by_id(self, id: str, model: ModelForm) -> Optional[ModelModel]:
try:
with get_db() as db:
# update only the fields that are present in the model
result = (
db.query(Model)
.filter_by(id=id)
.update(model.model_dump(exclude={"id"}, exclude_none=True))
)
db.commit()
model = db.get(Model, id)
db.refresh(model)
return ModelModel.model_validate(model)
except Exception as e:
print(e)
return None
def delete_model_by_id(self, id: str) -> bool:
try:
with get_db() as db:
db.query(Model).filter_by(id=id).delete()
db.commit()
return True
except:
return False
Models = ModelsTable()

View File

@@ -0,0 +1,119 @@
from pydantic import BaseModel, ConfigDict
from typing import List, Optional
import time
from sqlalchemy import String, Column, BigInteger, Text
from apps.webui.internal.db import Base, get_db
import json
####################
# Prompts DB Schema
####################
class Prompt(Base):
__tablename__ = "prompt"
command = Column(String, primary_key=True)
user_id = Column(String)
title = Column(Text)
content = Column(Text)
timestamp = Column(BigInteger)
class PromptModel(BaseModel):
command: str
user_id: str
title: str
content: str
timestamp: int # timestamp in epoch
model_config = ConfigDict(from_attributes=True)
####################
# Forms
####################
class PromptForm(BaseModel):
command: str
title: str
content: str
class PromptsTable:
def insert_new_prompt(
self, user_id: str, form_data: PromptForm
) -> Optional[PromptModel]:
prompt = PromptModel(
**{
"user_id": user_id,
"command": form_data.command,
"title": form_data.title,
"content": form_data.content,
"timestamp": int(time.time()),
}
)
try:
with get_db() as db:
result = Prompt(**prompt.dict())
db.add(result)
db.commit()
db.refresh(result)
if result:
return PromptModel.model_validate(result)
else:
return None
except Exception as e:
return None
def get_prompt_by_command(self, command: str) -> Optional[PromptModel]:
try:
with get_db() as db:
prompt = db.query(Prompt).filter_by(command=command).first()
return PromptModel.model_validate(prompt)
except:
return None
def get_prompts(self) -> List[PromptModel]:
with get_db() as db:
return [
PromptModel.model_validate(prompt) for prompt in db.query(Prompt).all()
]
def update_prompt_by_command(
self, command: str, form_data: PromptForm
) -> Optional[PromptModel]:
try:
with get_db() as db:
prompt = db.query(Prompt).filter_by(command=command).first()
prompt.title = form_data.title
prompt.content = form_data.content
prompt.timestamp = int(time.time())
db.commit()
return PromptModel.model_validate(prompt)
except:
return None
def delete_prompt_by_command(self, command: str) -> bool:
try:
with get_db() as db:
db.query(Prompt).filter_by(command=command).delete()
db.commit()
return True
except:
return False
Prompts = PromptsTable()

View File

@@ -0,0 +1,272 @@
from pydantic import BaseModel, ConfigDict
from typing import List, Optional
import json
import uuid
import time
import logging
from sqlalchemy import String, Column, BigInteger, Text
from apps.webui.internal.db import Base, get_db
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
# Tag DB Schema
####################
class Tag(Base):
__tablename__ = "tag"
id = Column(String, primary_key=True)
name = Column(String)
user_id = Column(String)
data = Column(Text, nullable=True)
class ChatIdTag(Base):
__tablename__ = "chatidtag"
id = Column(String, primary_key=True)
tag_name = Column(String)
chat_id = Column(String)
user_id = Column(String)
timestamp = Column(BigInteger)
class TagModel(BaseModel):
id: str
name: str
user_id: str
data: Optional[str] = None
model_config = ConfigDict(from_attributes=True)
class ChatIdTagModel(BaseModel):
id: str
tag_name: str
chat_id: str
user_id: str
timestamp: int
model_config = ConfigDict(from_attributes=True)
####################
# Forms
####################
class ChatIdTagForm(BaseModel):
tag_name: str
chat_id: str
class TagChatIdsResponse(BaseModel):
chat_ids: List[str]
class ChatTagsResponse(BaseModel):
tags: List[str]
class TagTable:
def insert_new_tag(self, name: str, user_id: str) -> Optional[TagModel]:
with get_db() as db:
id = str(uuid.uuid4())
tag = TagModel(**{"id": id, "user_id": user_id, "name": name})
try:
result = Tag(**tag.model_dump())
db.add(result)
db.commit()
db.refresh(result)
if result:
return TagModel.model_validate(result)
else:
return None
except Exception as e:
return None
def get_tag_by_name_and_user_id(
self, name: str, user_id: str
) -> Optional[TagModel]:
try:
with get_db() as db:
tag = db.query(Tag).filter(name=name, user_id=user_id).first()
return TagModel.model_validate(tag)
except Exception as e:
return None
def add_tag_to_chat(
self, user_id: str, form_data: ChatIdTagForm
) -> Optional[ChatIdTagModel]:
tag = self.get_tag_by_name_and_user_id(form_data.tag_name, user_id)
if tag == None:
tag = self.insert_new_tag(form_data.tag_name, user_id)
id = str(uuid.uuid4())
chatIdTag = ChatIdTagModel(
**{
"id": id,
"user_id": user_id,
"chat_id": form_data.chat_id,
"tag_name": tag.name,
"timestamp": int(time.time()),
}
)
try:
with get_db() as db:
result = ChatIdTag(**chatIdTag.model_dump())
db.add(result)
db.commit()
db.refresh(result)
if result:
return ChatIdTagModel.model_validate(result)
else:
return None
except:
return None
def get_tags_by_user_id(self, user_id: str) -> List[TagModel]:
with get_db() as db:
tag_names = [
chat_id_tag.tag_name
for chat_id_tag in (
db.query(ChatIdTag)
.filter_by(user_id=user_id)
.order_by(ChatIdTag.timestamp.desc())
.all()
)
]
return [
TagModel.model_validate(tag)
for tag in (
db.query(Tag)
.filter_by(user_id=user_id)
.filter(Tag.name.in_(tag_names))
.all()
)
]
def get_tags_by_chat_id_and_user_id(
self, chat_id: str, user_id: str
) -> List[TagModel]:
with get_db() as db:
tag_names = [
chat_id_tag.tag_name
for chat_id_tag in (
db.query(ChatIdTag)
.filter_by(user_id=user_id, chat_id=chat_id)
.order_by(ChatIdTag.timestamp.desc())
.all()
)
]
return [
TagModel.model_validate(tag)
for tag in (
db.query(Tag)
.filter_by(user_id=user_id)
.filter(Tag.name.in_(tag_names))
.all()
)
]
def get_chat_ids_by_tag_name_and_user_id(
self, tag_name: str, user_id: str
) -> List[ChatIdTagModel]:
with get_db() as db:
return [
ChatIdTagModel.model_validate(chat_id_tag)
for chat_id_tag in (
db.query(ChatIdTag)
.filter_by(user_id=user_id, tag_name=tag_name)
.order_by(ChatIdTag.timestamp.desc())
.all()
)
]
def count_chat_ids_by_tag_name_and_user_id(
self, tag_name: str, user_id: str
) -> int:
with get_db() as db:
return (
db.query(ChatIdTag)
.filter_by(tag_name=tag_name, user_id=user_id)
.count()
)
def delete_tag_by_tag_name_and_user_id(self, tag_name: str, user_id: str) -> bool:
try:
with get_db() as db:
res = (
db.query(ChatIdTag)
.filter_by(tag_name=tag_name, user_id=user_id)
.delete()
)
log.debug(f"res: {res}")
db.commit()
tag_count = self.count_chat_ids_by_tag_name_and_user_id(
tag_name, user_id
)
if tag_count == 0:
# Remove tag item from Tag col as well
db.query(Tag).filter_by(name=tag_name, user_id=user_id).delete()
db.commit()
return True
except Exception as e:
log.error(f"delete_tag: {e}")
return False
def delete_tag_by_tag_name_and_chat_id_and_user_id(
self, tag_name: str, chat_id: str, user_id: str
) -> bool:
try:
with get_db() as db:
res = (
db.query(ChatIdTag)
.filter_by(tag_name=tag_name, chat_id=chat_id, user_id=user_id)
.delete()
)
log.debug(f"res: {res}")
db.commit()
tag_count = self.count_chat_ids_by_tag_name_and_user_id(
tag_name, user_id
)
if tag_count == 0:
# Remove tag item from Tag col as well
db.query(Tag).filter_by(name=tag_name, user_id=user_id).delete()
db.commit()
return True
except Exception as e:
log.error(f"delete_tag: {e}")
return False
def delete_tags_by_chat_id_and_user_id(self, chat_id: str, user_id: str) -> bool:
tags = self.get_tags_by_chat_id_and_user_id(chat_id, user_id)
for tag in tags:
self.delete_tag_by_tag_name_and_chat_id_and_user_id(
tag.tag_name, chat_id, user_id
)
return True
Tags = TagTable()

View File

@@ -0,0 +1,213 @@
from pydantic import BaseModel, ConfigDict
from typing import List, Optional
import time
import logging
from sqlalchemy import String, Column, BigInteger, Text
from apps.webui.internal.db import Base, JSONField, get_db
from apps.webui.models.users import Users
import json
import copy
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
# Tools DB Schema
####################
class Tool(Base):
__tablename__ = "tool"
id = Column(String, primary_key=True)
user_id = Column(String)
name = Column(Text)
content = Column(Text)
specs = Column(JSONField)
meta = Column(JSONField)
valves = Column(JSONField)
updated_at = Column(BigInteger)
created_at = Column(BigInteger)
class ToolMeta(BaseModel):
description: Optional[str] = None
manifest: Optional[dict] = {}
class ToolModel(BaseModel):
id: str
user_id: str
name: str
content: str
specs: List[dict]
meta: ToolMeta
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
model_config = ConfigDict(from_attributes=True)
####################
# Forms
####################
class ToolResponse(BaseModel):
id: str
user_id: str
name: str
meta: ToolMeta
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
class ToolForm(BaseModel):
id: str
name: str
content: str
meta: ToolMeta
class ToolValves(BaseModel):
valves: Optional[dict] = None
class ToolsTable:
def insert_new_tool(
self, user_id: str, form_data: ToolForm, specs: List[dict]
) -> Optional[ToolModel]:
with get_db() as db:
tool = ToolModel(
**{
**form_data.model_dump(),
"specs": specs,
"user_id": user_id,
"updated_at": int(time.time()),
"created_at": int(time.time()),
}
)
try:
result = Tool(**tool.model_dump())
db.add(result)
db.commit()
db.refresh(result)
if result:
return ToolModel.model_validate(result)
else:
return None
except Exception as e:
print(f"Error creating tool: {e}")
return None
def get_tool_by_id(self, id: str) -> Optional[ToolModel]:
try:
with get_db() as db:
tool = db.get(Tool, id)
return ToolModel.model_validate(tool)
except:
return None
def get_tools(self) -> List[ToolModel]:
with get_db() as db:
return [ToolModel.model_validate(tool) for tool in db.query(Tool).all()]
def get_tool_valves_by_id(self, id: str) -> Optional[dict]:
try:
with get_db() as db:
tool = db.get(Tool, id)
return tool.valves if tool.valves else {}
except Exception as e:
print(f"An error occurred: {e}")
return None
def update_tool_valves_by_id(self, id: str, valves: dict) -> Optional[ToolValves]:
try:
with get_db() as db:
db.query(Tool).filter_by(id=id).update(
{"valves": valves, "updated_at": int(time.time())}
)
db.commit()
return self.get_tool_by_id(id)
except:
return None
def get_user_valves_by_id_and_user_id(
self, id: str, user_id: str
) -> Optional[dict]:
try:
user = Users.get_user_by_id(user_id)
user_settings = user.settings.model_dump() if user.settings else {}
# Check if user has "tools" and "valves" settings
if "tools" not in user_settings:
user_settings["tools"] = {}
if "valves" not in user_settings["tools"]:
user_settings["tools"]["valves"] = {}
return user_settings["tools"]["valves"].get(id, {})
except Exception as e:
print(f"An error occurred: {e}")
return None
def update_user_valves_by_id_and_user_id(
self, id: str, user_id: str, valves: dict
) -> Optional[dict]:
try:
user = Users.get_user_by_id(user_id)
user_settings = user.settings.model_dump() if user.settings else {}
# Check if user has "tools" and "valves" settings
if "tools" not in user_settings:
user_settings["tools"] = {}
if "valves" not in user_settings["tools"]:
user_settings["tools"]["valves"] = {}
user_settings["tools"]["valves"][id] = valves
# Update the user settings in the database
Users.update_user_by_id(user_id, {"settings": user_settings})
return user_settings["tools"]["valves"][id]
except Exception as e:
print(f"An error occurred: {e}")
return None
def update_tool_by_id(self, id: str, updated: dict) -> Optional[ToolModel]:
try:
with get_db() as db:
db.query(Tool).filter_by(id=id).update(
{**updated, "updated_at": int(time.time())}
)
db.commit()
tool = db.query(Tool).get(id)
db.refresh(tool)
return ToolModel.model_validate(tool)
except:
return None
def delete_tool_by_id(self, id: str) -> bool:
try:
with get_db() as db:
db.query(Tool).filter_by(id=id).delete()
db.commit()
return True
except:
return False
Tools = ToolsTable()

View File

@@ -0,0 +1,269 @@
from pydantic import BaseModel, ConfigDict, parse_obj_as
from typing import List, Union, Optional
import time
from sqlalchemy import String, Column, BigInteger, Text
from utils.misc import get_gravatar_url
from apps.webui.internal.db import Base, JSONField, Session, get_db
from apps.webui.models.chats import Chats
####################
# User DB Schema
####################
class User(Base):
__tablename__ = "user"
id = Column(String, primary_key=True)
name = Column(String)
email = Column(String)
role = Column(String)
profile_image_url = Column(Text)
last_active_at = Column(BigInteger)
updated_at = Column(BigInteger)
created_at = Column(BigInteger)
api_key = Column(String, nullable=True, unique=True)
settings = Column(JSONField, nullable=True)
info = Column(JSONField, nullable=True)
oauth_sub = Column(Text, unique=True)
class UserSettings(BaseModel):
ui: Optional[dict] = {}
model_config = ConfigDict(extra="allow")
pass
class UserModel(BaseModel):
id: str
name: str
email: str
role: str = "pending"
profile_image_url: str
last_active_at: int # timestamp in epoch
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
api_key: Optional[str] = None
settings: Optional[UserSettings] = None
info: Optional[dict] = None
oauth_sub: Optional[str] = None
model_config = ConfigDict(from_attributes=True)
####################
# Forms
####################
class UserRoleUpdateForm(BaseModel):
id: str
role: str
class UserUpdateForm(BaseModel):
name: str
email: str
profile_image_url: str
password: Optional[str] = None
class UsersTable:
def insert_new_user(
self,
id: str,
name: str,
email: str,
profile_image_url: str = "/user.png",
role: str = "pending",
oauth_sub: Optional[str] = None,
) -> Optional[UserModel]:
with get_db() as db:
user = UserModel(
**{
"id": id,
"name": name,
"email": email,
"role": role,
"profile_image_url": profile_image_url,
"last_active_at": int(time.time()),
"created_at": int(time.time()),
"updated_at": int(time.time()),
"oauth_sub": oauth_sub,
}
)
result = User(**user.model_dump())
db.add(result)
db.commit()
db.refresh(result)
if result:
return user
else:
return None
def get_user_by_id(self, id: str) -> Optional[UserModel]:
try:
with get_db() as db:
user = db.query(User).filter_by(id=id).first()
return UserModel.model_validate(user)
except Exception as e:
return None
def get_user_by_api_key(self, api_key: str) -> Optional[UserModel]:
try:
with get_db() as db:
user = db.query(User).filter_by(api_key=api_key).first()
return UserModel.model_validate(user)
except:
return None
def get_user_by_email(self, email: str) -> Optional[UserModel]:
try:
with get_db() as db:
user = db.query(User).filter_by(email=email).first()
return UserModel.model_validate(user)
except:
return None
def get_user_by_oauth_sub(self, sub: str) -> Optional[UserModel]:
try:
with get_db() as db:
user = db.query(User).filter_by(oauth_sub=sub).first()
return UserModel.model_validate(user)
except:
return None
def get_users(self, skip: int = 0, limit: int = 50) -> List[UserModel]:
with get_db() as db:
users = (
db.query(User)
# .offset(skip).limit(limit)
.all()
)
return [UserModel.model_validate(user) for user in users]
def get_num_users(self) -> Optional[int]:
with get_db() as db:
return db.query(User).count()
def get_first_user(self) -> UserModel:
try:
with get_db() as db:
user = db.query(User).order_by(User.created_at).first()
return UserModel.model_validate(user)
except:
return None
def update_user_role_by_id(self, id: str, role: str) -> Optional[UserModel]:
try:
with get_db() as db:
db.query(User).filter_by(id=id).update({"role": role})
db.commit()
user = db.query(User).filter_by(id=id).first()
return UserModel.model_validate(user)
except:
return None
def update_user_profile_image_url_by_id(
self, id: str, profile_image_url: str
) -> Optional[UserModel]:
try:
with get_db() as db:
db.query(User).filter_by(id=id).update(
{"profile_image_url": profile_image_url}
)
db.commit()
user = db.query(User).filter_by(id=id).first()
return UserModel.model_validate(user)
except:
return None
def update_user_last_active_by_id(self, id: str) -> Optional[UserModel]:
try:
with get_db() as db:
db.query(User).filter_by(id=id).update(
{"last_active_at": int(time.time())}
)
db.commit()
user = db.query(User).filter_by(id=id).first()
return UserModel.model_validate(user)
except:
return None
def update_user_oauth_sub_by_id(
self, id: str, oauth_sub: str
) -> Optional[UserModel]:
try:
with get_db() as db:
db.query(User).filter_by(id=id).update({"oauth_sub": oauth_sub})
db.commit()
user = db.query(User).filter_by(id=id).first()
return UserModel.model_validate(user)
except:
return None
def update_user_by_id(self, id: str, updated: dict) -> Optional[UserModel]:
try:
with get_db() as db:
db.query(User).filter_by(id=id).update(updated)
db.commit()
user = db.query(User).filter_by(id=id).first()
return UserModel.model_validate(user)
# return UserModel(**user.dict())
except Exception as e:
return None
def delete_user_by_id(self, id: str) -> bool:
try:
# Delete User Chats
result = Chats.delete_chats_by_user_id(id)
if result:
with get_db() as db:
# Delete User
db.query(User).filter_by(id=id).delete()
db.commit()
return True
else:
return False
except:
return False
def update_user_api_key_by_id(self, id: str, api_key: str) -> str:
try:
with get_db() as db:
result = db.query(User).filter_by(id=id).update({"api_key": api_key})
db.commit()
return True if result == 1 else False
except:
return False
def get_user_api_key_by_id(self, id: str) -> Optional[str]:
try:
with get_db() as db:
user = db.query(User).filter_by(id=id).first()
return user.api_key
except Exception as e:
return None
Users = UsersTable()

View File

@@ -2,6 +2,7 @@ import logging
from fastapi import Request, UploadFile, File
from fastapi import Depends, HTTPException, status
from fastapi.responses import Response
from fastapi import APIRouter
from pydantic import BaseModel
@@ -9,8 +10,7 @@ import re
import uuid
import csv
from apps.web.models.auths import (
from apps.webui.models.auths import (
SigninForm,
SignupForm,
AddUserForm,
@@ -21,7 +21,7 @@ from apps.web.models.auths import (
Auths,
ApiKey,
)
from apps.web.models.users import Users
from apps.webui.models.users import Users
from utils.utils import (
get_password_hash,
@@ -33,7 +33,11 @@ from utils.utils import (
from utils.misc import parse_duration, validate_email_format
from utils.webhook import post_webhook
from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
from config import WEBUI_AUTH, WEBUI_AUTH_TRUSTED_EMAIL_HEADER
from config import (
WEBUI_AUTH,
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
WEBUI_AUTH_TRUSTED_NAME_HEADER,
)
router = APIRouter()
@@ -43,7 +47,21 @@ router = APIRouter()
@router.get("/", response_model=UserResponse)
async def get_session_user(user=Depends(get_current_user)):
async def get_session_user(
request: Request, response: Response, user=Depends(get_current_user)
):
token = create_token(
data={"id": user.id},
expires_delta=parse_duration(request.app.state.config.JWT_EXPIRES_IN),
)
# Set the cookie token
response.set_cookie(
key="token",
value=token,
httponly=True, # Ensures the cookie is not accessible via JavaScript
)
return {
"id": user.id,
"email": user.email,
@@ -104,17 +122,23 @@ async def update_password(
@router.post("/signin", response_model=SigninResponse)
async def signin(request: Request, form_data: SigninForm):
async def signin(request: Request, response: Response, form_data: SigninForm):
if WEBUI_AUTH_TRUSTED_EMAIL_HEADER:
if WEBUI_AUTH_TRUSTED_EMAIL_HEADER not in request.headers:
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_TRUSTED_HEADER)
trusted_email = request.headers[WEBUI_AUTH_TRUSTED_EMAIL_HEADER].lower()
trusted_name = trusted_email
if WEBUI_AUTH_TRUSTED_NAME_HEADER:
trusted_name = request.headers.get(
WEBUI_AUTH_TRUSTED_NAME_HEADER, trusted_email
)
if not Users.get_user_by_email(trusted_email.lower()):
await signup(
request,
response,
SignupForm(
email=trusted_email, password=str(uuid.uuid4()), name=trusted_email
email=trusted_email, password=str(uuid.uuid4()), name=trusted_name
),
)
user = Auths.authenticate_user_by_trusted_header(trusted_email)
@@ -130,6 +154,7 @@ async def signin(request: Request, form_data: SigninForm):
await signup(
request,
response,
SignupForm(email=admin_email, password=admin_password, name="User"),
)
@@ -143,6 +168,13 @@ async def signin(request: Request, form_data: SigninForm):
expires_delta=parse_duration(request.app.state.config.JWT_EXPIRES_IN),
)
# Set the cookie token
response.set_cookie(
key="token",
value=token,
httponly=True, # Ensures the cookie is not accessible via JavaScript
)
return {
"token": token,
"token_type": "Bearer",
@@ -162,7 +194,7 @@ async def signin(request: Request, form_data: SigninForm):
@router.post("/signup", response_model=SigninResponse)
async def signup(request: Request, form_data: SignupForm):
async def signup(request: Request, response: Response, form_data: SignupForm):
if not request.app.state.config.ENABLE_SIGNUP and WEBUI_AUTH:
raise HTTPException(
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
@@ -198,6 +230,13 @@ async def signup(request: Request, form_data: SignupForm):
)
# response.set_cookie(key='token', value=token, httponly=True)
# Set the cookie token
response.set_cookie(
key="token",
value=token,
httponly=True, # Ensures the cookie is not accessible via JavaScript
)
if request.app.state.config.WEBHOOK_URL:
post_webhook(
request.app.state.config.WEBHOOK_URL,
@@ -269,73 +308,88 @@ async def add_user(form_data: AddUserForm, user=Depends(get_admin_user)):
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
############################
# GetAdminDetails
############################
@router.get("/admin/details")
async def get_admin_details(request: Request, user=Depends(get_current_user)):
if request.app.state.config.SHOW_ADMIN_DETAILS:
admin_email = request.app.state.config.ADMIN_EMAIL
admin_name = None
print(admin_email, admin_name)
if admin_email:
admin = Users.get_user_by_email(admin_email)
if admin:
admin_name = admin.name
else:
admin = Users.get_first_user()
if admin:
admin_email = admin.email
admin_name = admin.name
return {
"name": admin_name,
"email": admin_email,
}
else:
raise HTTPException(400, detail=ERROR_MESSAGES.ACTION_PROHIBITED)
############################
# ToggleSignUp
############################
@router.get("/signup/enabled", response_model=bool)
async def get_sign_up_status(request: Request, user=Depends(get_admin_user)):
return request.app.state.config.ENABLE_SIGNUP
@router.get("/admin/config")
async def get_admin_config(request: Request, user=Depends(get_admin_user)):
return {
"SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS,
"ENABLE_SIGNUP": request.app.state.config.ENABLE_SIGNUP,
"DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE,
"JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN,
"ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
}
@router.get("/signup/enabled/toggle", response_model=bool)
async def toggle_sign_up(request: Request, user=Depends(get_admin_user)):
request.app.state.config.ENABLE_SIGNUP = not request.app.state.config.ENABLE_SIGNUP
return request.app.state.config.ENABLE_SIGNUP
class AdminConfig(BaseModel):
SHOW_ADMIN_DETAILS: bool
ENABLE_SIGNUP: bool
DEFAULT_USER_ROLE: str
JWT_EXPIRES_IN: str
ENABLE_COMMUNITY_SHARING: bool
############################
# Default User Role
############################
@router.get("/signup/user/role")
async def get_default_user_role(request: Request, user=Depends(get_admin_user)):
return request.app.state.config.DEFAULT_USER_ROLE
class UpdateRoleForm(BaseModel):
role: str
@router.post("/signup/user/role")
async def update_default_user_role(
request: Request, form_data: UpdateRoleForm, user=Depends(get_admin_user)
@router.post("/admin/config")
async def update_admin_config(
request: Request, form_data: AdminConfig, user=Depends(get_admin_user)
):
if form_data.role in ["pending", "user", "admin"]:
request.app.state.config.DEFAULT_USER_ROLE = form_data.role
return request.app.state.config.DEFAULT_USER_ROLE
request.app.state.config.SHOW_ADMIN_DETAILS = form_data.SHOW_ADMIN_DETAILS
request.app.state.config.ENABLE_SIGNUP = form_data.ENABLE_SIGNUP
if form_data.DEFAULT_USER_ROLE in ["pending", "user", "admin"]:
request.app.state.config.DEFAULT_USER_ROLE = form_data.DEFAULT_USER_ROLE
############################
# JWT Expiration
############################
@router.get("/token/expires")
async def get_token_expires_duration(request: Request, user=Depends(get_admin_user)):
return request.app.state.config.JWT_EXPIRES_IN
class UpdateJWTExpiresDurationForm(BaseModel):
duration: str
@router.post("/token/expires/update")
async def update_token_expires_duration(
request: Request,
form_data: UpdateJWTExpiresDurationForm,
user=Depends(get_admin_user),
):
pattern = r"^(-1|0|(-?\d+(\.\d+)?)(ms|s|m|h|d|w))$"
# Check if the input string matches the pattern
if re.match(pattern, form_data.duration):
request.app.state.config.JWT_EXPIRES_IN = form_data.duration
return request.app.state.config.JWT_EXPIRES_IN
else:
return request.app.state.config.JWT_EXPIRES_IN
if re.match(pattern, form_data.JWT_EXPIRES_IN):
request.app.state.config.JWT_EXPIRES_IN = form_data.JWT_EXPIRES_IN
request.app.state.config.ENABLE_COMMUNITY_SHARING = (
form_data.ENABLE_COMMUNITY_SHARING
)
return {
"SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS,
"ENABLE_SIGNUP": request.app.state.config.ENABLE_SIGNUP,
"DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE,
"JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN,
"ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
}
############################

View File

@@ -1,14 +1,14 @@
from fastapi import Depends, Request, HTTPException, status
from datetime import datetime, timedelta
from typing import List, Union, Optional
from utils.utils import get_current_user, get_admin_user
from utils.utils import get_verified_user, get_admin_user
from fastapi import APIRouter
from pydantic import BaseModel
import json
import logging
from apps.web.models.users import Users
from apps.web.models.chats import (
from apps.webui.models.users import Users
from apps.webui.models.chats import (
ChatModel,
ChatResponse,
ChatTitleForm,
@@ -18,7 +18,7 @@ from apps.web.models.chats import (
)
from apps.web.models.tags import (
from apps.webui.models.tags import (
TagModel,
ChatIdTagModel,
ChatIdTagForm,
@@ -43,7 +43,7 @@ router = APIRouter()
@router.get("/", response_model=List[ChatTitleIdResponse])
@router.get("/list", response_model=List[ChatTitleIdResponse])
async def get_session_user_chat_list(
user=Depends(get_current_user), skip: int = 0, limit: int = 50
user=Depends(get_verified_user), skip: int = 0, limit: int = 50
):
return Chats.get_chat_list_by_user_id(user.id, skip, limit)
@@ -54,7 +54,7 @@ async def get_session_user_chat_list(
@router.delete("/", response_model=bool)
async def delete_all_user_chats(request: Request, user=Depends(get_current_user)):
async def delete_all_user_chats(request: Request, user=Depends(get_verified_user)):
if (
user.role == "user"
@@ -76,45 +76,30 @@ async def delete_all_user_chats(request: Request, user=Depends(get_current_user)
@router.get("/list/user/{user_id}", response_model=List[ChatTitleIdResponse])
async def get_user_chat_list_by_user_id(
user_id: str, user=Depends(get_admin_user), skip: int = 0, limit: int = 50
user_id: str,
user=Depends(get_admin_user),
skip: int = 0,
limit: int = 50,
):
return Chats.get_chat_list_by_user_id(user_id, skip, limit)
return Chats.get_chat_list_by_user_id(
user_id, include_archived=True, skip=skip, limit=limit
)
############################
# GetArchivedChats
# CreateNewChat
############################
@router.get("/archived", response_model=List[ChatTitleIdResponse])
async def get_archived_session_user_chat_list(
user=Depends(get_current_user), skip: int = 0, limit: int = 50
):
return Chats.get_archived_chat_list_by_user_id(user.id, skip, limit)
############################
# GetSharedChatById
############################
@router.get("/share/{share_id}", response_model=Optional[ChatResponse])
async def get_shared_chat_by_id(share_id: str, user=Depends(get_current_user)):
if user.role == "pending":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
)
if user.role == "user":
chat = Chats.get_chat_by_share_id(share_id)
elif user.role == "admin":
chat = Chats.get_chat_by_id(share_id)
if chat:
@router.post("/new", response_model=Optional[ChatResponse])
async def create_new_chat(form_data: ChatForm, user=Depends(get_verified_user)):
try:
chat = Chats.insert_new_chat(user.id, form_data)
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
else:
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
@@ -124,13 +109,26 @@ async def get_shared_chat_by_id(share_id: str, user=Depends(get_current_user)):
@router.get("/all", response_model=List[ChatResponse])
async def get_user_chats(user=Depends(get_current_user)):
async def get_user_chats(user=Depends(get_verified_user)):
return [
ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
for chat in Chats.get_chats_by_user_id(user.id)
]
############################
# GetArchivedChats
############################
@router.get("/all/archived", response_model=List[ChatResponse])
async def get_user_archived_chats(user=Depends(get_verified_user)):
return [
ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
for chat in Chats.get_archived_chats_by_user_id(user.id)
]
############################
# GetAllChatsInDB
############################
@@ -150,19 +148,49 @@ async def get_all_user_chats_in_db(user=Depends(get_admin_user)):
############################
# CreateNewChat
# GetArchivedChats
############################
@router.post("/new", response_model=Optional[ChatResponse])
async def create_new_chat(form_data: ChatForm, user=Depends(get_current_user)):
try:
chat = Chats.insert_new_chat(user.id, form_data)
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
except Exception as e:
log.exception(e)
@router.get("/archived", response_model=List[ChatTitleIdResponse])
async def get_archived_session_user_chat_list(
user=Depends(get_verified_user), skip: int = 0, limit: int = 50
):
return Chats.get_archived_chat_list_by_user_id(user.id, skip, limit)
############################
# ArchiveAllChats
############################
@router.post("/archive/all", response_model=bool)
async def archive_all_chats(user=Depends(get_verified_user)):
return Chats.archive_all_chats_by_user_id(user.id)
############################
# GetSharedChatById
############################
@router.get("/share/{share_id}", response_model=Optional[ChatResponse])
async def get_shared_chat_by_id(share_id: str, user=Depends(get_verified_user)):
if user.role == "pending":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
)
if user.role == "user":
chat = Chats.get_chat_by_share_id(share_id)
elif user.role == "admin":
chat = Chats.get_chat_by_id(share_id)
if chat:
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
)
@@ -179,10 +207,9 @@ class TagNameForm(BaseModel):
@router.post("/tags", response_model=List[ChatTitleIdResponse])
async def get_user_chat_list_by_tag_name(
form_data: TagNameForm, user=Depends(get_current_user)
form_data: TagNameForm, user=Depends(get_verified_user)
):
print(form_data)
chat_ids = [
chat_id_tag.chat_id
for chat_id_tag in Tags.get_chat_ids_by_tag_name_and_user_id(
@@ -204,7 +231,7 @@ async def get_user_chat_list_by_tag_name(
@router.get("/tags/all", response_model=List[TagModel])
async def get_all_tags(user=Depends(get_current_user)):
async def get_all_tags(user=Depends(get_verified_user)):
try:
tags = Tags.get_tags_by_user_id(user.id)
return tags
@@ -221,7 +248,7 @@ async def get_all_tags(user=Depends(get_current_user)):
@router.get("/{id}", response_model=Optional[ChatResponse])
async def get_chat_by_id(id: str, user=Depends(get_current_user)):
async def get_chat_by_id(id: str, user=Depends(get_verified_user)):
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
@@ -239,7 +266,7 @@ async def get_chat_by_id(id: str, user=Depends(get_current_user)):
@router.post("/{id}", response_model=Optional[ChatResponse])
async def update_chat_by_id(
id: str, form_data: ChatForm, user=Depends(get_current_user)
id: str, form_data: ChatForm, user=Depends(get_verified_user)
):
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
@@ -260,7 +287,7 @@ async def update_chat_by_id(
@router.delete("/{id}", response_model=bool)
async def delete_chat_by_id(request: Request, id: str, user=Depends(get_current_user)):
async def delete_chat_by_id(request: Request, id: str, user=Depends(get_verified_user)):
if user.role == "admin":
result = Chats.delete_chat_by_id(id)
@@ -276,13 +303,39 @@ async def delete_chat_by_id(request: Request, id: str, user=Depends(get_current_
return result
############################
# CloneChat
############################
@router.get("/{id}/clone", response_model=Optional[ChatResponse])
async def clone_chat_by_id(id: str, user=Depends(get_verified_user)):
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
chat_body = json.loads(chat.chat)
updated_chat = {
**chat_body,
"originalChatId": chat.id,
"branchPointMessageId": chat_body["history"]["currentId"],
"title": f"Clone of {chat.title}",
}
chat = Chats.insert_new_chat(user.id, ChatForm(**{"chat": updated_chat}))
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()
)
############################
# ArchiveChat
############################
@router.get("/{id}/archive", response_model=Optional[ChatResponse])
async def archive_chat_by_id(id: str, user=Depends(get_current_user)):
async def archive_chat_by_id(id: str, user=Depends(get_verified_user)):
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
chat = Chats.toggle_chat_archive_by_id(id)
@@ -299,7 +352,7 @@ async def archive_chat_by_id(id: str, user=Depends(get_current_user)):
@router.post("/{id}/share", response_model=Optional[ChatResponse])
async def share_chat_by_id(id: str, user=Depends(get_current_user)):
async def share_chat_by_id(id: str, user=Depends(get_verified_user)):
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
if chat.share_id:
@@ -331,7 +384,7 @@ async def share_chat_by_id(id: str, user=Depends(get_current_user)):
@router.delete("/{id}/share", response_model=Optional[bool])
async def delete_shared_chat_by_id(id: str, user=Depends(get_current_user)):
async def delete_shared_chat_by_id(id: str, user=Depends(get_verified_user)):
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
if not chat.share_id:
@@ -354,7 +407,7 @@ async def delete_shared_chat_by_id(id: str, user=Depends(get_current_user)):
@router.get("/{id}/tags", response_model=List[TagModel])
async def get_chat_tags_by_id(id: str, user=Depends(get_current_user)):
async def get_chat_tags_by_id(id: str, user=Depends(get_verified_user)):
tags = Tags.get_tags_by_chat_id_and_user_id(id, user.id)
if tags != None:
@@ -372,7 +425,7 @@ async def get_chat_tags_by_id(id: str, user=Depends(get_current_user)):
@router.post("/{id}/tags", response_model=Optional[ChatIdTagModel])
async def add_chat_tag_by_id(
id: str, form_data: ChatIdTagForm, user=Depends(get_current_user)
id: str, form_data: ChatIdTagForm, user=Depends(get_verified_user)
):
tags = Tags.get_tags_by_chat_id_and_user_id(id, user.id)
@@ -399,7 +452,7 @@ async def add_chat_tag_by_id(
@router.delete("/{id}/tags", response_model=Optional[bool])
async def delete_chat_tag_by_id(
id: str, form_data: ChatIdTagForm, user=Depends(get_current_user)
id: str, form_data: ChatIdTagForm, user=Depends(get_verified_user)
):
result = Tags.delete_tag_by_tag_name_and_chat_id_and_user_id(
form_data.tag_name, id, user.id
@@ -419,7 +472,7 @@ async def delete_chat_tag_by_id(
@router.delete("/{id}/tags/all", response_model=Optional[bool])
async def delete_all_chat_tags_by_id(id: str, user=Depends(get_current_user)):
async def delete_all_chat_tags_by_id(id: str, user=Depends(get_verified_user)):
result = Tags.delete_tags_by_chat_id_and_user_id(id, user.id)
if result:

View File

@@ -8,11 +8,13 @@ from pydantic import BaseModel
import time
import uuid
from apps.web.models.users import Users
from config import BannerModel
from apps.webui.models.users import Users
from utils.utils import (
get_password_hash,
get_current_user,
get_verified_user,
get_admin_user,
create_token,
)
@@ -57,3 +59,31 @@ async def set_global_default_suggestions(
data = form_data.model_dump()
request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS = data["suggestions"]
return request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS
############################
# SetBanners
############################
class SetBannersForm(BaseModel):
banners: List[BannerModel]
@router.post("/banners", response_model=List[BannerModel])
async def set_banners(
request: Request,
form_data: SetBannersForm,
user=Depends(get_admin_user),
):
data = form_data.model_dump()
request.app.state.config.BANNERS = data["banners"]
return request.app.state.config.BANNERS
@router.get("/banners", response_model=List[BannerModel])
async def get_banners(
request: Request,
user=Depends(get_verified_user),
):
return request.app.state.config.BANNERS

View File

@@ -6,7 +6,7 @@ from fastapi import APIRouter
from pydantic import BaseModel
import json
from apps.web.models.documents import (
from apps.webui.models.documents import (
Documents,
DocumentForm,
DocumentUpdateForm,
@@ -14,7 +14,7 @@ from apps.web.models.documents import (
DocumentResponse,
)
from utils.utils import get_current_user, get_admin_user
from utils.utils import get_verified_user, get_admin_user
from constants import ERROR_MESSAGES
router = APIRouter()
@@ -25,7 +25,7 @@ router = APIRouter()
@router.get("/", response_model=List[DocumentResponse])
async def get_documents(user=Depends(get_current_user)):
async def get_documents(user=Depends(get_verified_user)):
docs = [
DocumentResponse(
**{
@@ -73,8 +73,8 @@ async def create_new_doc(form_data: DocumentForm, user=Depends(get_admin_user)):
############################
@router.get("/name/{name}", response_model=Optional[DocumentResponse])
async def get_doc_by_name(name: str, user=Depends(get_current_user)):
@router.get("/doc", response_model=Optional[DocumentResponse])
async def get_doc_by_name(name: str, user=Depends(get_verified_user)):
doc = Documents.get_doc_by_name(name)
if doc:
@@ -105,8 +105,8 @@ class TagDocumentForm(BaseModel):
tags: List[dict]
@router.post("/name/{name}/tags", response_model=Optional[DocumentResponse])
async def tag_doc_by_name(form_data: TagDocumentForm, user=Depends(get_current_user)):
@router.post("/doc/tags", response_model=Optional[DocumentResponse])
async def tag_doc_by_name(form_data: TagDocumentForm, user=Depends(get_verified_user)):
doc = Documents.update_doc_content_by_name(form_data.name, {"tags": form_data.tags})
if doc:
@@ -128,9 +128,11 @@ async def tag_doc_by_name(form_data: TagDocumentForm, user=Depends(get_current_u
############################
@router.post("/name/{name}/update", response_model=Optional[DocumentResponse])
@router.post("/doc/update", response_model=Optional[DocumentResponse])
async def update_doc_by_name(
name: str, form_data: DocumentUpdateForm, user=Depends(get_admin_user)
name: str,
form_data: DocumentUpdateForm,
user=Depends(get_admin_user),
):
doc = Documents.update_doc_by_name(name, form_data)
if doc:
@@ -152,7 +154,7 @@ async def update_doc_by_name(
############################
@router.delete("/name/{name}/delete", response_model=bool)
@router.delete("/doc/delete", response_model=bool)
async def delete_doc_by_name(name: str, user=Depends(get_admin_user)):
result = Documents.delete_doc_by_name(name)
return result

View File

@@ -0,0 +1,239 @@
from fastapi import (
Depends,
FastAPI,
HTTPException,
status,
Request,
UploadFile,
File,
Form,
)
from datetime import datetime, timedelta
from typing import List, Union, Optional
from pathlib import Path
from fastapi import APIRouter
from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
from pydantic import BaseModel
import json
from apps.webui.models.files import (
Files,
FileForm,
FileModel,
FileModelResponse,
)
from utils.utils import get_verified_user, get_admin_user
from constants import ERROR_MESSAGES
from importlib import util
import os
import uuid
import os, shutil, logging, re
from config import SRC_LOG_LEVELS, UPLOAD_DIR
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
router = APIRouter()
############################
# Upload File
############################
@router.post("/")
def upload_file(file: UploadFile = File(...), user=Depends(get_verified_user)):
log.info(f"file.content_type: {file.content_type}")
try:
unsanitized_filename = file.filename
filename = os.path.basename(unsanitized_filename)
# replace filename with uuid
id = str(uuid.uuid4())
filename = f"{id}_{filename}"
file_path = f"{UPLOAD_DIR}/{filename}"
contents = file.file.read()
with open(file_path, "wb") as f:
f.write(contents)
f.close()
file = Files.insert_new_file(
user.id,
FileForm(
**{
"id": id,
"filename": filename,
"meta": {
"content_type": file.content_type,
"size": len(contents),
"path": file_path,
},
}
),
)
if file:
return file
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error uploading file"),
)
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
############################
# List Files
############################
@router.get("/", response_model=List[FileModel])
async def list_files(user=Depends(get_verified_user)):
files = Files.get_files()
return files
############################
# Delete All Files
############################
@router.delete("/all")
async def delete_all_files(user=Depends(get_admin_user)):
result = Files.delete_all_files()
if result:
folder = f"{UPLOAD_DIR}"
try:
# Check if the directory exists
if os.path.exists(folder):
# Iterate over all the files and directories in the specified directory
for filename in os.listdir(folder):
file_path = os.path.join(folder, filename)
try:
if os.path.isfile(file_path) or os.path.islink(file_path):
os.unlink(file_path) # Remove the file or link
elif os.path.isdir(file_path):
shutil.rmtree(file_path) # Remove the directory
except Exception as e:
print(f"Failed to delete {file_path}. Reason: {e}")
else:
print(f"The directory {folder} does not exist")
except Exception as e:
print(f"Failed to process the directory {folder}. Reason: {e}")
return {"message": "All files deleted successfully"}
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error deleting files"),
)
############################
# Get File By Id
############################
@router.get("/{id}", response_model=Optional[FileModel])
async def get_file_by_id(id: str, user=Depends(get_verified_user)):
file = Files.get_file_by_id(id)
if file:
return file
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# Get File Content By Id
############################
@router.get("/{id}/content", response_model=Optional[FileModel])
async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
file = Files.get_file_by_id(id)
if file:
file_path = Path(file.meta["path"])
# Check if the file already exists in the cache
if file_path.is_file():
print(f"file_path: {file_path}")
return FileResponse(file_path)
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
@router.get("/{id}/content/{file_name}", response_model=Optional[FileModel])
async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
file = Files.get_file_by_id(id)
if file:
file_path = Path(file.meta["path"])
# Check if the file already exists in the cache
if file_path.is_file():
print(f"file_path: {file_path}")
return FileResponse(file_path)
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# Delete File By Id
############################
@router.delete("/{id}")
async def delete_file_by_id(id: str, user=Depends(get_verified_user)):
file = Files.get_file_by_id(id)
if file:
result = Files.delete_file_by_id(id)
if result:
return {"message": "File deleted successfully"}
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error deleting file"),
)
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)

View File

@@ -0,0 +1,426 @@
from fastapi import Depends, FastAPI, HTTPException, status, Request
from datetime import datetime, timedelta
from typing import List, Union, Optional
from fastapi import APIRouter
from pydantic import BaseModel
import json
from apps.webui.models.functions import (
Functions,
FunctionForm,
FunctionModel,
FunctionResponse,
)
from apps.webui.utils import load_function_module_by_id
from utils.utils import get_verified_user, get_admin_user
from constants import ERROR_MESSAGES
from importlib import util
import os
from pathlib import Path
from config import DATA_DIR, CACHE_DIR, FUNCTIONS_DIR
router = APIRouter()
############################
# GetFunctions
############################
@router.get("/", response_model=List[FunctionResponse])
async def get_functions(user=Depends(get_verified_user)):
return Functions.get_functions()
############################
# ExportFunctions
############################
@router.get("/export", response_model=List[FunctionModel])
async def get_functions(user=Depends(get_admin_user)):
return Functions.get_functions()
############################
# CreateNewFunction
############################
@router.post("/create", response_model=Optional[FunctionResponse])
async def create_new_function(
request: Request, form_data: FunctionForm, user=Depends(get_admin_user)
):
if not form_data.id.isidentifier():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Only alphanumeric characters and underscores are allowed in the id",
)
form_data.id = form_data.id.lower()
function = Functions.get_function_by_id(form_data.id)
if function == None:
function_path = os.path.join(FUNCTIONS_DIR, f"{form_data.id}.py")
try:
with open(function_path, "w") as function_file:
function_file.write(form_data.content)
function_module, function_type, frontmatter = load_function_module_by_id(
form_data.id
)
form_data.meta.manifest = frontmatter
FUNCTIONS = request.app.state.FUNCTIONS
FUNCTIONS[form_data.id] = function_module
function = Functions.insert_new_function(user.id, function_type, form_data)
function_cache_dir = Path(CACHE_DIR) / "functions" / form_data.id
function_cache_dir.mkdir(parents=True, exist_ok=True)
if function:
return function
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error creating function"),
)
except Exception as e:
print(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.ID_TAKEN,
)
############################
# GetFunctionById
############################
@router.get("/id/{id}", response_model=Optional[FunctionModel])
async def get_function_by_id(id: str, user=Depends(get_admin_user)):
function = Functions.get_function_by_id(id)
if function:
return function
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# ToggleFunctionById
############################
@router.post("/id/{id}/toggle", response_model=Optional[FunctionModel])
async def toggle_function_by_id(id: str, user=Depends(get_admin_user)):
function = Functions.get_function_by_id(id)
if function:
function = Functions.update_function_by_id(
id, {"is_active": not function.is_active}
)
if function:
return function
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error updating function"),
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# ToggleGlobalById
############################
@router.post("/id/{id}/toggle/global", response_model=Optional[FunctionModel])
async def toggle_global_by_id(id: str, user=Depends(get_admin_user)):
function = Functions.get_function_by_id(id)
if function:
function = Functions.update_function_by_id(
id, {"is_global": not function.is_global}
)
if function:
return function
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error updating function"),
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# UpdateFunctionById
############################
@router.post("/id/{id}/update", response_model=Optional[FunctionModel])
async def update_function_by_id(
request: Request, id: str, form_data: FunctionForm, user=Depends(get_admin_user)
):
function_path = os.path.join(FUNCTIONS_DIR, f"{id}.py")
try:
with open(function_path, "w") as function_file:
function_file.write(form_data.content)
function_module, function_type, frontmatter = load_function_module_by_id(id)
form_data.meta.manifest = frontmatter
FUNCTIONS = request.app.state.FUNCTIONS
FUNCTIONS[id] = function_module
updated = {**form_data.model_dump(exclude={"id"}), "type": function_type}
print(updated)
function = Functions.update_function_by_id(id, updated)
if function:
return function
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error updating function"),
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
############################
# DeleteFunctionById
############################
@router.delete("/id/{id}/delete", response_model=bool)
async def delete_function_by_id(
request: Request, id: str, user=Depends(get_admin_user)
):
result = Functions.delete_function_by_id(id)
if result:
FUNCTIONS = request.app.state.FUNCTIONS
if id in FUNCTIONS:
del FUNCTIONS[id]
# delete the function file
function_path = os.path.join(FUNCTIONS_DIR, f"{id}.py")
try:
os.remove(function_path)
except:
pass
return result
############################
# GetFunctionValves
############################
@router.get("/id/{id}/valves", response_model=Optional[dict])
async def get_function_valves_by_id(id: str, user=Depends(get_admin_user)):
function = Functions.get_function_by_id(id)
if function:
try:
valves = Functions.get_function_valves_by_id(id)
return valves
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# GetFunctionValvesSpec
############################
@router.get("/id/{id}/valves/spec", response_model=Optional[dict])
async def get_function_valves_spec_by_id(
request: Request, id: str, user=Depends(get_admin_user)
):
function = Functions.get_function_by_id(id)
if function:
if id in request.app.state.FUNCTIONS:
function_module = request.app.state.FUNCTIONS[id]
else:
function_module, function_type, frontmatter = load_function_module_by_id(id)
request.app.state.FUNCTIONS[id] = function_module
if hasattr(function_module, "Valves"):
Valves = function_module.Valves
return Valves.schema()
return None
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# UpdateFunctionValves
############################
@router.post("/id/{id}/valves/update", response_model=Optional[dict])
async def update_function_valves_by_id(
request: Request, id: str, form_data: dict, user=Depends(get_admin_user)
):
function = Functions.get_function_by_id(id)
if function:
if id in request.app.state.FUNCTIONS:
function_module = request.app.state.FUNCTIONS[id]
else:
function_module, function_type, frontmatter = load_function_module_by_id(id)
request.app.state.FUNCTIONS[id] = function_module
if hasattr(function_module, "Valves"):
Valves = function_module.Valves
try:
form_data = {k: v for k, v in form_data.items() if v is not None}
valves = Valves(**form_data)
Functions.update_function_valves_by_id(id, valves.model_dump())
return valves.model_dump()
except Exception as e:
print(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# FunctionUserValves
############################
@router.get("/id/{id}/valves/user", response_model=Optional[dict])
async def get_function_user_valves_by_id(id: str, user=Depends(get_verified_user)):
function = Functions.get_function_by_id(id)
if function:
try:
user_valves = Functions.get_user_valves_by_id_and_user_id(id, user.id)
return user_valves
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
@router.get("/id/{id}/valves/user/spec", response_model=Optional[dict])
async def get_function_user_valves_spec_by_id(
request: Request, id: str, user=Depends(get_verified_user)
):
function = Functions.get_function_by_id(id)
if function:
if id in request.app.state.FUNCTIONS:
function_module = request.app.state.FUNCTIONS[id]
else:
function_module, function_type, frontmatter = load_function_module_by_id(id)
request.app.state.FUNCTIONS[id] = function_module
if hasattr(function_module, "UserValves"):
UserValves = function_module.UserValves
return UserValves.schema()
return None
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
@router.post("/id/{id}/valves/user/update", response_model=Optional[dict])
async def update_function_user_valves_by_id(
request: Request, id: str, form_data: dict, user=Depends(get_verified_user)
):
function = Functions.get_function_by_id(id)
if function:
if id in request.app.state.FUNCTIONS:
function_module = request.app.state.FUNCTIONS[id]
else:
function_module, function_type, frontmatter = load_function_module_by_id(id)
request.app.state.FUNCTIONS[id] = function_module
if hasattr(function_module, "UserValves"):
UserValves = function_module.UserValves
try:
form_data = {k: v for k, v in form_data.items() if v is not None}
user_valves = UserValves(**form_data)
Functions.update_user_valves_by_id_and_user_id(
id, user.id, user_valves.model_dump()
)
return user_valves.model_dump()
except Exception as e:
print(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)

View File

@@ -7,7 +7,7 @@ from fastapi import APIRouter
from pydantic import BaseModel
import logging
from apps.web.models.memories import Memories, MemoryModel
from apps.webui.models.memories import Memories, MemoryModel
from utils.utils import get_verified_user
from constants import ERROR_MESSAGES
@@ -44,9 +44,15 @@ class AddMemoryForm(BaseModel):
content: str
class MemoryUpdateModel(BaseModel):
content: Optional[str] = None
@router.post("/add", response_model=Optional[MemoryModel])
async def add_memory(
request: Request, form_data: AddMemoryForm, user=Depends(get_verified_user)
request: Request,
form_data: AddMemoryForm,
user=Depends(get_verified_user),
):
memory = Memories.insert_new_memory(user.id, form_data.content)
memory_embedding = request.app.state.EMBEDDING_FUNCTION(memory.content)
@@ -62,6 +68,34 @@ async def add_memory(
return memory
@router.post("/{memory_id}/update", response_model=Optional[MemoryModel])
async def update_memory_by_id(
memory_id: str,
request: Request,
form_data: MemoryUpdateModel,
user=Depends(get_verified_user),
):
memory = Memories.update_memory_by_id(memory_id, form_data.content)
if memory is None:
raise HTTPException(status_code=404, detail="Memory not found")
if form_data.content is not None:
memory_embedding = request.app.state.EMBEDDING_FUNCTION(form_data.content)
collection = CHROMA_CLIENT.get_or_create_collection(
name=f"user-memory-{user.id}"
)
collection.upsert(
documents=[form_data.content],
ids=[memory.id],
embeddings=[memory_embedding],
metadatas=[
{"created_at": memory.created_at, "updated_at": memory.updated_at}
],
)
return memory
############################
# QueryMemory
############################
@@ -69,6 +103,7 @@ async def add_memory(
class QueryMemoryForm(BaseModel):
content: str
k: Optional[int] = 1
@router.post("/query")
@@ -80,7 +115,7 @@ async def query_memory(
results = collection.query(
query_embeddings=[query_embedding],
n_results=1, # how many results to return
n_results=form_data.k, # how many results to return
)
return results

View File

@@ -0,0 +1,113 @@
from fastapi import Depends, FastAPI, HTTPException, status, Request
from datetime import datetime, timedelta
from typing import List, Union, Optional
from fastapi import APIRouter
from pydantic import BaseModel
import json
from apps.webui.models.models import Models, ModelModel, ModelForm, ModelResponse
from utils.utils import get_verified_user, get_admin_user
from constants import ERROR_MESSAGES
router = APIRouter()
###########################
# getModels
###########################
@router.get("/", response_model=List[ModelResponse])
async def get_models(user=Depends(get_verified_user)):
return Models.get_all_models()
############################
# AddNewModel
############################
@router.post("/add", response_model=Optional[ModelModel])
async def add_new_model(
request: Request,
form_data: ModelForm,
user=Depends(get_admin_user),
):
if form_data.id in request.app.state.MODELS:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.MODEL_ID_TAKEN,
)
else:
model = Models.insert_new_model(form_data, user.id)
if model:
return model
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.DEFAULT(),
)
############################
# GetModelById
############################
@router.get("/", response_model=Optional[ModelModel])
async def get_model_by_id(id: str, user=Depends(get_verified_user)):
model = Models.get_model_by_id(id)
if model:
return model
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# UpdateModelById
############################
@router.post("/update", response_model=Optional[ModelModel])
async def update_model_by_id(
request: Request,
id: str,
form_data: ModelForm,
user=Depends(get_admin_user),
):
model = Models.get_model_by_id(id)
if model:
model = Models.update_model_by_id(id, form_data)
return model
else:
if form_data.id in request.app.state.MODELS:
model = Models.insert_new_model(form_data, user.id)
if model:
return model
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.DEFAULT(),
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.DEFAULT(),
)
############################
# DeleteModelById
############################
@router.delete("/delete", response_model=bool)
async def delete_model_by_id(id: str, user=Depends(get_admin_user)):
result = Models.delete_model_by_id(id)
return result

View File

@@ -6,9 +6,9 @@ from fastapi import APIRouter
from pydantic import BaseModel
import json
from apps.web.models.prompts import Prompts, PromptForm, PromptModel
from apps.webui.models.prompts import Prompts, PromptForm, PromptModel
from utils.utils import get_current_user, get_admin_user
from utils.utils import get_verified_user, get_admin_user
from constants import ERROR_MESSAGES
router = APIRouter()
@@ -19,7 +19,7 @@ router = APIRouter()
@router.get("/", response_model=List[PromptModel])
async def get_prompts(user=Depends(get_current_user)):
async def get_prompts(user=Depends(get_verified_user)):
return Prompts.get_prompts()
@@ -52,7 +52,7 @@ async def create_new_prompt(form_data: PromptForm, user=Depends(get_admin_user))
@router.get("/command/{command}", response_model=Optional[PromptModel])
async def get_prompt_by_command(command: str, user=Depends(get_current_user)):
async def get_prompt_by_command(command: str, user=Depends(get_verified_user)):
prompt = Prompts.get_prompt_by_command(f"/{command}")
if prompt:
@@ -71,7 +71,9 @@ async def get_prompt_by_command(command: str, user=Depends(get_current_user)):
@router.post("/command/{command}/update", response_model=Optional[PromptModel])
async def update_prompt_by_command(
command: str, form_data: PromptForm, user=Depends(get_admin_user)
command: str,
form_data: PromptForm,
user=Depends(get_admin_user),
):
prompt = Prompts.update_prompt_by_command(f"/{command}", form_data)
if prompt:

View File

@@ -0,0 +1,379 @@
from fastapi import Depends, FastAPI, HTTPException, status, Request
from datetime import datetime, timedelta
from typing import List, Union, Optional
from fastapi import APIRouter
from pydantic import BaseModel
import json
from apps.webui.models.users import Users
from apps.webui.models.tools import Tools, ToolForm, ToolModel, ToolResponse
from apps.webui.utils import load_toolkit_module_by_id
from utils.utils import get_admin_user, get_verified_user
from utils.tools import get_tools_specs
from constants import ERROR_MESSAGES
from importlib import util
import os
from pathlib import Path
from config import DATA_DIR, CACHE_DIR
TOOLS_DIR = f"{DATA_DIR}/tools"
os.makedirs(TOOLS_DIR, exist_ok=True)
router = APIRouter()
############################
# GetToolkits
############################
@router.get("/", response_model=List[ToolResponse])
async def get_toolkits(user=Depends(get_verified_user)):
toolkits = [toolkit for toolkit in Tools.get_tools()]
return toolkits
############################
# ExportToolKits
############################
@router.get("/export", response_model=List[ToolModel])
async def get_toolkits(user=Depends(get_admin_user)):
toolkits = [toolkit for toolkit in Tools.get_tools()]
return toolkits
############################
# CreateNewToolKit
############################
@router.post("/create", response_model=Optional[ToolResponse])
async def create_new_toolkit(
request: Request,
form_data: ToolForm,
user=Depends(get_admin_user),
):
if not form_data.id.isidentifier():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Only alphanumeric characters and underscores are allowed in the id",
)
form_data.id = form_data.id.lower()
toolkit = Tools.get_tool_by_id(form_data.id)
if toolkit == None:
toolkit_path = os.path.join(TOOLS_DIR, f"{form_data.id}.py")
try:
with open(toolkit_path, "w") as tool_file:
tool_file.write(form_data.content)
toolkit_module, frontmatter = load_toolkit_module_by_id(form_data.id)
form_data.meta.manifest = frontmatter
TOOLS = request.app.state.TOOLS
TOOLS[form_data.id] = toolkit_module
specs = get_tools_specs(TOOLS[form_data.id])
toolkit = Tools.insert_new_tool(user.id, form_data, specs)
tool_cache_dir = Path(CACHE_DIR) / "tools" / form_data.id
tool_cache_dir.mkdir(parents=True, exist_ok=True)
if toolkit:
return toolkit
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error creating toolkit"),
)
except Exception as e:
print(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.ID_TAKEN,
)
############################
# GetToolkitById
############################
@router.get("/id/{id}", response_model=Optional[ToolModel])
async def get_toolkit_by_id(id: str, user=Depends(get_admin_user)):
toolkit = Tools.get_tool_by_id(id)
if toolkit:
return toolkit
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# UpdateToolkitById
############################
@router.post("/id/{id}/update", response_model=Optional[ToolModel])
async def update_toolkit_by_id(
request: Request,
id: str,
form_data: ToolForm,
user=Depends(get_admin_user),
):
toolkit_path = os.path.join(TOOLS_DIR, f"{id}.py")
try:
with open(toolkit_path, "w") as tool_file:
tool_file.write(form_data.content)
toolkit_module, frontmatter = load_toolkit_module_by_id(id)
form_data.meta.manifest = frontmatter
TOOLS = request.app.state.TOOLS
TOOLS[id] = toolkit_module
specs = get_tools_specs(TOOLS[id])
updated = {
**form_data.model_dump(exclude={"id"}),
"specs": specs,
}
print(updated)
toolkit = Tools.update_tool_by_id(id, updated)
if toolkit:
return toolkit
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error updating toolkit"),
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
############################
# DeleteToolkitById
############################
@router.delete("/id/{id}/delete", response_model=bool)
async def delete_toolkit_by_id(request: Request, id: str, user=Depends(get_admin_user)):
result = Tools.delete_tool_by_id(id)
if result:
TOOLS = request.app.state.TOOLS
if id in TOOLS:
del TOOLS[id]
# delete the toolkit file
toolkit_path = os.path.join(TOOLS_DIR, f"{id}.py")
os.remove(toolkit_path)
return result
############################
# GetToolValves
############################
@router.get("/id/{id}/valves", response_model=Optional[dict])
async def get_toolkit_valves_by_id(id: str, user=Depends(get_admin_user)):
toolkit = Tools.get_tool_by_id(id)
if toolkit:
try:
valves = Tools.get_tool_valves_by_id(id)
return valves
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# GetToolValvesSpec
############################
@router.get("/id/{id}/valves/spec", response_model=Optional[dict])
async def get_toolkit_valves_spec_by_id(
request: Request, id: str, user=Depends(get_admin_user)
):
toolkit = Tools.get_tool_by_id(id)
if toolkit:
if id in request.app.state.TOOLS:
toolkit_module = request.app.state.TOOLS[id]
else:
toolkit_module, frontmatter = load_toolkit_module_by_id(id)
request.app.state.TOOLS[id] = toolkit_module
if hasattr(toolkit_module, "Valves"):
Valves = toolkit_module.Valves
return Valves.schema()
return None
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# UpdateToolValves
############################
@router.post("/id/{id}/valves/update", response_model=Optional[dict])
async def update_toolkit_valves_by_id(
request: Request, id: str, form_data: dict, user=Depends(get_admin_user)
):
toolkit = Tools.get_tool_by_id(id)
if toolkit:
if id in request.app.state.TOOLS:
toolkit_module = request.app.state.TOOLS[id]
else:
toolkit_module, frontmatter = load_toolkit_module_by_id(id)
request.app.state.TOOLS[id] = toolkit_module
if hasattr(toolkit_module, "Valves"):
Valves = toolkit_module.Valves
try:
form_data = {k: v for k, v in form_data.items() if v is not None}
valves = Valves(**form_data)
Tools.update_tool_valves_by_id(id, valves.model_dump())
return valves.model_dump()
except Exception as e:
print(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# ToolUserValves
############################
@router.get("/id/{id}/valves/user", response_model=Optional[dict])
async def get_toolkit_user_valves_by_id(id: str, user=Depends(get_verified_user)):
toolkit = Tools.get_tool_by_id(id)
if toolkit:
try:
user_valves = Tools.get_user_valves_by_id_and_user_id(id, user.id)
return user_valves
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
@router.get("/id/{id}/valves/user/spec", response_model=Optional[dict])
async def get_toolkit_user_valves_spec_by_id(
request: Request, id: str, user=Depends(get_verified_user)
):
toolkit = Tools.get_tool_by_id(id)
if toolkit:
if id in request.app.state.TOOLS:
toolkit_module = request.app.state.TOOLS[id]
else:
toolkit_module, frontmatter = load_toolkit_module_by_id(id)
request.app.state.TOOLS[id] = toolkit_module
if hasattr(toolkit_module, "UserValves"):
UserValves = toolkit_module.UserValves
return UserValves.schema()
return None
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
@router.post("/id/{id}/valves/user/update", response_model=Optional[dict])
async def update_toolkit_user_valves_by_id(
request: Request, id: str, form_data: dict, user=Depends(get_verified_user)
):
toolkit = Tools.get_tool_by_id(id)
if toolkit:
if id in request.app.state.TOOLS:
toolkit_module = request.app.state.TOOLS[id]
else:
toolkit_module, frontmatter = load_toolkit_module_by_id(id)
request.app.state.TOOLS[id] = toolkit_module
if hasattr(toolkit_module, "UserValves"):
UserValves = toolkit_module.UserValves
try:
form_data = {k: v for k, v in form_data.items() if v is not None}
user_valves = UserValves(**form_data)
Tools.update_user_valves_by_id_and_user_id(
id, user.id, user_valves.model_dump()
)
return user_valves.model_dump()
except Exception as e:
print(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)

View File

@@ -9,11 +9,22 @@ import time
import uuid
import logging
from apps.web.models.users import UserModel, UserUpdateForm, UserRoleUpdateForm, Users
from apps.web.models.auths import Auths
from apps.web.models.chats import Chats
from apps.webui.models.users import (
UserModel,
UserUpdateForm,
UserRoleUpdateForm,
UserSettings,
Users,
)
from apps.webui.models.auths import Auths
from apps.webui.models.chats import Chats
from utils.utils import get_verified_user, get_password_hash, get_admin_user
from utils.utils import (
get_verified_user,
get_password_hash,
get_current_user,
get_admin_user,
)
from constants import ERROR_MESSAGES
from config import SRC_LOG_LEVELS
@@ -68,6 +79,88 @@ async def update_user_role(form_data: UserRoleUpdateForm, user=Depends(get_admin
)
############################
# GetUserSettingsBySessionUser
############################
@router.get("/user/settings", response_model=Optional[UserSettings])
async def get_user_settings_by_session_user(user=Depends(get_verified_user)):
user = Users.get_user_by_id(user.id)
if user:
return user.settings
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.USER_NOT_FOUND,
)
############################
# UpdateUserSettingsBySessionUser
############################
@router.post("/user/settings/update", response_model=UserSettings)
async def update_user_settings_by_session_user(
form_data: UserSettings, user=Depends(get_verified_user)
):
user = Users.update_user_by_id(user.id, {"settings": form_data.model_dump()})
if user:
return user.settings
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.USER_NOT_FOUND,
)
############################
# GetUserInfoBySessionUser
############################
@router.get("/user/info", response_model=Optional[dict])
async def get_user_info_by_session_user(user=Depends(get_verified_user)):
user = Users.get_user_by_id(user.id)
if user:
return user.info
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.USER_NOT_FOUND,
)
############################
# UpdateUserInfoBySessionUser
############################
@router.post("/user/info/update", response_model=Optional[dict])
async def update_user_info_by_session_user(
form_data: dict, user=Depends(get_verified_user)
):
user = Users.get_user_by_id(user.id)
if user:
if user.info is None:
user.info = {}
user = Users.update_user_by_id(user.id, {"info": {**user.info, **form_data}})
if user:
return user.info
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.USER_NOT_FOUND,
)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.USER_NOT_FOUND,
)
############################
# GetUserById
############################
@@ -81,6 +174,8 @@ class UserResponse(BaseModel):
@router.get("/{user_id}", response_model=UserResponse)
async def get_user_by_id(user_id: str, user=Depends(get_verified_user)):
# Check if user_id is a shared chat
# If it is, get the user_id from the chat
if user_id.startswith("shared-"):
chat_id = user_id.replace("shared-", "")
chat = Chats.get_chat_by_id(chat_id)
@@ -110,7 +205,9 @@ async def get_user_by_id(user_id: str, user=Depends(get_verified_user)):
@router.post("/{user_id}/update", response_model=Optional[UserModel])
async def update_user_by_id(
user_id: str, form_data: UserUpdateForm, session_user=Depends(get_admin_user)
user_id: str,
form_data: UserUpdateForm,
session_user=Depends(get_admin_user),
):
user = Users.get_user_by_id(user_id)

View File

@@ -1,14 +1,14 @@
from fastapi import APIRouter, UploadFile, File, Response
from fastapi import Depends, HTTPException, status
from peewee import SqliteDatabase
from starlette.responses import StreamingResponse, FileResponse
from pydantic import BaseModel
from fpdf import FPDF
import markdown
import black
from apps.web.internal.db import DB
from utils.utils import get_admin_user
from utils.misc import calculate_sha256, get_gravatar_url
@@ -26,6 +26,21 @@ async def get_gravatar(
return get_gravatar_url(email)
class CodeFormatRequest(BaseModel):
code: str
@router.post("/code/format")
async def format_code(request: CodeFormatRequest):
try:
formatted_code = black.format_str(request.code, mode=black.Mode())
return {"code": formatted_code}
except black.NothingChanged:
return {"code": request.code}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
class MarkdownForm(BaseModel):
md: str
@@ -97,13 +112,24 @@ async def download_db(user=Depends(get_admin_user)):
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
if not isinstance(DB, SqliteDatabase):
from apps.webui.internal.db import engine
if engine.name != "sqlite":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DB_NOT_SQLITE,
)
return FileResponse(
DB.database,
engine.url.database,
media_type="application/octet-stream",
filename="webui.db",
)
@router.get("/litellm/config")
async def download_litellm_config_yaml(user=Depends(get_admin_user)):
return FileResponse(
f"{DATA_DIR}/litellm/config.yaml",
media_type="application/octet-stream",
filename="config.yaml",
)

View File

@@ -0,0 +1,88 @@
from importlib import util
import os
import re
from config import TOOLS_DIR, FUNCTIONS_DIR
def extract_frontmatter(file_path):
"""
Extract frontmatter as a dictionary from the specified file path.
"""
frontmatter = {}
frontmatter_started = False
frontmatter_ended = False
frontmatter_pattern = re.compile(r"^\s*([a-z_]+):\s*(.*)\s*$", re.IGNORECASE)
try:
with open(file_path, "r", encoding="utf-8") as file:
first_line = file.readline()
if first_line.strip() != '"""':
# The file doesn't start with triple quotes
return {}
frontmatter_started = True
for line in file:
if '"""' in line:
if frontmatter_started:
frontmatter_ended = True
break
if frontmatter_started and not frontmatter_ended:
match = frontmatter_pattern.match(line)
if match:
key, value = match.groups()
frontmatter[key.strip()] = value.strip()
except FileNotFoundError:
print(f"Error: The file {file_path} does not exist.")
return {}
except Exception as e:
print(f"An error occurred: {e}")
return {}
return frontmatter
def load_toolkit_module_by_id(toolkit_id):
toolkit_path = os.path.join(TOOLS_DIR, f"{toolkit_id}.py")
spec = util.spec_from_file_location(toolkit_id, toolkit_path)
module = util.module_from_spec(spec)
frontmatter = extract_frontmatter(toolkit_path)
try:
spec.loader.exec_module(module)
print(f"Loaded module: {module.__name__}")
if hasattr(module, "Tools"):
return module.Tools(), frontmatter
else:
raise Exception("No Tools class found")
except Exception as e:
print(f"Error loading module: {toolkit_id}")
# Move the file to the error folder
os.rename(toolkit_path, f"{toolkit_path}.error")
raise e
def load_function_module_by_id(function_id):
function_path = os.path.join(FUNCTIONS_DIR, f"{function_id}.py")
spec = util.spec_from_file_location(function_id, function_path)
module = util.module_from_spec(spec)
frontmatter = extract_frontmatter(function_path)
try:
spec.loader.exec_module(module)
print(f"Loaded module: {module.__name__}")
if hasattr(module, "Pipe"):
return module.Pipe(), "pipe", frontmatter
elif hasattr(module, "Filter"):
return module.Filter(), "filter", frontmatter
else:
raise Exception("No Function class found")
except Exception as e:
print(f"Error loading module: {function_id}")
# Move the file to the error folder
os.rename(function_path, f"{function_path}.error")
raise e

View File

@@ -1,11 +1,14 @@
import os
import sys
import logging
import importlib.metadata
import pkgutil
import chromadb
from chromadb import Settings
from base64 import b64encode
from bs4 import BeautifulSoup
from typing import TypeVar, Generic, Union
from typing import TypeVar, Generic
from pydantic import BaseModel
from typing import Optional
from pathlib import Path
import json
@@ -15,17 +18,21 @@ import markdown
import requests
import shutil
from secrets import token_bytes
from constants import ERROR_MESSAGES
####################################
# Load .env file
####################################
BACKEND_DIR = Path(__file__).parent # the path containing this file
BASE_DIR = BACKEND_DIR.parent # the path containing the backend/
print(BASE_DIR)
try:
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv("../.env"))
load_dotenv(find_dotenv(str(BASE_DIR / ".env")))
except ImportError:
print("dotenv not installed, skipping...")
@@ -51,7 +58,6 @@ log_sources = [
"CONFIG",
"DB",
"IMAGES",
"LITELLM",
"MAIN",
"MODELS",
"OLLAMA",
@@ -87,10 +93,12 @@ WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png"
ENV = os.environ.get("ENV", "dev")
try:
with open(f"../package.json", "r") as f:
PACKAGE_DATA = json.load(f)
PACKAGE_DATA = json.loads((BASE_DIR / "package.json").read_text())
except:
PACKAGE_DATA = {"version": "0.0.0"}
try:
PACKAGE_DATA = {"version": importlib.metadata.version("open-webui")}
except importlib.metadata.PackageNotFoundError:
PACKAGE_DATA = {"version": "0.0.0"}
VERSION = PACKAGE_DATA["version"]
@@ -115,10 +123,13 @@ def parse_section(section):
try:
with open("../CHANGELOG.md", "r") as file:
changelog_path = BASE_DIR / "CHANGELOG.md"
with open(str(changelog_path.absolute()), "r", encoding="utf8") as file:
changelog_content = file.read()
except:
changelog_content = ""
changelog_content = (pkgutil.get_data("open_webui", "CHANGELOG.md") or b"").decode()
# Convert markdown content to HTML
html_content = markdown.markdown(changelog_content)
@@ -155,21 +166,37 @@ CHANGELOG = changelog_json
####################################
# WEBUI_VERSION
# SAFE_MODE
####################################
WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.100")
SAFE_MODE = os.environ.get("SAFE_MODE", "false").lower() == "true"
####################################
# WEBUI_BUILD_HASH
####################################
WEBUI_BUILD_HASH = os.environ.get("WEBUI_BUILD_HASH", "dev-build")
####################################
# DATA/FRONTEND BUILD DIR
####################################
DATA_DIR = str(Path(os.getenv("DATA_DIR", "./data")).resolve())
FRONTEND_BUILD_DIR = str(Path(os.getenv("FRONTEND_BUILD_DIR", "../build")))
DATA_DIR = Path(os.getenv("DATA_DIR", BACKEND_DIR / "data")).resolve()
FRONTEND_BUILD_DIR = Path(os.getenv("FRONTEND_BUILD_DIR", BASE_DIR / "build")).resolve()
RESET_CONFIG_ON_START = (
os.environ.get("RESET_CONFIG_ON_START", "False").lower() == "true"
)
if RESET_CONFIG_ON_START:
try:
os.remove(f"{DATA_DIR}/config.json")
with open(f"{DATA_DIR}/config.json", "w") as f:
f.write("{}")
except:
pass
try:
with open(f"{DATA_DIR}/config.json", "r") as f:
CONFIG_DATA = json.load(f)
CONFIG_DATA = json.loads((DATA_DIR / "config.json").read_text())
except:
CONFIG_DATA = {}
@@ -271,22 +298,179 @@ WEBUI_AUTH = os.environ.get("WEBUI_AUTH", "True").lower() == "true"
WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get(
"WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None
)
WEBUI_AUTH_TRUSTED_NAME_HEADER = os.environ.get("WEBUI_AUTH_TRUSTED_NAME_HEADER", None)
JWT_EXPIRES_IN = PersistentConfig(
"JWT_EXPIRES_IN", "auth.jwt_expiry", os.environ.get("JWT_EXPIRES_IN", "-1")
)
####################################
# OAuth config
####################################
ENABLE_OAUTH_SIGNUP = PersistentConfig(
"ENABLE_OAUTH_SIGNUP",
"oauth.enable_signup",
os.environ.get("ENABLE_OAUTH_SIGNUP", "False").lower() == "true",
)
OAUTH_MERGE_ACCOUNTS_BY_EMAIL = PersistentConfig(
"OAUTH_MERGE_ACCOUNTS_BY_EMAIL",
"oauth.merge_accounts_by_email",
os.environ.get("OAUTH_MERGE_ACCOUNTS_BY_EMAIL", "False").lower() == "true",
)
OAUTH_PROVIDERS = {}
GOOGLE_CLIENT_ID = PersistentConfig(
"GOOGLE_CLIENT_ID",
"oauth.google.client_id",
os.environ.get("GOOGLE_CLIENT_ID", ""),
)
GOOGLE_CLIENT_SECRET = PersistentConfig(
"GOOGLE_CLIENT_SECRET",
"oauth.google.client_secret",
os.environ.get("GOOGLE_CLIENT_SECRET", ""),
)
GOOGLE_OAUTH_SCOPE = PersistentConfig(
"GOOGLE_OAUTH_SCOPE",
"oauth.google.scope",
os.environ.get("GOOGLE_OAUTH_SCOPE", "openid email profile"),
)
MICROSOFT_CLIENT_ID = PersistentConfig(
"MICROSOFT_CLIENT_ID",
"oauth.microsoft.client_id",
os.environ.get("MICROSOFT_CLIENT_ID", ""),
)
MICROSOFT_CLIENT_SECRET = PersistentConfig(
"MICROSOFT_CLIENT_SECRET",
"oauth.microsoft.client_secret",
os.environ.get("MICROSOFT_CLIENT_SECRET", ""),
)
MICROSOFT_CLIENT_TENANT_ID = PersistentConfig(
"MICROSOFT_CLIENT_TENANT_ID",
"oauth.microsoft.tenant_id",
os.environ.get("MICROSOFT_CLIENT_TENANT_ID", ""),
)
MICROSOFT_OAUTH_SCOPE = PersistentConfig(
"MICROSOFT_OAUTH_SCOPE",
"oauth.microsoft.scope",
os.environ.get("MICROSOFT_OAUTH_SCOPE", "openid email profile"),
)
OAUTH_CLIENT_ID = PersistentConfig(
"OAUTH_CLIENT_ID",
"oauth.oidc.client_id",
os.environ.get("OAUTH_CLIENT_ID", ""),
)
OAUTH_CLIENT_SECRET = PersistentConfig(
"OAUTH_CLIENT_SECRET",
"oauth.oidc.client_secret",
os.environ.get("OAUTH_CLIENT_SECRET", ""),
)
OPENID_PROVIDER_URL = PersistentConfig(
"OPENID_PROVIDER_URL",
"oauth.oidc.provider_url",
os.environ.get("OPENID_PROVIDER_URL", ""),
)
OAUTH_SCOPES = PersistentConfig(
"OAUTH_SCOPES",
"oauth.oidc.scopes",
os.environ.get("OAUTH_SCOPES", "openid email profile"),
)
OAUTH_PROVIDER_NAME = PersistentConfig(
"OAUTH_PROVIDER_NAME",
"oauth.oidc.provider_name",
os.environ.get("OAUTH_PROVIDER_NAME", "SSO"),
)
OAUTH_USERNAME_CLAIM = PersistentConfig(
"OAUTH_USERNAME_CLAIM",
"oauth.oidc.username_claim",
os.environ.get("OAUTH_USERNAME_CLAIM", "name"),
)
OAUTH_PICTURE_CLAIM = PersistentConfig(
"OAUTH_USERNAME_CLAIM",
"oauth.oidc.avatar_claim",
os.environ.get("OAUTH_PICTURE_CLAIM", "picture"),
)
def load_oauth_providers():
OAUTH_PROVIDERS.clear()
if GOOGLE_CLIENT_ID.value and GOOGLE_CLIENT_SECRET.value:
OAUTH_PROVIDERS["google"] = {
"client_id": GOOGLE_CLIENT_ID.value,
"client_secret": GOOGLE_CLIENT_SECRET.value,
"server_metadata_url": "https://accounts.google.com/.well-known/openid-configuration",
"scope": GOOGLE_OAUTH_SCOPE.value,
}
if (
MICROSOFT_CLIENT_ID.value
and MICROSOFT_CLIENT_SECRET.value
and MICROSOFT_CLIENT_TENANT_ID.value
):
OAUTH_PROVIDERS["microsoft"] = {
"client_id": MICROSOFT_CLIENT_ID.value,
"client_secret": MICROSOFT_CLIENT_SECRET.value,
"server_metadata_url": f"https://login.microsoftonline.com/{MICROSOFT_CLIENT_TENANT_ID.value}/v2.0/.well-known/openid-configuration",
"scope": MICROSOFT_OAUTH_SCOPE.value,
}
if (
OAUTH_CLIENT_ID.value
and OAUTH_CLIENT_SECRET.value
and OPENID_PROVIDER_URL.value
):
OAUTH_PROVIDERS["oidc"] = {
"client_id": OAUTH_CLIENT_ID.value,
"client_secret": OAUTH_CLIENT_SECRET.value,
"server_metadata_url": OPENID_PROVIDER_URL.value,
"scope": OAUTH_SCOPES.value,
"name": OAUTH_PROVIDER_NAME.value,
}
load_oauth_providers()
####################################
# Static DIR
####################################
STATIC_DIR = str(Path(os.getenv("STATIC_DIR", "./static")).resolve())
STATIC_DIR = Path(os.getenv("STATIC_DIR", BACKEND_DIR / "static")).resolve()
frontend_favicon = f"{FRONTEND_BUILD_DIR}/favicon.png"
if os.path.exists(frontend_favicon):
shutil.copyfile(frontend_favicon, f"{STATIC_DIR}/favicon.png")
frontend_favicon = FRONTEND_BUILD_DIR / "static" / "favicon.png"
if frontend_favicon.exists():
try:
shutil.copyfile(frontend_favicon, STATIC_DIR / "favicon.png")
except Exception as e:
logging.error(f"An error occurred: {e}")
else:
logging.warning(f"Frontend favicon not found at {frontend_favicon}")
frontend_splash = FRONTEND_BUILD_DIR / "static" / "splash.png"
if frontend_splash.exists():
try:
shutil.copyfile(frontend_splash, STATIC_DIR / "splash.png")
except Exception as e:
logging.error(f"An error occurred: {e}")
else:
logging.warning(f"Frontend splash not found at {frontend_splash}")
####################################
# CUSTOM_NAME
####################################
@@ -311,6 +495,19 @@ if CUSTOM_NAME:
r.raw.decode_content = True
shutil.copyfileobj(r.raw, f)
if "splash" in data:
url = (
f"https://api.openwebui.com{data['splash']}"
if data["splash"][0] == "/"
else data["splash"]
)
r = requests.get(url, stream=True)
if r.status_code == 200:
with open(f"{STATIC_DIR}/splash.png", "wb") as f:
r.raw.decode_content = True
shutil.copyfileobj(r.raw, f)
WEBUI_NAME = data["name"]
except Exception as e:
log.exception(e)
@@ -341,6 +538,22 @@ DOCS_DIR = os.getenv("DOCS_DIR", f"{DATA_DIR}/docs")
Path(DOCS_DIR).mkdir(parents=True, exist_ok=True)
####################################
# Tools DIR
####################################
TOOLS_DIR = os.getenv("TOOLS_DIR", f"{DATA_DIR}/tools")
Path(TOOLS_DIR).mkdir(parents=True, exist_ok=True)
####################################
# Functions DIR
####################################
FUNCTIONS_DIR = os.getenv("FUNCTIONS_DIR", f"{DATA_DIR}/functions")
Path(FUNCTIONS_DIR).mkdir(parents=True, exist_ok=True)
####################################
# LITELLM_CONFIG
####################################
@@ -368,21 +581,39 @@ def create_config_file(file_path):
LITELLM_CONFIG_PATH = f"{DATA_DIR}/litellm/config.yaml"
if not os.path.exists(LITELLM_CONFIG_PATH):
log.info("Config file doesn't exist. Creating...")
create_config_file(LITELLM_CONFIG_PATH)
log.info("Config file created successfully.")
# if not os.path.exists(LITELLM_CONFIG_PATH):
# log.info("Config file doesn't exist. Creating...")
# create_config_file(LITELLM_CONFIG_PATH)
# log.info("Config file created successfully.")
####################################
# OLLAMA_BASE_URL
####################################
ENABLE_OLLAMA_API = PersistentConfig(
"ENABLE_OLLAMA_API",
"ollama.enable",
os.environ.get("ENABLE_OLLAMA_API", "True").lower() == "true",
)
OLLAMA_API_BASE_URL = os.environ.get(
"OLLAMA_API_BASE_URL", "http://localhost:11434/api"
)
OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "")
AIOHTTP_CLIENT_TIMEOUT = os.environ.get("AIOHTTP_CLIENT_TIMEOUT", "")
if AIOHTTP_CLIENT_TIMEOUT == "":
AIOHTTP_CLIENT_TIMEOUT = None
else:
try:
AIOHTTP_CLIENT_TIMEOUT = int(AIOHTTP_CLIENT_TIMEOUT)
except:
AIOHTTP_CLIENT_TIMEOUT = 300
K8S_FLAG = os.environ.get("K8S_FLAG", "")
USE_OLLAMA_DOCKER = os.environ.get("USE_OLLAMA_DOCKER", "false")
@@ -477,6 +708,13 @@ ENABLE_SIGNUP = PersistentConfig(
else os.environ.get("ENABLE_SIGNUP", "True").lower() == "true"
),
)
DEFAULT_LOCALE = PersistentConfig(
"DEFAULT_LOCALE",
"ui.default_locale",
os.environ.get("DEFAULT_LOCALE", ""),
)
DEFAULT_MODELS = PersistentConfig(
"DEFAULT_MODELS", "ui.default_models", os.environ.get("DEFAULT_MODELS", None)
)
@@ -549,6 +787,117 @@ WEBHOOK_URL = PersistentConfig(
ENABLE_ADMIN_EXPORT = os.environ.get("ENABLE_ADMIN_EXPORT", "True").lower() == "true"
ENABLE_COMMUNITY_SHARING = PersistentConfig(
"ENABLE_COMMUNITY_SHARING",
"ui.enable_community_sharing",
os.environ.get("ENABLE_COMMUNITY_SHARING", "True").lower() == "true",
)
class BannerModel(BaseModel):
id: str
type: str
title: Optional[str] = None
content: str
dismissible: bool
timestamp: int
try:
banners = json.loads(os.environ.get("WEBUI_BANNERS", "[]"))
banners = [BannerModel(**banner) for banner in banners]
except Exception as e:
print(f"Error loading WEBUI_BANNERS: {e}")
banners = []
WEBUI_BANNERS = PersistentConfig("WEBUI_BANNERS", "ui.banners", banners)
SHOW_ADMIN_DETAILS = PersistentConfig(
"SHOW_ADMIN_DETAILS",
"auth.admin.show",
os.environ.get("SHOW_ADMIN_DETAILS", "true").lower() == "true",
)
ADMIN_EMAIL = PersistentConfig(
"ADMIN_EMAIL",
"auth.admin.email",
os.environ.get("ADMIN_EMAIL", None),
)
####################################
# TASKS
####################################
TASK_MODEL = PersistentConfig(
"TASK_MODEL",
"task.model.default",
os.environ.get("TASK_MODEL", ""),
)
TASK_MODEL_EXTERNAL = PersistentConfig(
"TASK_MODEL_EXTERNAL",
"task.model.external",
os.environ.get("TASK_MODEL_EXTERNAL", ""),
)
TITLE_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
"TITLE_GENERATION_PROMPT_TEMPLATE",
"task.title.prompt_template",
os.environ.get(
"TITLE_GENERATION_PROMPT_TEMPLATE",
"""Here is the query:
{{prompt:middletruncate:8000}}
Create a concise, 3-5 word phrase with an emoji as a title for the previous query. Suitable Emojis for the summary can be used to enhance understanding but avoid quotation marks or special formatting. RESPOND ONLY WITH THE TITLE TEXT.
Examples of titles:
📉 Stock Market Trends
🍪 Perfect Chocolate Chip Recipe
Evolution of Music Streaming
Remote Work Productivity Tips
Artificial Intelligence in Healthcare
🎮 Video Game Development Insights""",
),
)
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
"SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE",
"task.search.prompt_template",
os.environ.get(
"SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE",
"""You are tasked with generating web search queries. Give me an appropriate query to answer my question for google search. Answer with only the query. Today is {{CURRENT_DATE}}.
Question:
{{prompt:end:4000}}""",
),
)
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD = PersistentConfig(
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD",
"task.search.prompt_length_threshold",
int(
os.environ.get(
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD",
100,
)
),
)
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = PersistentConfig(
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE",
"task.tools.prompt_template",
os.environ.get(
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE",
"""Tools: {{TOOLS}}
If a function tool doesn't match the query, return an empty string. Else, pick a function tool, fill in the parameters from the function tool's schema, and return it in the format { "name": \"functionName\", "parameters": { "key": "value" } }. Only pick a function if the user asks. Only return the object. Do not return any other text.""",
),
)
####################################
# WEBUI_SECRET_KEY
####################################
@@ -560,9 +909,35 @@ WEBUI_SECRET_KEY = os.environ.get(
), # DEPRECATED: remove at next major version
)
WEBUI_SESSION_COOKIE_SAME_SITE = os.environ.get(
"WEBUI_SESSION_COOKIE_SAME_SITE",
os.environ.get("WEBUI_SESSION_COOKIE_SAME_SITE", "lax"),
)
WEBUI_SESSION_COOKIE_SECURE = os.environ.get(
"WEBUI_SESSION_COOKIE_SECURE",
os.environ.get("WEBUI_SESSION_COOKIE_SECURE", "false").lower() == "true",
)
if WEBUI_AUTH and WEBUI_SECRET_KEY == "":
raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND)
####################################
# RAG document content extraction
####################################
CONTENT_EXTRACTION_ENGINE = PersistentConfig(
"CONTENT_EXTRACTION_ENGINE",
"rag.CONTENT_EXTRACTION_ENGINE",
os.environ.get("CONTENT_EXTRACTION_ENGINE", "").lower(),
)
TIKA_SERVER_URL = PersistentConfig(
"TIKA_SERVER_URL",
"rag.tika_server_url",
os.getenv("TIKA_SERVER_URL", "http://tika:9998"), # Default for sidecar deployment
)
####################################
# RAG
####################################
@@ -631,6 +1006,12 @@ RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE = (
os.environ.get("RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE", "").lower() == "true"
)
RAG_EMBEDDING_OPENAI_BATCH_SIZE = PersistentConfig(
"RAG_EMBEDDING_OPENAI_BATCH_SIZE",
"rag.embedding_openai_batch_size",
os.environ.get("RAG_EMBEDDING_OPENAI_BATCH_SIZE", 1),
)
RAG_RERANKING_MODEL = PersistentConfig(
"RAG_RERANKING_MODEL",
"rag.reranking_model",
@@ -725,6 +1106,98 @@ YOUTUBE_LOADER_LANGUAGE = PersistentConfig(
os.getenv("YOUTUBE_LOADER_LANGUAGE", "en").split(","),
)
ENABLE_RAG_WEB_SEARCH = PersistentConfig(
"ENABLE_RAG_WEB_SEARCH",
"rag.web.search.enable",
os.getenv("ENABLE_RAG_WEB_SEARCH", "False").lower() == "true",
)
RAG_WEB_SEARCH_ENGINE = PersistentConfig(
"RAG_WEB_SEARCH_ENGINE",
"rag.web.search.engine",
os.getenv("RAG_WEB_SEARCH_ENGINE", ""),
)
# You can provide a list of your own websites to filter after performing a web search.
# This ensures the highest level of safety and reliability of the information sources.
RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = PersistentConfig(
"RAG_WEB_SEARCH_DOMAIN_FILTER_LIST",
"rag.rag.web.search.domain.filter_list",
[
# "wikipedia.com",
# "wikimedia.org",
# "wikidata.org",
],
)
SEARXNG_QUERY_URL = PersistentConfig(
"SEARXNG_QUERY_URL",
"rag.web.search.searxng_query_url",
os.getenv("SEARXNG_QUERY_URL", ""),
)
GOOGLE_PSE_API_KEY = PersistentConfig(
"GOOGLE_PSE_API_KEY",
"rag.web.search.google_pse_api_key",
os.getenv("GOOGLE_PSE_API_KEY", ""),
)
GOOGLE_PSE_ENGINE_ID = PersistentConfig(
"GOOGLE_PSE_ENGINE_ID",
"rag.web.search.google_pse_engine_id",
os.getenv("GOOGLE_PSE_ENGINE_ID", ""),
)
BRAVE_SEARCH_API_KEY = PersistentConfig(
"BRAVE_SEARCH_API_KEY",
"rag.web.search.brave_search_api_key",
os.getenv("BRAVE_SEARCH_API_KEY", ""),
)
SERPSTACK_API_KEY = PersistentConfig(
"SERPSTACK_API_KEY",
"rag.web.search.serpstack_api_key",
os.getenv("SERPSTACK_API_KEY", ""),
)
SERPSTACK_HTTPS = PersistentConfig(
"SERPSTACK_HTTPS",
"rag.web.search.serpstack_https",
os.getenv("SERPSTACK_HTTPS", "True").lower() == "true",
)
SERPER_API_KEY = PersistentConfig(
"SERPER_API_KEY",
"rag.web.search.serper_api_key",
os.getenv("SERPER_API_KEY", ""),
)
SERPLY_API_KEY = PersistentConfig(
"SERPLY_API_KEY",
"rag.web.search.serply_api_key",
os.getenv("SERPLY_API_KEY", ""),
)
TAVILY_API_KEY = PersistentConfig(
"TAVILY_API_KEY",
"rag.web.search.tavily_api_key",
os.getenv("TAVILY_API_KEY", ""),
)
RAG_WEB_SEARCH_RESULT_COUNT = PersistentConfig(
"RAG_WEB_SEARCH_RESULT_COUNT",
"rag.web.search.result_count",
int(os.getenv("RAG_WEB_SEARCH_RESULT_COUNT", "3")),
)
RAG_WEB_SEARCH_CONCURRENT_REQUESTS = PersistentConfig(
"RAG_WEB_SEARCH_CONCURRENT_REQUESTS",
"rag.web.search.concurrent_requests",
int(os.getenv("RAG_WEB_SEARCH_CONCURRENT_REQUESTS", "10")),
)
####################################
# Transcribe
####################################
@@ -756,6 +1229,11 @@ AUTOMATIC1111_BASE_URL = PersistentConfig(
"image_generation.automatic1111.base_url",
os.getenv("AUTOMATIC1111_BASE_URL", ""),
)
AUTOMATIC1111_API_AUTH = PersistentConfig(
"AUTOMATIC1111_API_AUTH",
"image_generation.automatic1111.api_auth",
os.getenv("AUTOMATIC1111_API_AUTH", ""),
)
COMFYUI_BASE_URL = PersistentConfig(
"COMFYUI_BASE_URL",
@@ -763,6 +1241,30 @@ COMFYUI_BASE_URL = PersistentConfig(
os.getenv("COMFYUI_BASE_URL", ""),
)
COMFYUI_CFG_SCALE = PersistentConfig(
"COMFYUI_CFG_SCALE",
"image_generation.comfyui.cfg_scale",
os.getenv("COMFYUI_CFG_SCALE", ""),
)
COMFYUI_SAMPLER = PersistentConfig(
"COMFYUI_SAMPLER",
"image_generation.comfyui.sampler",
os.getenv("COMFYUI_SAMPLER", ""),
)
COMFYUI_SCHEDULER = PersistentConfig(
"COMFYUI_SCHEDULER",
"image_generation.comfyui.scheduler",
os.getenv("COMFYUI_SCHEDULER", ""),
)
COMFYUI_SD3 = PersistentConfig(
"COMFYUI_SD3",
"image_generation.comfyui.sd3",
os.environ.get("COMFYUI_SD3", "").lower() == "true",
)
IMAGES_OPENAI_API_BASE_URL = PersistentConfig(
"IMAGES_OPENAI_API_BASE_URL",
"image_generation.openai.api_base_url",
@@ -792,38 +1294,60 @@ IMAGE_GENERATION_MODEL = PersistentConfig(
# Audio
####################################
AUDIO_OPENAI_API_BASE_URL = PersistentConfig(
"AUDIO_OPENAI_API_BASE_URL",
"audio.openai.api_base_url",
os.getenv("AUDIO_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
)
AUDIO_OPENAI_API_KEY = PersistentConfig(
"AUDIO_OPENAI_API_KEY",
"audio.openai.api_key",
os.getenv("AUDIO_OPENAI_API_KEY", OPENAI_API_KEY),
)
AUDIO_OPENAI_API_MODEL = PersistentConfig(
"AUDIO_OPENAI_API_MODEL",
"audio.openai.api_model",
os.getenv("AUDIO_OPENAI_API_MODEL", "tts-1"),
)
AUDIO_OPENAI_API_VOICE = PersistentConfig(
"AUDIO_OPENAI_API_VOICE",
"audio.openai.api_voice",
os.getenv("AUDIO_OPENAI_API_VOICE", "alloy"),
AUDIO_STT_OPENAI_API_BASE_URL = PersistentConfig(
"AUDIO_STT_OPENAI_API_BASE_URL",
"audio.stt.openai.api_base_url",
os.getenv("AUDIO_STT_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
)
####################################
# LiteLLM
####################################
AUDIO_STT_OPENAI_API_KEY = PersistentConfig(
"AUDIO_STT_OPENAI_API_KEY",
"audio.stt.openai.api_key",
os.getenv("AUDIO_STT_OPENAI_API_KEY", OPENAI_API_KEY),
)
AUDIO_STT_ENGINE = PersistentConfig(
"AUDIO_STT_ENGINE",
"audio.stt.engine",
os.getenv("AUDIO_STT_ENGINE", ""),
)
AUDIO_STT_MODEL = PersistentConfig(
"AUDIO_STT_MODEL",
"audio.stt.model",
os.getenv("AUDIO_STT_MODEL", "whisper-1"),
)
AUDIO_TTS_OPENAI_API_BASE_URL = PersistentConfig(
"AUDIO_TTS_OPENAI_API_BASE_URL",
"audio.tts.openai.api_base_url",
os.getenv("AUDIO_TTS_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
)
AUDIO_TTS_OPENAI_API_KEY = PersistentConfig(
"AUDIO_TTS_OPENAI_API_KEY",
"audio.tts.openai.api_key",
os.getenv("AUDIO_TTS_OPENAI_API_KEY", OPENAI_API_KEY),
)
ENABLE_LITELLM = os.environ.get("ENABLE_LITELLM", "True").lower() == "true"
AUDIO_TTS_ENGINE = PersistentConfig(
"AUDIO_TTS_ENGINE",
"audio.tts.engine",
os.getenv("AUDIO_TTS_ENGINE", ""),
)
LITELLM_PROXY_PORT = int(os.getenv("LITELLM_PROXY_PORT", "14365"))
if LITELLM_PROXY_PORT < 0 or LITELLM_PROXY_PORT > 65535:
raise ValueError("Invalid port number for LITELLM_PROXY_PORT")
LITELLM_PROXY_HOST = os.getenv("LITELLM_PROXY_HOST", "127.0.0.1")
AUDIO_TTS_MODEL = PersistentConfig(
"AUDIO_TTS_MODEL",
"audio.tts.model",
os.getenv("AUDIO_TTS_MODEL", "tts-1"),
)
AUDIO_TTS_VOICE = PersistentConfig(
"AUDIO_TTS_VOICE",
"audio.tts.voice",
os.getenv("AUDIO_TTS_VOICE", "alloy"),
)
####################################
@@ -831,3 +1355,7 @@ LITELLM_PROXY_HOST = os.getenv("LITELLM_PROXY_HOST", "127.0.0.1")
####################################
DATABASE_URL = os.environ.get("DATABASE_URL", f"sqlite:///{DATA_DIR}/webui.db")
# Replace the postgres:// with postgresql://
if "postgres://" in DATABASE_URL:
DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://")

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