Compare commits

..
353 Commits
Author SHA1 Message Date
Tim Jaeryang Baek aca37f592d Merge pull request #12807 from open-webui/dev
0.6.4
2025-04-12 23:01:19 -07:00
Timothy Jaeryang Baek 888b468576 fix 2025-04-12 23:00:34 -07:00
Timothy Jaeryang Baek 9348c05f1a doc: changelog 2025-04-12 22:57:13 -07:00
Timothy Jaeryang Baek 4dafbbccfc fix: rag template display issue 2025-04-12 22:55:24 -07:00
Tim Jaeryang Baek 8b0e565e2c Merge pull request #12715 from open-webui/dev
0.6.3
2025-04-12 22:44:24 -07:00
Timothy Jaeryang Baek 2766d7df6a doc: changelog 2025-04-12 22:44:00 -07:00
Tim Jaeryang Baek 8e810e758d Merge pull request #12806 from tth37/fix_web_rag_settings
fix: Resolve small bugs on updated web/rag settings
2025-04-12 22:26:04 -07:00
Timothy Jaeryang Baek e7821288f5 doc: changelog 2025-04-12 22:25:02 -07:00
tth37 db9d4ead59 format 2025-04-13 13:10:39 +08:00
Timothy Jaeryang Baek 1a6eaf6445 chore: dependency bump 2025-04-12 22:07:23 -07:00
tth37 8d53f1e770 fix: small bugs on updated web/rag settings 2025-04-13 12:55:50 +08:00
Timothy Jaeryang Baek c1444cbbba refac 2025-04-12 21:47:46 -07:00
Timothy Jaeryang Baek 40f9747ae4 refac 2025-04-12 21:44:47 -07:00
Timothy Jaeryang Baek ac8e81834c refac 2025-04-12 21:44:15 -07:00
Timothy Jaeryang Baek 8d62cc1935 refac 2025-04-12 21:09:26 -07:00
Timothy Jaeryang Baek f3fe82da80 fix: ongoing chat stop issue 2025-04-12 20:51:02 -07:00
Timothy Jaeryang Baek fa61065c1e refac 2025-04-12 20:15:00 -07:00
Timothy Jaeryang Baek 5844779384 refac: styling 2025-04-12 20:14:32 -07:00
Timothy Jaeryang Baek 451465151e refac 2025-04-12 19:31:02 -07:00
Tim Jaeryang Baek 85ef912f6a Merge pull request #12794 from TiancongLx/dev
i18n: update zh-TW
2025-04-12 19:27:26 -07:00
Tiancong Li 2b8cd4dff4 i18n: update zh-TW 2025-04-13 10:00:58 +08:00
Timothy Jaeryang Baek f6c8184a74 fix: model hide issue 2025-04-12 18:49:49 -07:00
Timothy Jaeryang Baek bde89fd29e refac: audio 2025-04-12 18:40:09 -07:00
Timothy Jaeryang Baek daabc188e8 fix: model list hide issue 2025-04-12 18:34:10 -07:00
Timothy Jaeryang Baek d5ee39de21 fix: katex display overflow issue 2025-04-12 18:27:23 -07:00
Timothy Jaeryang Baek cdb4904999 refac 2025-04-12 18:11:22 -07:00
Tim Jaeryang Baek 577406e00b Merge pull request #12788 from TiancongLx/dev
i18n: update zh-TW
2025-04-12 17:58:01 -07:00
Timothy Jaeryang Baek 175919c787 enh: widescreen mode in shared chats 2025-04-12 17:53:23 -07:00
Tiancong Li 03e2e5279f i18n: update zh-TW 2025-04-13 08:50:22 +08:00
Timothy Jaeryang Baek 4ca6247496 chore: format 2025-04-12 17:45:33 -07:00
Timothy Jaeryang Baek 40a3e7786b refac: voice recording 2025-04-12 17:41:53 -07:00
Timothy Jaeryang Baek 79dff685ea feat: multiple models chat user permissions 2025-04-12 17:37:30 -07:00
Timothy Jaeryang Baek 881d81d87f refac: default tool calling behaviour 2025-04-12 17:26:35 -07:00
Timothy Jaeryang Baek db8780fd9c refac: default tool boilerplate 2025-04-12 17:16:27 -07:00
Timothy Jaeryang Baek 0a69db291d enh: iframe sandbox policy setting 2025-04-12 17:09:39 -07:00
Timothy Jaeryang Baek af0d99b2a3 refac: artifacts styling 2025-04-12 17:01:45 -07:00
Tim Jaeryang Baek 540c27c589 Merge pull request #12784 from rgaricano/dev
UPD: Spanish Translation (es-ES)  Rev.0.6.3
2025-04-12 16:51:47 -07:00
_00_ 4106681889 UPD: Spanish Translation (es-ES) Rev.0.6.3
Revision of Spanish Translation (es-ES)
Adapted to Open-WebUI v.0.6.3

Has been done

Revision & Complete.
2025-04-13 01:42:28 +02:00
Timothy Jaeryang Baek 91a455a284 chore: format 2025-04-12 16:35:11 -07:00
Timothy Jaeryang Baek 77b25ae36a refac 2025-04-12 16:34:20 -07:00
Timothy Jaeryang Baek 48a23ce3fe refac: web/rag config 2025-04-12 16:33:36 -07:00
Timothy Jaeryang Baek c3497da5dd enh: only copy text message content 2025-04-12 15:11:03 -07:00
Timothy Jaeryang Baek bdef1001ac refac 2025-04-12 15:10:43 -07:00
Tim Jaeryang Baek 6862d8fba8 Merge pull request #12769 from tth37/feat_frontend_web_loader
feat: Add Frontend Configuration for `RAG_WEB_LOADER_ENGINE`
2025-04-12 14:56:47 -07:00
Tim Jaeryang Baek 02ba7aeb95 Merge pull request #12366 from Ithanil/redis_sentinel_2
chore: adapt websocket redis sentinel code to new native support in python-socketio
2025-04-12 11:41:30 -07:00
Jan Kessler c6755f9151 bump python-socketio to 5.13.0 (to support Redis Sentinel natively) 2025-04-12 19:22:10 +02:00
tth37 5eac5960ef feat: Add frontend configuration for web loader 2025-04-12 17:13:30 +08:00
Timothy Jaeryang Baek 30d02c638c refac: banners 2025-04-11 15:34:21 -07:00
Timothy Jaeryang Baek c5636ff68c refac 2025-04-11 15:27:25 -07:00
Timothy Jaeryang Baek c846a550fa refac 2025-04-11 15:19:47 -07:00
Timothy Jaeryang Baek 59dd798156 refac/security: set ac to private by default 2025-04-11 14:46:02 -07:00
Timothy Jaeryang Baek 5fea0a2221 refac: styling 2025-04-10 21:52:03 -07:00
Timothy Jaeryang Baek 6d63337ee9 refac 2025-04-10 21:22:14 -07:00
Timothy Jaeryang Baek b97ea72616 refac: tool call result display 2025-04-10 20:06:00 -07:00
Timothy Jaeryang Baekanddulicon 1921420319 refac
Co-Authored-By: dulicon <57261058+dulicon@users.noreply.github.com>
2025-04-10 20:01:07 -07:00
Timothy Jaeryang Baek a47a958edd refac: rename functions 2025-04-10 19:43:26 -07:00
Timothy Jaeryang Baek 53df5d3946 refac: rename hard to understand cryptic function names 2025-04-10 19:41:17 -07:00
Timothy Jaeryang Baek 0f27968ceb fix: tool server session auth 2025-04-10 17:47:07 -07:00
Tim Jaeryang Baek 36ac81b229 Merge pull request #12727 from decent-engineer-decent-datascientist/main
feat: add Azure AI Speech STT provider
2025-04-10 16:50:40 -07:00
priten 9a55257c5b feat: add Azure AI Speech STT provider
- Add Azure STT configuration variables for API key, region and locales
- Implement Azure STT transcription endpoint with 200MB file size limit
- Update Audio settings UI to include Azure STT configuration fields
- Handle Azure API responses and error cases consistently
2025-04-10 15:38:59 -05:00
Timothy Jaeryang Baek 05aa9c6d9c refac 2025-04-10 12:27:11 -07:00
Tim Jaeryang Baek f0efee5de4 Merge pull request #12603 from alpha-pet/fix-convert-ogg-container-openai-transcription
fix: Convert ogg to wav for OpenAI transcription endpoint
2025-04-10 12:25:46 -07:00
Timothy Jaeryang Baek bf0716e85e refac: styling 2025-04-10 12:05:01 -07:00
Timothy Jaeryang Baek f9bd49bf56 refac 2025-04-10 11:18:53 -07:00
Tim Jaeryang Baek 4cbf16c456 Merge pull request #12717 from Kheiden/progressive-webapp-enhancements
feat: Add PWA external manifest URL env var
2025-04-10 10:55:54 -07:00
Timothy Jaeryang BaekandRobert Piosik 1eba6a812e enh: smaller code text size
Co-Authored-By: Robert Piosik <robertpiosik@gmail.com>
2025-04-10 10:50:46 -07:00
Timothy Jaeryang Baek 3f71929a69 refac: styling 2025-04-10 10:46:44 -07:00
Timothy Jaeryang Baek d8780e4de2 feat: detect artifacts toggle 2025-04-10 10:36:57 -07:00
Kurt Heiden c12f8d28b9 Add PWA external manifest URL env var 2025-04-10 11:35:43 -06:00
Timothy Jaeryang Baek d4297a3e3e refac: styling 2025-04-10 10:31:38 -07:00
Timothy Jaeryang Baek fb0ee1329d enh: tool server data timeout 2025-04-10 10:23:14 -07:00
Timothy Jaeryang Baek 23dcae7054 refac: selector 2025-04-10 10:18:56 -07:00
Tim Jaeryang Baek 7aab8b9307 Merge pull request #12567 from aleprj/fix/ms-sso
fix: Make Microsoft SSO work even when custom signing keys. #12475
2025-04-10 10:11:49 -07:00
Timothy Jaeryang BaekandAnthony Durussel f10566f3de feat: allow events from pipelines
Co-Authored-By: Anthony Durussel <87324020+anthonydurussel@users.noreply.github.com>
2025-04-10 09:20:18 -07:00
Timothy Jaeryang Baek 0611eb8ac7 refac: layout load 2025-04-10 09:15:08 -07:00
Tim Jaeryang Baek a3e477e100 Merge pull request #12602 from alpha-pet/fix-tool-server-indexing-mismatch
fix: mismatch between TOOL_SERVERS / TOOL_SERVER_CONNECTIONS indexing
2025-04-10 09:06:30 -07:00
Tim Jaeryang Baek b40b50fb2d Merge pull request #12563 from IONOS-Productivity/oidc-add-support-for-pkce
OIDC add support for PKCE
2025-04-10 09:03:31 -07:00
Tim Jaeryang Baek e065073bea Merge pull request #12689 from asncx/fix-folder-chat-rename
fix: Chat rename not updating inside folders
2025-04-10 08:51:22 -07:00
Tim Jaeryang Baek 33b35b797c Merge pull request #12594 from gaby/search-files
feat: Add support for searching files by name
2025-04-10 08:50:13 -07:00
Tim Jaeryang Baek ef14d0b46d Merge pull request #12562 from almajo/fix/agrimm/correct_knowledge_base_citations
fix: Correct Citations for knowledge base models
2025-04-10 08:48:47 -07:00
Tim Jaeryang Baek 62ef0bad6f Merge pull request #12680 from lucyknada/patch-1
fix #12678
2025-04-10 08:46:41 -07:00
Timothy Jaeryang Baek 63e5200e2f refac 2025-04-10 08:46:12 -07:00
Tim Jaeryang Baek 1865e29bfc Merge pull request #12683 from Youggls/dev
feat: Add sougou web search engine.
2025-04-10 08:45:15 -07:00
Tim Jaeryang Baek f7dc6474ee Merge pull request #12694 from SadmL/dev
[i18n] Russian locale misspelling fix
2025-04-10 08:43:51 -07:00
Timothy Jaeryang Baek 536bc36e36 chore: bump duckduckgo 2025-04-10 08:42:03 -07:00
SadmL de926a060d [i18n] Russian locale misspelling fix 2025-04-10 13:17:52 +03:00
Thomas Rehn 4731e0d0e3 fix: convert webm to wav for OpenAI transcription endpoint 2025-04-10 09:00:51 +02:00
Youggls 3e2a6df1fb feat: Add sougou web search API for backend, add config panel in for frontend. 2025-04-10 14:51:44 +08:00
lucy bc295546cd fix #12678 2025-04-10 07:23:34 +02:00
Tim Jaeryang Baek 6c9a4d6ce6 Merge pull request #12606 from alpha-pet/feat-openapi-yaml-support
Feat: openapi yaml support
2025-04-09 18:37:09 -07:00
Tim Jaeryang Baek e27c5989bc Merge pull request #12657 from noamtamim/patch-1
docs: Set FastAPI app title to "Open WebUI"
2025-04-09 18:32:26 -07:00
Tim Jaeryang Baek 5f116d69c2 Merge pull request #12664 from Beni34/main
Updated Hungarian language to full. translation.json
2025-04-09 16:43:08 -07:00
Tim Jaeryang Baek a27dcc84a3 Merge pull request #12654 from valckmir/main
feat: disable persistent config
2025-04-09 16:16:14 -07:00
Beni34 7cee0073cf Update translation.json 2025-04-09 21:40:23 +02:00
Miroslav Valcicak a5faaf388e refactor: rename persistent config variable 2025-04-09 21:22:25 +02:00
asncx 87af2692e5 fix: folder chat rename 2025-04-09 20:56:49 +02:00
Noam Tamim 15fd822916 Set FastAPI title to Open WebUI
This affects the generated OpenAPI (`/docs` and `/openapi.json`).
2025-04-09 19:45:17 +03:00
Miroslav Valcicak 68aea6bf10 feat: disable persistent config 2025-04-09 16:18:58 +02:00
Tim Jaeryang Baek 38d5b03685 Merge pull request #12647 from behrmann/multimail
fix: choose the first mail if multiple are returned from LDAP
2025-04-09 03:47:49 -07:00
Jörg Behrmann 2e7a01f30a fix: choose the first mail if multiple are returned from LDAP 2025-04-09 10:53:02 +02:00
Tim Jaeryang Baek a363c0619a Merge pull request #12638 from panda44312/patch-12
i18n: zh-cn
2025-04-09 01:45:41 -07:00
Tim Jaeryang Baek f49d3d09f5 Merge pull request #12637 from TiancongLx/dev
i18n: update zh-TW
2025-04-09 01:45:31 -07:00
Thomas Rehn 17ea20d3ad feat: OpenAPI YAML support for frontend toolserver 2025-04-09 09:46:44 +02:00
Panda 92611b588c i18n: zh-cn 2025-04-09 09:44:10 +02:00
Tiancong Li de1e9f3c0f i18n: update zh-TW 2025-04-09 15:25:37 +08:00
Tim Jaeryang Baek d8c0a1227a Merge pull request #12552 from tth37/dev
i18n: update zh-CN
2025-04-08 22:17:57 -07:00
Tim Jaeryang Baek 84ea9ea011 Merge pull request #12555 from SadmL/dev
[i18n] Russian locale update
2025-04-08 22:17:50 -07:00
Tim Jaeryang Baek b130b55f5b Merge pull request #12571 from hurxxxx/feat/reindex-knowledge-files
feat: reindex knowledge files
2025-04-08 22:10:35 -07:00
Tim Jaeryang Baek 2ff1657a7a Merge pull request #12573 from gem85247/dev
fix: manifest.json cross-origin issues
2025-04-08 22:07:43 -07:00
Tim Jaeryang Baek 2575dac4ed Merge pull request #12604 from maurerle/ddg_improve_stacktrace
**fix** improve stack trace of duckduckgo exception
2025-04-08 13:03:57 -07:00
Tim Jaeryang Baek 8c3146f6f8 Merge pull request #12615 from robert-norberg/additional-RAG-logging
chore: Add debug logging to RAG utils
2025-04-08 13:03:38 -07:00
Tim Jaeryang Baek 1f12023d85 Merge pull request #12617 from Classic298/dev
feat: UVICORN_WORKERS variable and docs
2025-04-08 12:52:47 -07:00
Timothy Jaeryang Baek 63eab42208 refac 2025-04-08 12:51:48 -07:00
Timothy Jaeryang Baek 60d11c1f6f enh: password max length verification 2025-04-08 12:50:25 -07:00
DrMelone f17befc853 Update __init__.py 2025-04-08 18:55:07 +02:00
DrMelone 65b5c6c0c7 Update start_windows.bat 2025-04-08 18:52:12 +02:00
DrMelone 2c1d0e3857 Update start.sh 2025-04-08 18:50:07 +02:00
DrMelone 4c9a791b06 Update env.py 2025-04-08 18:47:48 +02:00
DrMelone f822d89350 Update env.py 2025-04-08 18:38:00 +02:00
Robert Norberg 2337b36609 add debug logging to RAG utils 2025-04-08 12:08:32 -04:00
Thomas Rehn 81af20aa9b refac: use logging instead of print 2025-04-08 15:16:55 +02:00
Thomas Rehn 27e8c6fce4 feat: add support for OpenAPI spec in YAML format 2025-04-08 15:16:52 +02:00
Thomas Rehn d99a883867 fix: convert ogg to wav for OpenAI transcription endpoint 2025-04-08 15:04:04 +02:00
Thomas Rehn f5b2867e45 fix: mismatch between TOOL_SERVERS / TOOL_SERVER_CONNECTIONS indexing 2025-04-08 14:08:08 +02:00
Florian Maurer 760ea3f4af duckduckgo: backend api has been deprecated since december
also increase duckduckgo-search version

see https://github.com/deedy5/duckduckgo_search/commit/3ee8e08b1c8efc9b7356f406db299f1114dc4860
2025-04-08 14:02:06 +02:00
Florian Maurer 337c7caafa improve stack trace of duckduckgo exception
* fix search_results out of scope
* ddgs.text does already always return a list
2025-04-08 13:52:23 +02:00
Haodong Tian d50ca0aa43 Update zh-CN translation.json 2025-04-08 15:49:20 +08:00
Haodong Tian 399e7b1670 Update zh-CN translation.json 2025-04-08 15:47:24 +08:00
Haodong Tian e92d3073bb Update zh-CN translation.json
- Minor fix
2025-04-08 15:37:50 +08:00
Haodong Tian 00f58658f7 i18n: Apply i18n to Web Search proxy tooltips 2025-04-08 07:01:56 +00:00
Haodong Tian b15adc63fe Update zh-CN translation.json
- Standardize Chinese translations for "autocomplete"
2025-04-08 14:24:39 +08:00
Juan Calderon-Perez fed47f2e2b Add content param to /search route 2025-04-08 01:00:06 -04:00
Juan Calderon-Perez 1c60b8d543 Rewrite logic 2025-04-08 00:56:21 -04:00
Juan Calderon-Perez 8609ca3657 Fix formatting again 2025-04-08 00:49:55 -04:00
Juan Calderon-Perez e06ff17a70 Fix formatting 2025-04-08 00:48:54 -04:00
Juan Calderon-Perez 7c1b004687 simplify logic 2025-04-08 00:44:47 -04:00
Juan Calderon-Perez 353b104c77 add support for searching files 2025-04-08 00:42:37 -04:00
Tim Jaeryang Baek 66322727e3 Merge pull request #12580 from lainedfles/opentelemetry-requirements-fix
fix: Upgrade OpenTelemetry instrumentation packages to 0.52b1
2025-04-07 14:19:17 -07:00
Self Denial 0e71b96597 fix: Upgrade OpenTelemetry instrumentation packages to 0.52b1
This commit updates all OpenTelemetry instrumentation dependencies (FastAPI, SQLAlchemy, Redis, Requests, Logging, HTTPX, aiohttp-client) from version 0.52b0 to 0.52b1 to satisfy opentelemetry-sdk==1.31.1 dependencies.
2025-04-07 14:49:47 -06:00
Gilles M f3f45209e9 fix: manifest.json cross-origin
When utilizing Cloudflare Zero Trust, fetching the manifest.json file may encounter issues because the request is redirected through the Zero Trust authentication page.
2025-04-07 19:01:18 +02:00
hurxxxx e7715f3760 i18n: add reindex knowledge files dialog 2025-04-08 01:37:50 +09:00
hurxxxx 7c828015d3 fix: ReindexKnowledgeFilesConfirmDialog 2025-04-08 00:53:11 +09:00
hurxxxx 4e545d432b feat: add new admin func - reindex knowledge files 2025-04-08 00:44:10 +09:00
aleprj e8f02328f4 Merge branch 'open-webui:main' into fix/ms-sso 2025-04-07 11:16:21 -03:00
aleprj 818d24e672 fix: add appid to the URL to ensure we fetch the correct parameters in all scenarios 2025-04-07 11:15:47 -03:00
Alexander Grimm 1e7a36478b ~ update to latest change 2025-04-07 15:16:47 +02:00
Alexander Grimm d8a7e9c660 fix: correct document citation 2025-04-07 15:16:47 +02:00
Thomas Lehmann 5c658a4879 feat(config): add config OAUTH_CODE_CHALLENGE_METHOD
Add support to enable OIDC code challenge method (PKCE).
2025-04-07 14:03:52 +02:00
SadmL b81789190e [i18n] Russian locale update 2025-04-07 12:04:58 +03:00
Haodong Tian f172403049 Update zh-CN translation.json 2025-04-07 16:11:54 +08:00
Timothy Jaeryang Baek 9cb1104f09 refac 2025-04-07 00:58:32 -07:00
Timothy Jaeryang Baek d1a77a9110 Merge pull request #12549 from ulagbulag/fix/500-when-unknown-models
fix: internal server error when calling completions API with non-existent model names
2025-04-07 00:58:02 -07:00
Ho Kim 1efae95a36 fix: internal server error when calling completions API with non-existent model names
Signed-off-by: Ho Kim <ho.kim@ulagbulag.io>
2025-04-07 06:48:39 +00:00
Timothy Jaeryang Baek 54a74b2c0a chore: bump 2025-04-06 23:46:59 -07:00
Timothy Jaeryang Baek 908492702a chore: bump 2025-04-06 23:46:40 -07:00
Timothy Jaeryang Baek 5553ec3e51 Merge pull request #12537 from open-webui/dependabot/npm_and_yarn/dev/tiptap/pm-2.11.7
build(deps): bump @tiptap/pm from 2.10.0 to 2.11.7
2025-04-06 23:45:59 -07:00
Timothy Jaeryang Baek ddec6cf0fa Merge pull request #12545 from mogorman/elixir_syntax_highlighting
feat: add Elixir highlighting
2025-04-06 23:45:30 -07:00
Timothy Jaeryang Baek 1857f152c2 Merge pull request #12548 from aleixdorca/dev
i18n: Update Catalan translation.json
2025-04-06 23:45:07 -07:00
Timothy Jaeryang Baek c5deb5cda4 Merge pull request #12541 from open-webui/dependabot/uv/dev/opentelemetry-instrumentation-logging-0.52b0
build(deps): bump opentelemetry-instrumentation-logging from 0.51b0 to 0.52b0
2025-04-06 23:43:48 -07:00
Timothy Jaeryang Baek d3ed20e907 Merge pull request #12535 from open-webui/dependabot/pip/backend/dev/youtube-transcript-api-1.0.3
build(deps): bump youtube-transcript-api from 0.6.3 to 1.0.3 in /backend
2025-04-06 23:43:35 -07:00
Timothy Jaeryang Baek 82252cb760 Merge pull request #12532 from open-webui/dependabot/pip/backend/dev/chromadb-0.6.3
build(deps): bump chromadb from 0.6.2 to 0.6.3 in /backend
2025-04-06 23:43:08 -07:00
Timothy Jaeryang Baek 9e700dc9a2 Merge pull request #12533 from open-webui/dependabot/pip/backend/dev/pgvector-0.4.0
build(deps): bump pgvector from 0.3.5 to 0.4.0 in /backend
2025-04-06 23:42:30 -07:00
Timothy Jaeryang Baek e1f60feb7f Merge pull request #12529 from open-webui/dependabot/pip/backend/dev/azure-identity-1.21.0
build(deps): bump azure-identity from 1.20.0 to 1.21.0 in /backend
2025-04-06 23:41:53 -07:00
Timothy Jaeryang Baek aefb3117db fix: chat completion non-existent model issue 2025-04-06 23:40:48 -07:00
Aleix Dorca 76d8935d0b Update catalan translation.json 2025-04-07 08:31:44 +02:00
dependabot[bot] dbc06084a7 build(deps): bump opentelemetry-instrumentation-logging
Bumps [opentelemetry-instrumentation-logging](https://github.com/open-telemetry/opentelemetry-python-contrib) from 0.51b0 to 0.52b0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-python-contrib/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-python-contrib/commits)

---
updated-dependencies:
- dependency-name: opentelemetry-instrumentation-logging
  dependency-version: 0.52b0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 03:43:31 +00:00
Matthew O'Gorman 8a81f5f188 add support for elixir code syntax highlighting 2025-04-06 23:42:53 -04:00
dependabot[bot] ba66995532 build(deps): bump @tiptap/pm from 2.10.0 to 2.11.7
Bumps [@tiptap/pm](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/pm) from 2.10.0 to 2.11.7.
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/@tiptap/pm@2.11.7/packages/pm/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/@tiptap/pm@2.11.7/packages/pm)

---
updated-dependencies:
- dependency-name: "@tiptap/pm"
  dependency-version: 2.11.7
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 03:42:44 +00:00
dependabot[bot] 96ecdc37b3 build(deps): bump youtube-transcript-api from 0.6.3 to 1.0.3 in /backend
Bumps [youtube-transcript-api](https://github.com/jdepoix/youtube-transcript-api) from 0.6.3 to 1.0.3.
- [Release notes](https://github.com/jdepoix/youtube-transcript-api/releases)
- [Commits](https://github.com/jdepoix/youtube-transcript-api/compare/v0.6.3...v1.0.3)

---
updated-dependencies:
- dependency-name: youtube-transcript-api
  dependency-version: 1.0.3
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 03:42:21 +00:00
dependabot[bot] f187a1c9ef build(deps): bump pgvector from 0.3.5 to 0.4.0 in /backend
Bumps [pgvector](https://github.com/pgvector/pgvector-python) from 0.3.5 to 0.4.0.
- [Changelog](https://github.com/pgvector/pgvector-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/pgvector/pgvector-python/compare/v0.3.5...v0.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 03:42:17 +00:00
dependabot[bot] 9e8b7ac611 build(deps): bump chromadb from 0.6.2 to 0.6.3 in /backend
Bumps [chromadb](https://github.com/chroma-core/chroma) from 0.6.2 to 0.6.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.6.2...0.6.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 03:42:14 +00:00
dependabot[bot] 5379c08b9d build(deps): bump azure-identity from 1.20.0 to 1.21.0 in /backend
Bumps [azure-identity](https://github.com/Azure/azure-sdk-for-python) from 1.20.0 to 1.21.0.
- [Release notes](https://github.com/Azure/azure-sdk-for-python/releases)
- [Changelog](https://github.com/Azure/azure-sdk-for-python/blob/main/doc/esrp_release.md)
- [Commits](https://github.com/Azure/azure-sdk-for-python/compare/azure-identity_1.20.0...azure-identity_1.21.0)

---
updated-dependencies:
- dependency-name: azure-identity
  dependency-version: 1.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 03:42:11 +00:00
Timothy Jaeryang Baek 63533c9e3a Merge pull request #12524 from open-webui/dev
0.6.2
2025-04-06 20:41:10 -07:00
Timothy Jaeryang Baek 8e8bc3125e doc: changelog 2025-04-06 20:41:03 -07:00
Timothy Jaeryang Baek 2c083f58d9 chore: format 2025-04-06 20:36:24 -07:00
Timothy Jaeryang Baek 93962f01cc doc: changelog 2025-04-06 20:34:47 -07:00
Timothy Jaeryang Baek 40d019ff50 refac 2025-04-06 20:23:23 -07:00
Timothy Jaeryang Baek 3885ea4a15 Merge pull request #12526 from gaby/dependabot-update
chore: Dependabot update
2025-04-06 19:46:03 -07:00
Juan Calderon-Perez bd28173de6 Track backend/requirements.txt updates 2025-04-06 22:33:34 -04:00
Timothy Jaeryang Baek 06820e701c refac: citations modal 2025-04-06 19:14:24 -07:00
Timothy Jaeryang Baek 2be08f27ea revert 2025-04-06 19:12:08 -07:00
Timothy Jaeryang Baek fabe5876c0 Merge pull request #12525 from TiancongLx/dev
i18n: update zh-TW
2025-04-06 18:57:19 -07:00
Tiancong Li f1a4afeb8f i18n: update zh-TW 2025-04-07 09:28:20 +08:00
Timothy Jaeryang Baek 914eb49767 chore: include accelerate dependency 2025-04-06 17:44:05 -07:00
Timothy Jaeryang Baek a8bc0d65a0 chore: format 2025-04-06 17:36:16 -07:00
Timothy Jaeryang Baek cbe2056587 fix: audio file upload response issue 2025-04-06 17:31:50 -07:00
Timothy Jaeryang Baek 65ed76abe1 refac: embedding prefix 2025-04-06 17:17:24 -07:00
Timothy Jaeryang Baek bc17511a05 refac 2025-04-06 17:10:09 -07:00
Timothy Jaeryang Baek 3413dd8ae1 refac 2025-04-06 17:02:39 -07:00
Timothy Jaeryang Baek 03759e79a3 refac 2025-04-06 17:00:46 -07:00
Timothy Jaeryang Baek 20211a489f refac 2025-04-06 16:57:36 -07:00
Timothy Jaeryang Baek 5d2e725f81 refac 2025-04-06 16:53:30 -07:00
Timothy Jaeryang Baek 2722613cde refac 2025-04-06 16:43:58 -07:00
Timothy Jaeryang Baek f243e523a6 refac 2025-04-06 15:52:38 -07:00
Timothy Jaeryang Baek 155dbd5a66 refac 2025-04-06 15:45:48 -07:00
Timothy Jaeryang Baek 9825d03602 Merge pull request #12507 from Ithanil/fix_web_result_collection_source_ids
fix: fix web results all getting the same source id when using embedding and retrieval
2025-04-06 15:43:21 -07:00
Timothy Jaeryang Baek 66cd40f959 chore: format 2025-04-06 15:37:46 -07:00
Timothy Jaeryang Baek dc2f8ec8c6 refac 2025-04-06 15:37:27 -07:00
Timothy Jaeryang Baek bf118718b3 Merge pull request #12521 from gaby/optimize-workflow
chore: Only run GitHub workflows when needed
2025-04-06 15:24:20 -07:00
Timothy Jaeryang Baek 64a0b28dab refac 2025-04-06 15:23:40 -07:00
Timothy Jaeryang Baek 6751d68034 Merge pull request #12506 from Ithanil/fix_web_result_source_ids
fix: fix web results all getting the same source id when bypassing embedding and retrieval
2025-04-06 15:21:14 -07:00
Timothy Jaeryang Baek 6274f4d0d3 Merge pull request #12520 from gaby/bump-docker
chore: Bump Python Docker image to python3.12
2025-04-06 15:17:27 -07:00
Juan Calderon-Perez 624c5250b1 Optimize GitHub actions 2025-04-06 18:15:56 -04:00
Timothy Jaeryang Baek c1ff69721a refac 2025-04-06 15:14:57 -07:00
Timothy Jaeryang Baek 1e98ae7608 Merge pull request #12431 from gaby/fix-12237
feat: Allow making content optional when listing all files
2025-04-06 15:13:13 -07:00
Timothy Jaeryang Baek 1bde36e072 Merge pull request #12510 from newnol/patch-1
i18n language pack vietnamese
2025-04-06 15:12:09 -07:00
Juan Calderon-Perez 89e7913ff2 Bump Python base Docker image to 3.12 2025-04-06 18:10:38 -04:00
Timothy Jaeryang Baek 51bf9eaa88 Merge pull request #12493 from TiancongLx/dev
i18n: update zh-TW
2025-04-06 15:09:21 -07:00
Timothy Jaeryang Baek b8cf9b738d Merge pull request #12511 from panda44312/patch-11
i18n: zh-cn
2025-04-06 15:09:13 -07:00
Timothy Jaeryang Baek 635c08a5ab Merge pull request #12517 from Ithanil/only_keep_retrieved_urls
fix: only keep URLs as sources for which the content was actually retrieved
2025-04-06 15:08:47 -07:00
Timothy Jaeryang Baek 6323b9f4f0 Merge pull request #12515 from gaby/fix-dependabot
chore: Fix dependabot configuration
2025-04-06 13:41:18 -07:00
Juan Calderon-Perez 94453839ba Update index.ts 2025-04-06 16:02:16 -04:00
Jan Kessler a506a1a61e only keep URLs as sources for which the content could actually be retrieved 2025-04-06 20:31:12 +02:00
Juan Calderon-Perez d266490862 Fix dependabot configuration 2025-04-06 13:58:17 -04:00
Juan Calderon-Perez ff1d454bdd Fix formatting 2025-04-06 13:32:51 -04:00
Juan Calderon-Perez eda3eba084 Merge branch 'open-webui:main' into fix-12237 2025-04-06 13:30:37 -04:00
Panda 60c6e00d9f i18n: fix 2025-04-06 16:47:06 +02:00
Panda 246c839ee6 i18n: zh-cn 2025-04-06 16:42:09 +02:00
Ngô Tấn Tài 2994c58fd9 Update translation.json 2025-04-06 21:26:58 +07:00
Jan Kessler 4476060044 fix web results all getting the same source id when using embedding and retrieval 2025-04-06 15:51:05 +02:00
Jan Kessler 2729d8aab4 fix web results all getting the same source id when bypassing embedding and retrieval 2025-04-06 13:37:42 +02:00
Tiancong Li 26e1bfc772 Merge remote-tracking branch 'upstream/dev' into dev
* upstream/dev:
  refac
  Fallback from desc to summary to placeholder
2025-04-06 10:33:01 +08:00
Timothy Jaeryang Baek 9ea6cea0ba refac 2025-04-05 14:31:22 -07:00
Timothy Jaeryang Baek 71625663c0 Merge pull request #12496 from dan-sullivan/fix-12479/add-desc-to-openapi-tools
fix: openapi tools should pass description to model not summary
2025-04-05 15:30:19 -06:00
Tiancong Li 33451cf66d i18n: update zh-TW 2025-04-06 04:46:31 +08:00
Timothy Jaeryang Baek 41aca84913 Merge pull request #12491 from open-webui/dev
refac
2025-04-05 10:24:32 -07:00
Timothy Jaeryang Baek 5296ee0f71 refac 2025-04-05 11:24:05 -06:00
Timothy Jaeryang Baek da94835165 Merge pull request #12373 from open-webui/dev
0.6.1
2025-04-05 10:15:32 -07:00
Timothy Jaeryang Baek 13dfca4e76 chore: bump 2025-04-05 11:13:30 -06:00
Timothy Jaeryang Baek d5063f47f2 doc: changelog 2025-04-05 11:11:37 -06:00
Timothy Jaeryang Baek ef787e4a79 Merge pull request #12486 from FabioPolito24/text-file-handling-docling
fix: text file handling with docling
2025-04-05 09:55:51 -07:00
Timothy Jaeryang Baek 48d690c55d Merge pull request #12481 from gaby/generic-errors
fix: Improve auth error messages
2025-04-05 09:47:52 -07:00
Timothy Jaeryang Baek 69c68df34b Merge pull request #12480 from Kylapaallikko/dev
i18n: Update Finnish Translation
2025-04-05 09:47:39 -07:00
Timothy Jaeryang Baek b3110ca291 Merge branch 'dev' into dev 2025-04-05 09:47:30 -07:00
Fabio Polito cd0a1b4852 fix: fix for text file handling with docling 2025-04-05 16:44:08 +00:00
Timothy Jaeryang Baek 32f309b41d chore: format 2025-04-05 10:42:21 -06:00
Timothy Jaeryang Baek d23f757c1f refac 2025-04-05 10:41:46 -06:00
Juan Calderon-Perez 324550423c Fix formatting issues 2025-04-05 10:03:24 -04:00
Juan Calderon-Perez b4277c7d2e Make auth error messages generic 2025-04-05 09:56:11 -04:00
Kylapaallikko 84788b73ce Update translation.json 2025-04-05 16:37:54 +03:00
dan-sullivan 09344bbb80 Fallback from desc to summary to placeholder 2025-04-05 14:18:14 +01:00
Timothy Jaeryang Baek fe4f760b4a refac 2025-04-05 05:57:25 -06:00
Timothy Jaeryang Baek 807b208418 refac 2025-04-05 05:55:56 -06:00
Timothy Jaeryang Baek ae484e8edf refac 2025-04-05 05:45:32 -06:00
Timothy Jaeryang Baek 56dc7c5640 refac 2025-04-05 05:38:46 -06:00
Timothy Jaeryang Baek e570a98bf7 refac: substandard codebase overhauled 2025-04-05 05:31:22 -06:00
Timothy Jaeryang Baek 93bb77ede3 refac 2025-04-05 05:03:15 -06:00
Timothy Jaeryang Baek 66db2e1515 refac: tools removed UNNECESSARY CODE 2025-04-05 04:59:11 -06:00
Timothy Jaeryang Baek c9e9ce931b refac 2025-04-05 04:49:07 -06:00
Timothy Jaeryang Baek 9747a0e1f1 refac: tool servers 2025-04-05 04:40:01 -06:00
Timothy Jaeryang Baek 61778c8f71 Merge pull request #12478 from theredcat/main
Improve FR translation
2025-04-05 03:22:54 -07:00
Timothy Jaeryang Baek 579aca65bb Merge pull request #12477 from Ithanil/improved_rag_template
fix: more coherent default RAG template
2025-04-05 03:22:41 -07:00
Timothy Jaeryang Baek ee44383c83 refac 2025-04-05 04:12:57 -06:00
Timothy Jaeryang Baek 0c0505e1cd refac 2025-04-05 04:05:52 -06:00
Nathan DELHAYE 1b8557ce86 i18n: improve fr translation 2025-04-05 12:04:12 +02:00
Jan Kessler e0da6003f1 tweak default rag template to be more coherent and improve consistency with citations 2025-04-05 11:40:48 +02:00
Timothy Jaeryang Baek 0f310b3509 Merge pull request #12476 from Phlogi/dev-hybrid-search
fix: Avoid secondary error when collection is empty in parallel hybrid search
2025-04-05 02:10:29 -07:00
Phlogi 8cf8121812 Update utils.py
Avoid running any tasks for collections that failed to fetch data (have assigned None)
2025-04-05 10:41:21 +02:00
Timothy Jaeryang Baek a2f2203325 Merge pull request #12426 from silentoplayz/fix-feedback
fix: Conditionally hide export buttons
2025-04-05 01:30:51 -07:00
Timothy Jaeryang Baek 193a927aba fix: temp chat 2025-04-05 02:16:35 -06:00
Timothy Jaeryang Baek 4ad10f0c6e chore: format 2025-04-05 01:31:45 -06:00
Timothy Jaeryang Baek 5e06b6d3f4 refac: styling 2025-04-05 01:24:32 -06:00
Timothy Jaeryang Baek cb94a87cb0 refac: tools styling 2025-04-05 01:05:47 -06:00
Timothy Jaeryang Baek 793aa307ef Merge pull request #12433 from gaby/fix-allowed-endpoints
fix: API Key Endpoint Restrictions for Dynamic Paths
2025-04-04 08:24:10 -07:00
Timothy Jaeryang Baek 7e46cc4562 Merge pull request #12439 from narutopden/i18n-add-Tibetan-translation
i18n: add Tibetan language
2025-04-04 08:20:25 -07:00
Timothy Jaeryang Baek b612af29e9 refac 2025-04-04 09:17:19 -06:00
Timothy Jaeryang Baek feaf434d4e refac: input prompt 2025-04-04 09:11:54 -06:00
Timothy Jaeryang Baek 77fa11098a enh: openapi tool server custom path 2025-04-04 08:31:13 -06:00
Timothy Jaeryang Baek a32bb850fa Merge pull request #12450 from mahenning/fix-missing-parameter-rag
Fix: Add missing parameter for query_doc_with_hybrid_search
2025-04-04 07:00:16 -07:00
Timothy Jaeryang Baek 64b68b3641 Merge pull request #12447 from floriankick/fix-openai-api-empty-message-after-tool-call
fix: make content parameter optional in OpenAI chat completion API endpoint
2025-04-04 06:59:12 -07:00
Marko Henning 3b2b6e183d Added missing parameter for query_doc_with_hybrid_search. 2025-04-04 15:30:57 +02:00
Juan Calderon-Perez 138e985d66 Rename field to include_content 2025-04-04 08:38:24 -04:00
Florian Kick ec3435de00 make content parameter optional in OpenAI chat completion API endpoint 2025-04-04 13:30:51 +02:00
narutopden a25d87607e i18n: add Tibetan language 2025-04-04 14:55:49 +08:00
Juan Calderon-Perez 1c57e3e02c Fix API_KEY_ALLOWED_ENDPOINTS 2025-04-03 23:52:10 -04:00
Silentoplayz 41b9ff9e00 fix: funcs, models, prompts, & tools conditional export button 2025-04-03 23:33:30 -04:00
Juan Calderon-Perez 6d5cb6b04e Add query param to remove content from GET /api/v1/files 2025-04-03 23:02:24 -04:00
silentoplayz 1366822f99 Update Feedbacks.svelte
fix
2025-04-04 02:38:03 +00:00
Silentoplayz 2b0d9c26eb fix: don't show export button if nothing to export 2025-04-03 22:20:40 -04:00
Timothy Jaeryang Baek be20e6dec0 refac: message edit 2025-04-03 13:43:45 -06:00
Timothy Jaeryang Baek a1f3300767 fix: tls cert requirement 2025-04-03 10:01:03 -07:00
Timothy Jaeryang Baek dfdde8bb1f refac 2025-04-03 09:40:26 -07:00
Timothy Jaeryang Baek ba77a72925 refac: styling 2025-04-03 01:00:40 -07:00
Timothy Jaeryang Baek bcf0a8791e refac: styling 2025-04-03 01:00:04 -07:00
Timothy Jaeryang Baek 100f5a57e7 refac 2025-04-02 23:56:15 -07:00
Timothy Jaeryang Baek 911321878f refac 2025-04-02 23:49:42 -07:00
Timothy Jaeryang Baek faa68fcdaa enh: image tool response 2025-04-02 23:46:39 -07:00
Jan Kessler 257ca45456 leave out @ in redis+sentine url when no username/password is provided 2025-04-03 08:24:24 +02:00
Timothy Jaeryang Baek 561b2c0b1a refac: styling 2025-04-02 21:23:45 -07:00
Timothy Jaeryang Baek 436e3ff7de refac 2025-04-02 20:42:23 -07:00
Timothy Jaeryang Baek c0711ba0c9 refac 2025-04-02 20:07:09 -07:00
Timothy Jaeryang Baek 5c5160c294 refac: remove None params 2025-04-02 19:56:39 -07:00
Timothy Jaeryang Baek 2277566ce1 fix: tool server api key not being sent 2025-04-02 19:52:12 -07:00
Timothy Jaeryang Baek 959995c715 refac: use selected model for merge response 2025-04-02 19:33:20 -07:00
Timothy Jaeryang Baek 7a1e10f3a7 refac: rm OAUTH_USE_PICTURE_CLAIM 2025-04-02 19:23:24 -07:00
Timothy Jaeryang Baek b15bf0dc70 refac 2025-04-02 19:17:26 -07:00
Timothy Jaeryang Baek 506950b03a Merge pull request #12376 from MushroomLamp-COB/main
feat: Add option to disable OAuth profile pictures
2025-04-02 19:16:36 -07:00
Timothy Jaeryang Baek 94353458a5 refac: settings ui styling 2025-04-02 19:01:39 -07:00
Timothy Jaeryang Baek 7eea95a48c feat: direct tools user permissions 2025-04-02 18:36:03 -07:00
Timothy Jaeryang Baek 94bf49440d enh: unload hybrid model if set to False 2025-04-02 18:15:14 -07:00
Timothy Jaeryang Baek 0644abe402 fix: admin folder deletion issue 2025-04-02 17:55:24 -07:00
Timothy Jaeryang Baek 9036945361 Merge pull request #12355 from silentoplayz/logging-fix
fix: update datetime and logging imports
2025-04-02 17:44:30 -07:00
silentoplayz dd5bafe8af Update env.py
Revert
2025-04-03 00:36:16 +00:00
CityOfBunbury 548c7f17d7 Added OAUTH_USE_PICTURE_CLAIM env var
Added OAUTH_USE_PICTURE_CLAIM to config.py

Added check to oauth.py on OAUTH_USE_PICTURE_CLAIM, to decide whether to user the profile picture in the claim or the default user.png
2025-04-03 08:24:14 +08:00
Timothy Jaeryang Baek bb7f4d408a Merge pull request #12364 from silentoplayz/archive-chats-option
feat: Added Archived Chats option in Chats settings section
2025-04-02 17:12:34 -07:00
Timothy Jaeryang Baek 6ee30042e7 Merge pull request #12353 from SadmL/dev
[i18n] Russian localization update
2025-04-02 15:47:44 -07:00
Timothy Jaeryang Baek 0554bbb1cb Merge pull request #12307 from paddy313/feature/mistral_ocr
feat: Added support for Mistral OCR for Content Extraction
2025-04-02 15:47:33 -07:00
Timothy Jaeryang Baek 517a57b7ca Merge pull request #12331 from panda44312/patch-10
i18n: zh-cn fix
2025-04-02 15:46:42 -07:00
Jan Kessler 35ea29b184 prepare websocket redis sentinel code for upcoming native support of sentinel in python-socketio 2025-04-02 21:50:00 +02:00
Silentoplayz ee68c9ea55 Update Chats.svelte
Added Archived Chats option in Chats settings section
2025-04-02 14:25:03 -04:00
Silentoplayz d65471c420 fix
my dev environment works again!
2025-04-02 11:28:45 -04:00
SadmL de8f94b008 [i18n] Russian localization update
Fixed typos and translated some lines
2025-04-02 18:13:52 +03:00
Patrick Wachter 0ac00b9256 refactor: update import path for MistralLoader 2025-04-02 13:56:10 +02:00
Timothy Jaeryang Baek d0db4756a6 Merge pull request #12310 from landerrosette/fix_filter_priority
fix: Filter function’s priority valve has no effect
2025-04-02 04:41:00 -07:00
Panda adaa614986 fix 2025-04-02 10:35:05 +02:00
Timothy Jaeryang Baek 5ab783001d Merge pull request #12304 from OriginalSimon/main
18n: Update Ukrainian translation
2025-04-01 16:12:59 -07:00
Timothy Jaeryang Baek 2a163996f9 Merge pull request #12294 from Xelaph/dev
**i18n**: Update Dutch translations
2025-04-01 16:12:48 -07:00
Timothy Jaeryang Baek 1b37bdcec8 Merge pull request #12278 from Saidoua/main
**i18n** Standard Arabic Translation
2025-04-01 16:11:54 -07:00
Timothy Jaeryang Baek 39b0c06630 Merge pull request #12299 from lowlyocean/rpi_onnxruntime
fix: Allow v0.6.0 to run on Raspberry Pi
2025-04-01 16:02:05 -07:00
landerrosette 2b7dd6e399 refactor: standardize filter valve retrieval logic 2025-04-02 02:58:02 +08:00
Patrick Wachter c5a8d2f857 refactor: update MistralLoader documentation and adjust parameters for signed URL retrieval 2025-04-01 20:14:34 +02:00
Patrick Wachter 93d7702e8c refactor: move MistralLoader to a separate module and just use the requests package instead of mistralai 2025-04-01 20:14:34 +02:00
Simon 0447d90951 Update translation.json 2025-04-01 20:01:08 +02:00
Daniel Nowak 8f8c34471e Pin onnxruntime to 1.20.1 to address SIGILL on certain arm64 hosts 2025-04-01 12:03:09 -04:00
Alex 883ad55f5c Update translation.json 2025-04-01 16:12:31 +02:00
Alex 82e5a6410c Update translation.json 2025-04-01 16:09:17 +02:00
Alex 88b93241db Update translation.json 2025-04-01 15:20:04 +02:00
Alex b652b8e5f3 Update translation.json 2025-04-01 14:25:04 +02:00
Patrick Wachter 1ac6879268 Add Mistral OCR integration and configuration support 2025-04-01 14:24:33 +02:00
Said Ouhdachi 825becceb0 Arabic Translation 2025-04-01 13:10:59 +02:00
Timothy Jaeryang Baek fffa793d6d Merge pull request #12270 from TiancongLx/dev
i18n: update zh-TW
2025-04-01 02:29:00 -07:00
Timothy Jaeryang Baek 289d49dbe7 Merge pull request #12269 from panda44312/patch-9
i18n: zh-cn
2025-04-01 02:28:51 -07:00
Tiancong Li fa72c277db i18n: update zh-TW 2025-04-01 17:10:32 +08:00
Panda 8799ff9575 fix 2025-04-01 10:52:05 +02:00
Panda 85c8a9bd68 i18n: zh-cn 2025-04-01 10:47:52 +02:00
Timothy Jaeryang Baek 15f6e3af12 Merge pull request #12259 from weisser-dev/dev
[FEAT] adjust Translations for temporary chat
2025-04-01 00:59:27 -07:00
Timothy Jaeryang Baek c79fa4214f Merge pull request #12262 from lukasz-pekala/fb/fixes-to-polish-translation
i18n: fixes to polish translation
2025-04-01 00:59:08 -07:00
Łukasz Pękala 29d5745335 fix: update Polish translation for "Capture" to improve accuracy 2025-04-01 09:14:29 +02:00
Erik 70bb0562f3 Merge branch 'open-webui:dev' into dev 2025-04-01 09:09:33 +02:00
Erik c8210d4043 upated like in PR discussed 2025-04-01 09:06:15 +02:00
Łukasz Pękala 9f470a4ff5 fix: update Polish translations to sound more natural 2025-04-01 08:53:12 +02:00
Timothy Jaeryang Baek cea5c1a63b Merge pull request #12254 from open-webui/dependabot/pip/backend/dev/einops-0.8.1
build(deps): bump einops from 0.8.0 to 0.8.1 in /backend
2025-03-31 23:45:23 -07:00
Timothy Jaeryang Baek 4ce05ad279 Merge pull request #12255 from open-webui/dependabot/pip/backend/dev/pypandoc-1.15
build(deps): bump pypandoc from 1.13 to 1.15 in /backend
2025-03-31 23:45:15 -07:00
Timothy Jaeryang Baek f3c2614b6c Merge pull request #12253 from open-webui/dependabot/pip/backend/dev/python-multipart-0.0.20
build(deps): bump python-multipart from 0.0.18 to 0.0.20 in /backend
2025-03-31 23:44:40 -07:00
Timothy Jaeryang Baek 79cca06e0c Merge pull request #12260 from weisser-dev/feat-default-value-for-enforce-temp-chat
[improvement] default permission for new groups is false for enforce …
2025-03-31 23:43:08 -07:00
Łukasz Pękala b60beb6ff6 fix: improve Polish translations for clarity and accuracy 2025-04-01 08:39:36 +02:00
Erik 295c7eb4c7 [improvement] default permission for new groups is false for enforce temp chat 2025-04-01 08:27:29 +02:00
Łukasz Pękala 361584b4fd fix: update Polish translations for clarity and accuracy 2025-04-01 08:26:21 +02:00
Erik abba7c128c [FEAT]-Adjust Translations for temporary chat 2025-04-01 08:21:03 +02:00
Timothy Jaeryang Baek e0ec2cdeb0 refac: $user 2025-03-31 20:32:12 -07:00
dependabot[bot] bb0e11bb97 build(deps): bump pypandoc from 1.13 to 1.15 in /backend
Bumps [pypandoc](https://github.com/JessicaTegner/pypandoc) from 1.13 to 1.15.
- [Release notes](https://github.com/JessicaTegner/pypandoc/releases)
- [Changelog](https://github.com/JessicaTegner/pypandoc/blob/master/release.md)
- [Commits](https://github.com/JessicaTegner/pypandoc/compare/v1.13...v1.15)

---
updated-dependencies:
- dependency-name: pypandoc
  dependency-version: '1.15'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-01 02:22:16 +00:00
dependabot[bot] fc5c8dbe3f build(deps): bump einops from 0.8.0 to 0.8.1 in /backend
Bumps [einops](https://github.com/arogozhnikov/einops) from 0.8.0 to 0.8.1.
- [Release notes](https://github.com/arogozhnikov/einops/releases)
- [Commits](https://github.com/arogozhnikov/einops/compare/v0.8.0...v0.8.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-01 02:22:13 +00:00
dependabot[bot] 1d30d8a13a build(deps): bump python-multipart from 0.0.18 to 0.0.20 in /backend
Bumps [python-multipart](https://github.com/Kludex/python-multipart) from 0.0.18 to 0.0.20.
- [Release notes](https://github.com/Kludex/python-multipart/releases)
- [Changelog](https://github.com/Kludex/python-multipart/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Kludex/python-multipart/compare/0.0.18...0.0.20)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-01 02:22:11 +00:00
190 changed files with 12474 additions and 5077 deletions
+14
View File
@@ -1,12 +1,26 @@
version: 2
updates:
- package-ecosystem: uv
directory: '/'
schedule:
interval: monthly
target-branch: 'dev'
- package-ecosystem: pip
directory: '/backend'
schedule:
interval: monthly
target-branch: 'dev'
- package-ecosystem: npm
directory: '/'
schedule:
interval: monthly
target-branch: 'dev'
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
# Check for updates to GitHub Actions every week
interval: monthly
target-branch: 'dev'
+12 -2
View File
@@ -5,10 +5,18 @@ on:
branches:
- main
- dev
paths:
- 'backend/**'
- 'pyproject.toml'
- 'uv.lock'
pull_request:
branches:
- main
- dev
paths:
- 'backend/**'
- 'pyproject.toml'
- 'uv.lock'
jobs:
build:
@@ -17,7 +25,9 @@ jobs:
strategy:
matrix:
python-version: [3.11]
python-version:
- 3.11.x
- 3.12.x
steps:
- uses: actions/checkout@v4
@@ -25,7 +35,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
python-version: '${{ matrix.python-version }}'
- name: Install dependencies
run: |
+9 -1
View File
@@ -5,10 +5,18 @@ on:
branches:
- main
- dev
paths-ignore:
- 'backend/**'
- 'pyproject.toml'
- 'uv.lock'
pull_request:
branches:
- main
- dev
paths-ignore:
- 'backend/**'
- 'pyproject.toml'
- 'uv.lock'
jobs:
build:
@@ -21,7 +29,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22' # Or specify any other version you want to use
node-version: '22'
- name: Install Dependencies
run: npm install
+86
View File
@@ -5,6 +5,92 @@ 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.6.4] - 2025-04-12
### Fixed
- 🛠️ **RAG_TEMPLATE Display Issue Resolved**: Fixed a formatting problem where the custom RAG_TEMPLATE wasn't correctly rendered in the interface—ensuring that custom retrieval prompts now appear exactly as intended for more reliable prompt engineering.
## [0.6.3] - 2025-04-12
### Added
- 🧪 **Auto-Artifact Detection Toggle**: Automatically detects artifacts in results—but now you can disable this behavior under advanced settings for full control.
- 🖼️ **Widescreen Mode for Shared Chats**: Shared link conversations now support widescreen layouts—perfect for presentations or easier review across wider displays.
- 🔁 **Reindex Knowledge Files on Demand**: Admins can now trigger reindexing of all knowledge files after changing embeddings—ensuring immediate alignment with new models for optimal RAG performance.
- 📄 **OpenAPI YAML Format Support**: External tools can now use YAML-format OpenAPI specs—making integration simpler for developers familiar with YAML-based configurations.
- 💬 **Message Content Copy Behavior**: Copy action now excludes 'details' tags—streamlining clipboard content when sharing or pasting summaries elsewhere.
- 🧭 **Sougou Web Search Integration**: New search engine option added—enhancing global relevance and diversity of search sources for multilingual users.
- 🧰 **Frontend Web Loader Engine Configuration**: Admins can now set preferred web loader engine for RAG workflows directly from the frontend—offering more control across setups.
- 👥 **Multi-Model Chat Permission Control**: Admins can manage access to multi-model chats per user group—allowing tighter governance in team environments.
- 🧱 **Persistent Configuration Can Be Disabled**: New environment variable lets advanced users and hosts turn off persistent configs—ideal for volatile or stateless deployments.
- 🧠 **Elixir Code Highlighting Support**: Elixir syntax is now beautifully rendered in code blocks—perfect for developers using this language in AI or automation projects.
- 🌐 **PWA External Manifest URL Support**: You can now define an external manifest.json—integrate Open WebUI seamlessly in managed or proxy-based PWA environments like Cloudflare Zero Trust.
- 🧪 **Azure AI Speech-to-Text Provider Integration**: Easily transcribe large audio files (up to 200MB) with high accuracy using Microsoft's Azure STT—fully configurable in Audio Settings.
- 🔏 **PKCE (Code Challenge Method) Support for OIDC**: Enhance your OIDC login security with Proof Key for Code Exchange—ideal for zero-trust and native client apps.
- ✨ **General UI/UX Enhancements**: Numerous refinements across layout, styling, and tool interactions—reducing visual noise and improving overall usability across key workflows.
- 🌍 **Translation Updates Across Multiple Languages**: Refined Catalan, Russian, Chinese (Simplified & Traditional), Hungarian, and Spanish translations for clearer navigation and instructions globally.
### Fixed
- 💥 **Chat Completion Error with Missing Models Resolved**: Fixed internal server error when referencing a model that doesn’t exist—ensuring graceful fallback and clear error guidance.
- 🔧 **Correct Knowledge Base Citations Restored**: Citations generated by RAG workflows now show accurate references—ensuring verifiability in outputs from sourced content.
- 🎙️ **Broken OGG/WebM Audio Upload Handling for OpenAI Fixed**: Uploading OGG or WebM files now converts properly to WAV before transcription—restoring accurate AI speech recognition workflows.
- 🔐 **Tool Server 'Session' Authentication Restored**: Previously broken session auth on external tool servers is now fully functional—ensuring secure and seamless access to connected tools.
- 🌐 **Folder-Based Chat Rename Now Updates Correctly**: Renaming chats in folders now reflects instantly everywhere—improving chat organization and clarity.
- 📜 **KaTeX Overflow Displays Fixed**: Math expressions now stay neatly within message bounds—preserving layout consistency even with long formulas.
- 🚫 **Stopping Ongoing Chat Fixed**: You can now return to an active (ongoing) chat and stop generation at any time—ensuring full control over sessions.
- 🔧 **TOOL_SERVERS / TOOL_SERVER_CONNECTIONS Indexing Issue Fixed**: Fixed a mismatch between tool lists and their access paths—restoring full function and preventing confusion in tool management.
- 🔐 **LDAP Login Handles Multiple Emails**: When LDAP returns multiple email attributes, the first valid one is now used—ensuring login success and account consistency.
- 🧩 **Model Visibility Toggle Fix**: Toggling model visibility now works even for untouched models—letting admins smoothly manage user access across base models.
- ⚙️ **Cross-Origin manifest.json Now Loads Properly**: Compatibility issues with Cloudflare Zero Trust (and others) resolved, allowing manifest.json to load behind authenticated proxies.
### Changed
- 🔒 **Default Access Scopes Set to Private for All Resources**: Models, tools, and knowledge are now private by default when created—ensuring better baseline security and visibility controls.
- 🧱 **General Backend Refactoring for Stability**: Numerous invisible improvements enhance backend scalability, security, and maintainability—powering upcoming features with a stronger foundation.
- 🧩 **Stable Dependency Upgrades**: Updated key platform libraries—Chromadb (0.6.3), pgvector (0.4.0), Azure Identity (1.21.0), and Youtube Transcript API (1.0.3)—for improved compatibility, functionality, and security.
## [0.6.2] - 2025-04-06
### Added
- 🌍 **Improved Global Language Support**: Expanded and refined translations across multiple languages to enhance clarity and consistency for international users.
### Fixed
- 🛠️ **Accurate Tool Descriptions from OpenAPI Servers**: External tools now use full endpoint descriptions instead of summaries when generating tool specifications—helping AI models understand tool purpose more precisely and choose the right tool more accurately in tool workflows.
- 🔧 **Precise Web Results Source Attribution**: Fixed a key issue where all web search results showed the same source ID—now each result gets its correct and distinct source, ensuring accurate citations and traceability.
- 🔍 **Clean Web Search Retrieval**: Web search now retains only results from URLs where real content was successfully fetched—improving accuracy and removing empty or broken links from citations.
- 🎵 **Audio File Upload Response Restored**: Resolved an issue where uploading audio files did not return valid responses, restoring smooth file handling for transcription and audio-based workflows.
### Changed
- 🧰 **General Backend Refactoring**: Multiple behind-the-scenes improvements streamline backend performance, reduce complexity, and ensure a more stable, maintainable system overall—making everything smoother without changing your workflow.
## [0.6.1] - 2025-04-05
### Added
- 🛠️ **Global Tool Servers Configuration**: Admins can now centrally configure global external tool servers from Admin Settings > Tools, allowing seamless sharing of tool integrations across all users without manual setup per user.
- 🔐 **Direct Tool Usage Permission for Users**: Introduced a new user-level permission toggle that grants non-admin users access to direct external tools, empowering broader team collaboration while maintaining control.
- 🧠 **Mistral OCR Content Extraction Support**: Added native support for Mistral OCR as a high-accuracy document loader, drastically improving text extraction from scanned documents in RAG workflows.
- 🖼️ **Tools Indicator UI Redesign**: Enhanced message input now smartly displays both built-in and external tools via a unified dropdown, making it simpler and more intuitive to activate tools during conversations.
- 📄 **RAG Prompt Improved and More Coherent**: Default RAG system prompt has been revised to be more clear and citation-focused—admins can leave the template field empty to use this new gold-standard prompt.
- 🧰 **Performance & Developer Improvements**: Major internal restructuring of several tool-related components, simplifying styling and merging external/internal handling logic, resulting in better maintainability and performance.
- 🌍 **Improved Translations**: Updated translations for Tibetan, Polish, Chinese (Simplified & Traditional), Arabic, Russian, Ukrainian, Dutch, Finnish, and French to improve clarity and consistency across the interface.
### Fixed
- 🔑 **External Tool Server API Key Bug Resolved**: Fixed a critical issue where authentication headers were not being sent when calling tools from external OpenAPI tool servers, ensuring full security and smooth tool operations.
- 🚫 **Conditional Export Button Visibility**: UI now gracefully hides export buttons when there's nothing to export in models, prompts, tools, or functions, improving visual clarity and reducing confusion.
- 🧪 **Hybrid Search Failure Recovery**: Resolved edge case in parallel hybrid search where empty or unindexed collections caused backend crashes—these are now cleanly skipped to ensure system stability.
- 📂 **Admin Folder Deletion Fix**: Addressed an issue where folders created in the admin workspace couldn't be deleted, restoring full organizational flexibility for admins.
- 🔐 **Improved Generic Error Feedback on Login**: Authentication errors now show simplified, non-revealing messages for privacy and improved UX, especially with federated logins.
- 📝 **Tool Message with Images Improved**: Enhanced how tool-generated messages with image outputs are shown in chat, making them more readable and consistent with the overall UI design.
- ⚙️ **Auto-Exclusion for Broken RAG Collections**: Auto-skips document collections that fail to fetch data or return "None", preventing silent errors and streamlining retrieval workflows.
- 📝 **Docling Text File Handling Fix**: Fixed file parsing inconsistency that broke docling-based RAG functionality for certain plain text files, ensuring wider file compatibility.
## [0.6.0] - 2025-03-31
### Added
+8 -1
View File
@@ -73,8 +73,15 @@ def serve(
os.environ["LD_LIBRARY_PATH"] = ":".join(LD_LIBRARY_PATH)
import open_webui.main # we need set environment variables before importing main
from open_webui.env import UVICORN_WORKERS # Import the workers setting
uvicorn.run(open_webui.main.app, host=host, port=port, forwarded_allow_ips="*")
uvicorn.run(
open_webui.main.app,
host=host,
port=port,
forwarded_allow_ips="*",
workers=UVICORN_WORKERS,
)
@app.command()
+152 -62
View File
@@ -201,6 +201,10 @@ def save_config(config):
T = TypeVar("T")
ENABLE_PERSISTENT_CONFIG = (
os.environ.get("ENABLE_PERSISTENT_CONFIG", "True").lower() == "true"
)
class PersistentConfig(Generic[T]):
def __init__(self, env_name: str, config_path: str, env_value: T):
@@ -208,7 +212,7 @@ class PersistentConfig(Generic[T]):
self.config_path = config_path
self.env_value = env_value
self.config_value = get_config_value(config_path)
if self.config_value is not None:
if self.config_value is not None and ENABLE_PERSISTENT_CONFIG:
log.info(f"'{env_name}' loaded from the latest database entry")
self.value = self.config_value
else:
@@ -331,12 +335,14 @@ JWT_EXPIRES_IN = PersistentConfig(
# 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",
@@ -454,6 +460,12 @@ OAUTH_SCOPES = PersistentConfig(
os.environ.get("OAUTH_SCOPES", "openid email profile"),
)
OAUTH_CODE_CHALLENGE_METHOD = PersistentConfig(
"OAUTH_CODE_CHALLENGE_METHOD",
"oauth.oidc.code_challenge_method",
os.environ.get("OAUTH_CODE_CHALLENGE_METHOD", None),
)
OAUTH_PROVIDER_NAME = PersistentConfig(
"OAUTH_PROVIDER_NAME",
"oauth.oidc.provider_name",
@@ -466,6 +478,7 @@ OAUTH_USERNAME_CLAIM = PersistentConfig(
os.environ.get("OAUTH_USERNAME_CLAIM", "name"),
)
OAUTH_PICTURE_CLAIM = PersistentConfig(
"OAUTH_PICTURE_CLAIM",
"oauth.oidc.avatar_claim",
@@ -557,7 +570,7 @@ def load_oauth_providers():
name="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",
server_metadata_url=f"https://login.microsoftonline.com/{MICROSOFT_CLIENT_TENANT_ID.value}/v2.0/.well-known/openid-configuration?appid={MICROSOFT_CLIENT_ID.value}",
client_kwargs={
"scope": MICROSOFT_OAUTH_SCOPE.value,
},
@@ -598,14 +611,27 @@ def load_oauth_providers():
):
def oidc_oauth_register(client):
client_kwargs = {
"scope": OAUTH_SCOPES.value,
}
if (
OAUTH_CODE_CHALLENGE_METHOD.value
and OAUTH_CODE_CHALLENGE_METHOD.value == "S256"
):
client_kwargs["code_challenge_method"] = "S256"
elif OAUTH_CODE_CHALLENGE_METHOD.value:
raise Exception(
'Code challenge methods other than "%s" not supported. Given: "%s"'
% ("S256", OAUTH_CODE_CHALLENGE_METHOD.value)
)
client.register(
name="oidc",
client_id=OAUTH_CLIENT_ID.value,
client_secret=OAUTH_CLIENT_SECRET.value,
server_metadata_url=OPENID_PROVIDER_URL.value,
client_kwargs={
"scope": OAUTH_SCOPES.value,
},
client_kwargs=client_kwargs,
redirect_uri=OPENID_REDIRECT_URI.value,
)
@@ -878,6 +904,17 @@ except Exception:
pass
OPENAI_API_BASE_URL = "https://api.openai.com/v1"
####################################
# TOOL_SERVERS
####################################
TOOL_SERVER_CONNECTIONS = PersistentConfig(
"TOOL_SERVER_CONNECTIONS",
"tool_server.connections",
[],
)
####################################
# WEBUI
####################################
@@ -1025,6 +1062,10 @@ USER_PERMISSIONS_CHAT_EDIT = (
os.environ.get("USER_PERMISSIONS_CHAT_EDIT", "True").lower() == "true"
)
USER_PERMISSIONS_CHAT_MULTIPLE_MODELS = (
os.environ.get("USER_PERMISSIONS_CHAT_MULTIPLE_MODELS", "True").lower() == "true"
)
USER_PERMISSIONS_CHAT_TEMPORARY = (
os.environ.get("USER_PERMISSIONS_CHAT_TEMPORARY", "True").lower() == "true"
)
@@ -1034,6 +1075,12 @@ USER_PERMISSIONS_CHAT_TEMPORARY_ENFORCED = (
== "true"
)
USER_PERMISSIONS_FEATURES_DIRECT_TOOL_SERVERS = (
os.environ.get("USER_PERMISSIONS_FEATURES_DIRECT_TOOL_SERVERS", "False").lower()
== "true"
)
USER_PERMISSIONS_FEATURES_WEB_SEARCH = (
os.environ.get("USER_PERMISSIONS_FEATURES_WEB_SEARCH", "True").lower() == "true"
)
@@ -1067,10 +1114,12 @@ DEFAULT_USER_PERMISSIONS = {
"file_upload": USER_PERMISSIONS_CHAT_FILE_UPLOAD,
"delete": USER_PERMISSIONS_CHAT_DELETE,
"edit": USER_PERMISSIONS_CHAT_EDIT,
"multiple_models": USER_PERMISSIONS_CHAT_MULTIPLE_MODELS,
"temporary": USER_PERMISSIONS_CHAT_TEMPORARY,
"temporary_enforced": USER_PERMISSIONS_CHAT_TEMPORARY_ENFORCED,
},
"features": {
"direct_tool_servers": USER_PERMISSIONS_FEATURES_DIRECT_TOOL_SERVERS,
"web_search": USER_PERMISSIONS_FEATURES_WEB_SEARCH,
"image_generation": USER_PERMISSIONS_FEATURES_IMAGE_GENERATION,
"code_interpreter": USER_PERMISSIONS_FEATURES_CODE_INTERPRETER,
@@ -1727,6 +1776,11 @@ DOCUMENT_INTELLIGENCE_KEY = PersistentConfig(
os.getenv("DOCUMENT_INTELLIGENCE_KEY", ""),
)
MISTRAL_OCR_API_KEY = PersistentConfig(
"MISTRAL_OCR_API_KEY",
"rag.mistral_ocr_api_key",
os.getenv("MISTRAL_OCR_API_KEY", ""),
)
BYPASS_EMBEDDING_AND_RETRIEVAL = PersistentConfig(
"BYPASS_EMBEDDING_AND_RETRIEVAL",
@@ -1781,12 +1835,6 @@ RAG_FILE_MAX_SIZE = PersistentConfig(
),
)
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = PersistentConfig(
"ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION",
"rag.enable_web_loader_ssl_verification",
os.environ.get("ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION", "True").lower() == "true",
)
RAG_EMBEDDING_ENGINE = PersistentConfig(
"RAG_EMBEDDING_ENGINE",
"rag.embedding_engine",
@@ -1875,7 +1923,7 @@ CHUNK_OVERLAP = PersistentConfig(
)
DEFAULT_RAG_TEMPLATE = """### Task:
Respond to the user query using the provided context, incorporating inline citations in the format [source_id] **only when the <source_id> tag is explicitly provided** in the context.
Respond to the user query using the provided context, incorporating inline citations in the format [id] **only when the <source> tag includes an explicit id attribute** (e.g., <source id="1">).
### Guidelines:
- If you don't know the answer, clearly state that.
@@ -1883,18 +1931,17 @@ Respond to the user query using the provided context, incorporating inline citat
- Respond in the same language as the user's query.
- If the context is unreadable or of poor quality, inform the user and provide the best possible answer.
- If the answer isn't present in the context but you possess the knowledge, explain this to the user and provide the answer using your own understanding.
- **Only include inline citations using [source_id] (e.g., [1], [2]) when a `<source_id>` tag is explicitly provided in the context.**
- Do not cite if the <source_id> tag is not provided in the context.
- **Only include inline citations using [id] (e.g., [1], [2]) when the <source> tag includes an id attribute.**
- Do not cite if the <source> tag does not contain an id attribute.
- Do not use XML tags in your response.
- Ensure citations are concise and directly related to the information provided.
### Example of Citation:
If the user asks about a specific topic and the information is found in "whitepaper.pdf" with a provided <source_id>, the response should include the citation like so:
* "According to the study, the proposed method increases efficiency by 20% [whitepaper.pdf]."
If no <source_id> is present, the response should omit the citation.
If the user asks about a specific topic and the information is found in a source with a provided id attribute, the response should include the citation like in the following example:
* "According to the study, the proposed method increases efficiency by 20% [1]."
### Output:
Provide a clear and direct response to the user's query, including inline citations in the format [source_id] only when the <source_id> tag is present in the context.
Provide a clear and direct response to the user's query, including inline citations in the format [id] only when the <source> tag with id attribute is present in the context.
<context>
{{CONTEXT}}
@@ -1952,16 +1999,20 @@ YOUTUBE_LOADER_PROXY_URL = PersistentConfig(
)
ENABLE_RAG_WEB_SEARCH = PersistentConfig(
"ENABLE_RAG_WEB_SEARCH",
####################################
# Web Search (RAG)
####################################
ENABLE_WEB_SEARCH = PersistentConfig(
"ENABLE_WEB_SEARCH",
"rag.web.search.enable",
os.getenv("ENABLE_RAG_WEB_SEARCH", "False").lower() == "true",
os.getenv("ENABLE_WEB_SEARCH", "False").lower() == "true",
)
RAG_WEB_SEARCH_ENGINE = PersistentConfig(
"RAG_WEB_SEARCH_ENGINE",
WEB_SEARCH_ENGINE = PersistentConfig(
"WEB_SEARCH_ENGINE",
"rag.web.search.engine",
os.getenv("RAG_WEB_SEARCH_ENGINE", ""),
os.getenv("WEB_SEARCH_ENGINE", ""),
)
BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = PersistentConfig(
@@ -1970,10 +2021,18 @@ BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = PersistentConfig(
os.getenv("BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL", "False").lower() == "true",
)
WEB_SEARCH_RESULT_COUNT = PersistentConfig(
"WEB_SEARCH_RESULT_COUNT",
"rag.web.search.result_count",
int(os.getenv("WEB_SEARCH_RESULT_COUNT", "3")),
)
# 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",
WEB_SEARCH_DOMAIN_FILTER_LIST = PersistentConfig(
"WEB_SEARCH_DOMAIN_FILTER_LIST",
"rag.web.search.domain.filter_list",
[
# "wikipedia.com",
@@ -1982,6 +2041,30 @@ RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = PersistentConfig(
],
)
WEB_SEARCH_CONCURRENT_REQUESTS = PersistentConfig(
"WEB_SEARCH_CONCURRENT_REQUESTS",
"rag.web.search.concurrent_requests",
int(os.getenv("WEB_SEARCH_CONCURRENT_REQUESTS", "10")),
)
WEB_LOADER_ENGINE = PersistentConfig(
"WEB_LOADER_ENGINE",
"rag.web.loader.engine",
os.environ.get("WEB_LOADER_ENGINE", ""),
)
ENABLE_WEB_LOADER_SSL_VERIFICATION = PersistentConfig(
"ENABLE_WEB_LOADER_SSL_VERIFICATION",
"rag.web.loader.ssl_verification",
os.environ.get("ENABLE_WEB_LOADER_SSL_VERIFICATION", "True").lower() == "true",
)
WEB_SEARCH_TRUST_ENV = PersistentConfig(
"WEB_SEARCH_TRUST_ENV",
"rag.web.search.trust_env",
os.getenv("WEB_SEARCH_TRUST_ENV", "False").lower() == "true",
)
SEARXNG_QUERY_URL = PersistentConfig(
"SEARXNG_QUERY_URL",
@@ -2049,18 +2132,6 @@ SERPLY_API_KEY = PersistentConfig(
os.getenv("SERPLY_API_KEY", ""),
)
TAVILY_API_KEY = PersistentConfig(
"TAVILY_API_KEY",
"rag.web.search.tavily_api_key",
os.getenv("TAVILY_API_KEY", ""),
)
TAVILY_EXTRACT_DEPTH = PersistentConfig(
"TAVILY_EXTRACT_DEPTH",
"rag.web.search.tavily_extract_depth",
os.getenv("TAVILY_EXTRACT_DEPTH", "basic"),
)
JINA_API_KEY = PersistentConfig(
"JINA_API_KEY",
"rag.web.search.jina_api_key",
@@ -2117,54 +2188,55 @@ PERPLEXITY_API_KEY = PersistentConfig(
os.getenv("PERPLEXITY_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")),
SOUGOU_API_SID = PersistentConfig(
"SOUGOU_API_SID",
"rag.web.search.sougou_api_sid",
os.getenv("SOUGOU_API_SID", ""),
)
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")),
SOUGOU_API_SK = PersistentConfig(
"SOUGOU_API_SK",
"rag.web.search.sougou_api_sk",
os.getenv("SOUGOU_API_SK", ""),
)
RAG_WEB_LOADER_ENGINE = PersistentConfig(
"RAG_WEB_LOADER_ENGINE",
"rag.web.loader.engine",
os.environ.get("RAG_WEB_LOADER_ENGINE", "safe_web"),
TAVILY_API_KEY = PersistentConfig(
"TAVILY_API_KEY",
"rag.web.search.tavily_api_key",
os.getenv("TAVILY_API_KEY", ""),
)
RAG_WEB_SEARCH_TRUST_ENV = PersistentConfig(
"RAG_WEB_SEARCH_TRUST_ENV",
"rag.web.search.trust_env",
os.getenv("RAG_WEB_SEARCH_TRUST_ENV", "False").lower() == "true",
TAVILY_EXTRACT_DEPTH = PersistentConfig(
"TAVILY_EXTRACT_DEPTH",
"rag.web.search.tavily_extract_depth",
os.getenv("TAVILY_EXTRACT_DEPTH", "basic"),
)
PLAYWRIGHT_WS_URI = PersistentConfig(
"PLAYWRIGHT_WS_URI",
"rag.web.loader.engine.playwright.ws.uri",
os.environ.get("PLAYWRIGHT_WS_URI", None),
PLAYWRIGHT_WS_URL = PersistentConfig(
"PLAYWRIGHT_WS_URL",
"rag.web.loader.playwright_ws_url",
os.environ.get("PLAYWRIGHT_WS_URL", ""),
)
PLAYWRIGHT_TIMEOUT = PersistentConfig(
"PLAYWRIGHT_TIMEOUT",
"rag.web.loader.engine.playwright.timeout",
int(os.environ.get("PLAYWRIGHT_TIMEOUT", "10")),
"rag.web.loader.playwright_timeout",
int(os.environ.get("PLAYWRIGHT_TIMEOUT", "10000")),
)
FIRECRAWL_API_KEY = PersistentConfig(
"FIRECRAWL_API_KEY",
"firecrawl.api_key",
"rag.web.loader.firecrawl_api_key",
os.environ.get("FIRECRAWL_API_KEY", ""),
)
FIRECRAWL_API_BASE_URL = PersistentConfig(
"FIRECRAWL_API_BASE_URL",
"firecrawl.api_url",
"rag.web.loader.firecrawl_api_url",
os.environ.get("FIRECRAWL_API_BASE_URL", "https://api.firecrawl.dev"),
)
####################################
# Images
####################################
@@ -2448,6 +2520,24 @@ AUDIO_STT_MODEL = PersistentConfig(
os.getenv("AUDIO_STT_MODEL", ""),
)
AUDIO_STT_AZURE_API_KEY = PersistentConfig(
"AUDIO_STT_AZURE_API_KEY",
"audio.stt.azure.api_key",
os.getenv("AUDIO_STT_AZURE_API_KEY", ""),
)
AUDIO_STT_AZURE_REGION = PersistentConfig(
"AUDIO_STT_AZURE_REGION",
"audio.stt.azure.region",
os.getenv("AUDIO_STT_AZURE_REGION", ""),
)
AUDIO_STT_AZURE_LOCALES = PersistentConfig(
"AUDIO_STT_AZURE_LOCALES",
"audio.stt.azure.locales",
os.getenv("AUDIO_STT_AZURE_LOCALES", ""),
)
AUDIO_TTS_OPENAI_API_BASE_URL = PersistentConfig(
"AUDIO_TTS_OPENAI_API_BASE_URL",
"audio.tts.openai.api_base_url",
+1
View File
@@ -31,6 +31,7 @@ class ERROR_MESSAGES(str, Enum):
USERNAME_TAKEN = (
"Uh-oh! This username is already registered. Please choose another username."
)
PASSWORD_TOO_LONG = "Uh-oh! The password you entered is too long. Please make sure your password is less than 72 bytes long."
COMMAND_TAKEN = "Uh-oh! This command is already registered. Please choose another command string."
FILE_EXISTS = "Uh-oh! This file is already registered. Please choose another file."
+36
View File
@@ -326,6 +326,20 @@ REDIS_URL = os.environ.get("REDIS_URL", "")
REDIS_SENTINEL_HOSTS = os.environ.get("REDIS_SENTINEL_HOSTS", "")
REDIS_SENTINEL_PORT = os.environ.get("REDIS_SENTINEL_PORT", "26379")
####################################
# UVICORN WORKERS
####################################
# Number of uvicorn worker processes for handling requests
UVICORN_WORKERS = os.environ.get("UVICORN_WORKERS", "1")
try:
UVICORN_WORKERS = int(UVICORN_WORKERS)
if UVICORN_WORKERS < 1:
UVICORN_WORKERS = 1
except ValueError:
UVICORN_WORKERS = 1
log.info(f"Invalid UVICORN_WORKERS value, defaulting to {UVICORN_WORKERS}")
####################################
# WEBUI_AUTH (Required for security)
####################################
@@ -408,6 +422,21 @@ else:
except Exception:
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = 10
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = os.environ.get(
"AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA", "10"
)
if AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA == "":
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = None
else:
try:
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = int(
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA
)
except Exception:
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = 10
####################################
# OFFLINE_MODE
####################################
@@ -460,3 +489,10 @@ OTEL_TRACES_SAMPLER = os.environ.get(
PIP_OPTIONS = os.getenv("PIP_OPTIONS", "").split()
PIP_PACKAGE_INDEX_OPTIONS = os.getenv("PIP_PACKAGE_INDEX_OPTIONS", "").split()
####################################
# PROGRESSIVE WEB APP OPTIONS
####################################
EXTERNAL_PWA_MANIFEST_URL = os.environ.get("EXTERNAL_PWA_MANIFEST_URL")
+107 -54
View File
@@ -105,6 +105,8 @@ from open_webui.config import (
OPENAI_API_CONFIGS,
# Direct Connections
ENABLE_DIRECT_CONNECTIONS,
# Tool Server Configs
TOOL_SERVER_CONNECTIONS,
# Code Execution
ENABLE_CODE_EXECUTION,
CODE_EXECUTION_ENGINE,
@@ -146,6 +148,9 @@ from open_webui.config import (
AUDIO_STT_MODEL,
AUDIO_STT_OPENAI_API_BASE_URL,
AUDIO_STT_OPENAI_API_KEY,
AUDIO_STT_AZURE_API_KEY,
AUDIO_STT_AZURE_REGION,
AUDIO_STT_AZURE_LOCALES,
AUDIO_TTS_API_KEY,
AUDIO_TTS_ENGINE,
AUDIO_TTS_MODEL,
@@ -155,11 +160,11 @@ from open_webui.config import (
AUDIO_TTS_VOICE,
AUDIO_TTS_AZURE_SPEECH_REGION,
AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT,
PLAYWRIGHT_WS_URI,
PLAYWRIGHT_WS_URL,
PLAYWRIGHT_TIMEOUT,
FIRECRAWL_API_BASE_URL,
FIRECRAWL_API_KEY,
RAG_WEB_LOADER_ENGINE,
WEB_LOADER_ENGINE,
WHISPER_MODEL,
DEEPGRAM_API_KEY,
WHISPER_MODEL_AUTO_UPDATE,
@@ -191,6 +196,7 @@ from open_webui.config import (
DOCLING_SERVER_URL,
DOCUMENT_INTELLIGENCE_ENDPOINT,
DOCUMENT_INTELLIGENCE_KEY,
MISTRAL_OCR_API_KEY,
RAG_TOP_K,
RAG_TOP_K_RERANKER,
RAG_TEXT_SPLITTER,
@@ -199,12 +205,13 @@ from open_webui.config import (
YOUTUBE_LOADER_LANGUAGE,
YOUTUBE_LOADER_PROXY_URL,
# Retrieval (Web Search)
RAG_WEB_SEARCH_ENGINE,
ENABLE_WEB_SEARCH,
WEB_SEARCH_ENGINE,
BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL,
RAG_WEB_SEARCH_RESULT_COUNT,
RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
RAG_WEB_SEARCH_TRUST_ENV,
RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
WEB_SEARCH_RESULT_COUNT,
WEB_SEARCH_CONCURRENT_REQUESTS,
WEB_SEARCH_TRUST_ENV,
WEB_SEARCH_DOMAIN_FILTER_LIST,
JINA_API_KEY,
SEARCHAPI_API_KEY,
SEARCHAPI_ENGINE,
@@ -222,6 +229,8 @@ from open_webui.config import (
BRAVE_SEARCH_API_KEY,
EXA_API_KEY,
PERPLEXITY_API_KEY,
SOUGOU_API_SID,
SOUGOU_API_SK,
KAGI_SEARCH_API_KEY,
MOJEEK_SEARCH_API_KEY,
BOCHA_SEARCH_API_KEY,
@@ -232,8 +241,7 @@ from open_webui.config import (
ONEDRIVE_CLIENT_ID,
ENABLE_RAG_HYBRID_SEARCH,
ENABLE_RAG_LOCAL_WEB_FETCH,
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
ENABLE_RAG_WEB_SEARCH,
ENABLE_WEB_LOADER_SSL_VERIFICATION,
ENABLE_GOOGLE_DRIVE_INTEGRATION,
ENABLE_ONEDRIVE_INTEGRATION,
UPLOAD_DIR,
@@ -337,6 +345,7 @@ from open_webui.env import (
RESET_CONFIG_ON_START,
OFFLINE_MODE,
ENABLE_OTEL,
EXTERNAL_PWA_MANIFEST_URL,
)
@@ -355,6 +364,7 @@ from open_webui.utils.access_control import has_access
from open_webui.utils.auth import (
get_license_data,
get_http_authorization_cred,
decode_token,
get_admin_user,
get_verified_user,
@@ -362,7 +372,11 @@ from open_webui.utils.auth import (
from open_webui.utils.oauth import OAuthManager
from open_webui.utils.security_headers import SecurityHeadersMiddleware
from open_webui.tasks import stop_task, list_tasks # Import from tasks.py
from open_webui.tasks import (
list_task_ids_by_chat_id,
stop_task,
list_tasks,
) # Import from tasks.py
from open_webui.utils.redis import get_sentinels_from_env
@@ -422,6 +436,7 @@ async def lifespan(app: FastAPI):
app = FastAPI(
title="Open WebUI",
docs_url="/docs" if ENV == "dev" else None,
openapi_url="/openapi.json" if ENV == "dev" else None,
redoc_url=None,
@@ -477,6 +492,15 @@ app.state.config.OPENAI_API_CONFIGS = OPENAI_API_CONFIGS
app.state.OPENAI_MODELS = {}
########################################
#
# TOOL SERVERS
#
########################################
app.state.config.TOOL_SERVER_CONNECTIONS = TOOL_SERVER_CONNECTIONS
app.state.TOOL_SERVERS = []
########################################
#
# DIRECT CONNECTIONS
@@ -551,6 +575,7 @@ app.state.config.LDAP_CIPHERS = LDAP_CIPHERS
app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
app.state.AUTH_TRUSTED_NAME_HEADER = WEBUI_AUTH_TRUSTED_NAME_HEADER
app.state.EXTERNAL_PWA_MANIFEST_URL = EXTERNAL_PWA_MANIFEST_URL
app.state.USER_COUNT = None
app.state.TOOLS = {}
@@ -573,15 +598,14 @@ app.state.config.FILE_MAX_COUNT = RAG_FILE_MAX_COUNT
app.state.config.RAG_FULL_CONTEXT = RAG_FULL_CONTEXT
app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL = BYPASS_EMBEDDING_AND_RETRIEVAL
app.state.config.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH
app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
)
app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION = ENABLE_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.DOCLING_SERVER_URL = DOCLING_SERVER_URL
app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = DOCUMENT_INTELLIGENCE_ENDPOINT
app.state.config.DOCUMENT_INTELLIGENCE_KEY = DOCUMENT_INTELLIGENCE_KEY
app.state.config.MISTRAL_OCR_API_KEY = MISTRAL_OCR_API_KEY
app.state.config.TEXT_SPLITTER = RAG_TEXT_SPLITTER
app.state.config.TIKTOKEN_ENCODING_NAME = TIKTOKEN_ENCODING_NAME
@@ -607,12 +631,16 @@ app.state.config.YOUTUBE_LOADER_LANGUAGE = YOUTUBE_LOADER_LANGUAGE
app.state.config.YOUTUBE_LOADER_PROXY_URL = YOUTUBE_LOADER_PROXY_URL
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.ENABLE_WEB_SEARCH = ENABLE_WEB_SEARCH
app.state.config.WEB_SEARCH_ENGINE = WEB_SEARCH_ENGINE
app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST = WEB_SEARCH_DOMAIN_FILTER_LIST
app.state.config.WEB_SEARCH_RESULT_COUNT = WEB_SEARCH_RESULT_COUNT
app.state.config.WEB_SEARCH_CONCURRENT_REQUESTS = WEB_SEARCH_CONCURRENT_REQUESTS
app.state.config.WEB_LOADER_ENGINE = WEB_LOADER_ENGINE
app.state.config.WEB_SEARCH_TRUST_ENV = WEB_SEARCH_TRUST_ENV
app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = (
BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL
)
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = RAG_WEB_SEARCH_DOMAIN_FILTER_LIST
app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = ENABLE_GOOGLE_DRIVE_INTEGRATION
app.state.config.ENABLE_ONEDRIVE_INTEGRATION = ENABLE_ONEDRIVE_INTEGRATION
@@ -637,12 +665,11 @@ app.state.config.BING_SEARCH_V7_ENDPOINT = BING_SEARCH_V7_ENDPOINT
app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY = BING_SEARCH_V7_SUBSCRIPTION_KEY
app.state.config.EXA_API_KEY = EXA_API_KEY
app.state.config.PERPLEXITY_API_KEY = PERPLEXITY_API_KEY
app.state.config.SOUGOU_API_SID = SOUGOU_API_SID
app.state.config.SOUGOU_API_SK = SOUGOU_API_SK
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
app.state.config.RAG_WEB_LOADER_ENGINE = RAG_WEB_LOADER_ENGINE
app.state.config.RAG_WEB_SEARCH_TRUST_ENV = RAG_WEB_SEARCH_TRUST_ENV
app.state.config.PLAYWRIGHT_WS_URI = PLAYWRIGHT_WS_URI
app.state.config.PLAYWRIGHT_WS_URL = PLAYWRIGHT_WS_URL
app.state.config.PLAYWRIGHT_TIMEOUT = PLAYWRIGHT_TIMEOUT
app.state.config.FIRECRAWL_API_BASE_URL = FIRECRAWL_API_BASE_URL
app.state.config.FIRECRAWL_API_KEY = FIRECRAWL_API_KEY
@@ -764,6 +791,10 @@ app.state.config.STT_MODEL = AUDIO_STT_MODEL
app.state.config.WHISPER_MODEL = WHISPER_MODEL
app.state.config.DEEPGRAM_API_KEY = DEEPGRAM_API_KEY
app.state.config.AUDIO_STT_AZURE_API_KEY = AUDIO_STT_AZURE_API_KEY
app.state.config.AUDIO_STT_AZURE_REGION = AUDIO_STT_AZURE_REGION
app.state.config.AUDIO_STT_AZURE_LOCALES = AUDIO_STT_AZURE_LOCALES
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
@@ -862,6 +893,10 @@ async def commit_session_after_request(request: Request, call_next):
@app.middleware("http")
async def check_url(request: Request, call_next):
start_time = int(time.time())
request.state.token = get_http_authorization_cred(
request.headers.get("Authorization")
)
request.state.enable_api_key = app.state.config.ENABLE_API_KEY
response = await call_next(request)
process_time = int(time.time()) - start_time
@@ -1035,6 +1070,7 @@ async def chat_completion(
model_item = form_data.pop("model_item", {})
tasks = form_data.pop("background_tasks", None)
metadata = {}
try:
if not model_item.get("direct", False):
model_id = form_data.get("model", None)
@@ -1090,13 +1126,15 @@ async def chat_completion(
except Exception as e:
log.debug(f"Error processing chat payload: {e}")
Chats.upsert_message_to_chat_by_id_and_message_id(
metadata["chat_id"],
metadata["message_id"],
{
"error": {"content": str(e)},
},
)
if metadata.get("chat_id") and metadata.get("message_id"):
# Update the chat message with the error
Chats.upsert_message_to_chat_by_id_and_message_id(
metadata["chat_id"],
metadata["message_id"],
{
"error": {"content": str(e)},
},
)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -1162,7 +1200,7 @@ async def chat_action(
@app.post("/api/tasks/stop/{task_id}")
async def stop_task_endpoint(task_id: str, user=Depends(get_verified_user)):
try:
result = await stop_task(task_id) # Use the function from tasks.py
result = await stop_task(task_id)
return result
except ValueError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
@@ -1170,7 +1208,19 @@ async def stop_task_endpoint(task_id: str, user=Depends(get_verified_user)):
@app.get("/api/tasks")
async def list_tasks_endpoint(user=Depends(get_verified_user)):
return {"tasks": list_tasks()} # Use the function from tasks.py
return {"tasks": list_tasks()}
@app.get("/api/tasks/chat/{chat_id}")
async def list_tasks_by_chat_id_endpoint(chat_id: str, user=Depends(get_verified_user)):
chat = Chats.get_chat_by_id(chat_id)
if chat is None or chat.user_id != user.id:
return {"task_ids": []}
task_ids = list_task_ids_by_chat_id(chat_id)
print(f"Task IDs for chat {chat_id}: {task_ids}")
return {"task_ids": task_ids}
##################################
@@ -1226,7 +1276,7 @@ async def get_app_config(request: Request):
{
"enable_direct_connections": app.state.config.ENABLE_DIRECT_CONNECTIONS,
"enable_channels": app.state.config.ENABLE_CHANNELS,
"enable_web_search": app.state.config.ENABLE_RAG_WEB_SEARCH,
"enable_web_search": app.state.config.ENABLE_WEB_SEARCH,
"enable_code_execution": app.state.config.ENABLE_CODE_EXECUTION,
"enable_code_interpreter": app.state.config.ENABLE_CODE_INTERPRETER,
"enable_image_generation": app.state.config.ENABLE_IMAGE_GENERATION,
@@ -1372,29 +1422,32 @@ async def oauth_callback(provider: str, request: Request, response: Response):
@app.get("/manifest.json")
async def get_manifest_json():
return {
"name": app.state.WEBUI_NAME,
"short_name": app.state.WEBUI_NAME,
"description": "Open WebUI is an open, extensible, user-friendly interface for AI that adapts to your workflow.",
"start_url": "/",
"display": "standalone",
"background_color": "#343541",
"orientation": "natural",
"icons": [
{
"src": "/static/logo.png",
"type": "image/png",
"sizes": "500x500",
"purpose": "any",
},
{
"src": "/static/logo.png",
"type": "image/png",
"sizes": "500x500",
"purpose": "maskable",
},
],
}
if app.state.EXTERNAL_PWA_MANIFEST_URL:
return requests.get(app.state.EXTERNAL_PWA_MANIFEST_URL).json()
else:
return {
"name": app.state.WEBUI_NAME,
"short_name": app.state.WEBUI_NAME,
"description": "Open WebUI is an open, extensible, user-friendly interface for AI that adapts to your workflow.",
"start_url": "/",
"display": "standalone",
"background_color": "#343541",
"orientation": "natural",
"icons": [
{
"src": "/static/logo.png",
"type": "image/png",
"sizes": "500x500",
"purpose": "any",
},
{
"src": "/static/logo.png",
"type": "image/png",
"sizes": "500x500",
"purpose": "maskable",
},
],
}
@app.get("/opensearch.xml")
+3 -2
View File
@@ -63,14 +63,15 @@ class MemoriesTable:
else:
return None
def update_memory_by_id(
def update_memory_by_id_and_user_id(
self,
id: str,
user_id: str,
content: str,
) -> Optional[MemoryModel]:
with get_db() as db:
try:
db.query(Memory).filter_by(id=id).update(
db.query(Memory).filter_by(id=id, user_id=user_id).update(
{"content": content, "updated_at": int(time.time())}
)
db.commit()
+27 -11
View File
@@ -20,6 +20,9 @@ from langchain_community.document_loaders import (
YoutubeLoader,
)
from langchain_core.documents import Document
from open_webui.retrieval.loaders.mistral import MistralLoader
from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
@@ -181,13 +184,16 @@ class Loader:
for doc in docs
]
def _is_text_file(self, file_ext: str, file_content_type: str) -> bool:
return file_ext in known_source_ext or (
file_content_type and file_content_type.find("text/") >= 0
)
def _get_loader(self, filename: str, file_content_type: str, file_path: str):
file_ext = filename.split(".")[-1].lower()
if self.engine == "tika" and self.kwargs.get("TIKA_SERVER_URL"):
if file_ext in known_source_ext or (
file_content_type and file_content_type.find("text/") >= 0
):
if self._is_text_file(file_ext, file_content_type):
loader = TextLoader(file_path, autodetect_encoding=True)
else:
loader = TikaLoader(
@@ -196,11 +202,14 @@ class Loader:
mime_type=file_content_type,
)
elif self.engine == "docling" and self.kwargs.get("DOCLING_SERVER_URL"):
loader = DoclingLoader(
url=self.kwargs.get("DOCLING_SERVER_URL"),
file_path=file_path,
mime_type=file_content_type,
)
if self._is_text_file(file_ext, file_content_type):
loader = TextLoader(file_path, autodetect_encoding=True)
else:
loader = DoclingLoader(
url=self.kwargs.get("DOCLING_SERVER_URL"),
file_path=file_path,
mime_type=file_content_type,
)
elif (
self.engine == "document_intelligence"
and self.kwargs.get("DOCUMENT_INTELLIGENCE_ENDPOINT") != ""
@@ -222,6 +231,15 @@ class Loader:
api_endpoint=self.kwargs.get("DOCUMENT_INTELLIGENCE_ENDPOINT"),
api_key=self.kwargs.get("DOCUMENT_INTELLIGENCE_KEY"),
)
elif (
self.engine == "mistral_ocr"
and self.kwargs.get("MISTRAL_OCR_API_KEY") != ""
and file_ext
in ["pdf"] # Mistral OCR currently only supports PDF and images
):
loader = MistralLoader(
api_key=self.kwargs.get("MISTRAL_OCR_API_KEY"), file_path=file_path
)
else:
if file_ext == "pdf":
loader = PyPDFLoader(
@@ -257,9 +275,7 @@ class Loader:
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
):
elif self._is_text_file(file_ext, file_content_type):
loader = TextLoader(file_path, autodetect_encoding=True)
else:
loader = TextLoader(file_path, autodetect_encoding=True)
@@ -0,0 +1,225 @@
import requests
import logging
import os
import sys
from typing import List, Dict, Any
from langchain_core.documents import Document
from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class MistralLoader:
"""
Loads documents by processing them through the Mistral OCR API.
"""
BASE_API_URL = "https://api.mistral.ai/v1"
def __init__(self, api_key: str, file_path: str):
"""
Initializes the loader.
Args:
api_key: Your Mistral API key.
file_path: The local path to the PDF file to process.
"""
if not api_key:
raise ValueError("API key cannot be empty.")
if not os.path.exists(file_path):
raise FileNotFoundError(f"File not found at {file_path}")
self.api_key = api_key
self.file_path = file_path
self.headers = {"Authorization": f"Bearer {self.api_key}"}
def _handle_response(self, response: requests.Response) -> Dict[str, Any]:
"""Checks response status and returns JSON content."""
try:
response.raise_for_status() # Raises HTTPError for bad responses (4xx or 5xx)
# Handle potential empty responses for certain successful requests (e.g., DELETE)
if response.status_code == 204 or not response.content:
return {} # Return empty dict if no content
return response.json()
except requests.exceptions.HTTPError as http_err:
log.error(f"HTTP error occurred: {http_err} - Response: {response.text}")
raise
except requests.exceptions.RequestException as req_err:
log.error(f"Request exception occurred: {req_err}")
raise
except ValueError as json_err: # Includes JSONDecodeError
log.error(f"JSON decode error: {json_err} - Response: {response.text}")
raise # Re-raise after logging
def _upload_file(self) -> str:
"""Uploads the file to Mistral for OCR processing."""
log.info("Uploading file to Mistral API")
url = f"{self.BASE_API_URL}/files"
file_name = os.path.basename(self.file_path)
try:
with open(self.file_path, "rb") as f:
files = {"file": (file_name, f, "application/pdf")}
data = {"purpose": "ocr"}
upload_headers = self.headers.copy() # Avoid modifying self.headers
response = requests.post(
url, headers=upload_headers, files=files, data=data
)
response_data = self._handle_response(response)
file_id = response_data.get("id")
if not file_id:
raise ValueError("File ID not found in upload response.")
log.info(f"File uploaded successfully. File ID: {file_id}")
return file_id
except Exception as e:
log.error(f"Failed to upload file: {e}")
raise
def _get_signed_url(self, file_id: str) -> str:
"""Retrieves a temporary signed URL for the uploaded file."""
log.info(f"Getting signed URL for file ID: {file_id}")
url = f"{self.BASE_API_URL}/files/{file_id}/url"
params = {"expiry": 1}
signed_url_headers = {**self.headers, "Accept": "application/json"}
try:
response = requests.get(url, headers=signed_url_headers, params=params)
response_data = self._handle_response(response)
signed_url = response_data.get("url")
if not signed_url:
raise ValueError("Signed URL not found in response.")
log.info("Signed URL received.")
return signed_url
except Exception as e:
log.error(f"Failed to get signed URL: {e}")
raise
def _process_ocr(self, signed_url: str) -> Dict[str, Any]:
"""Sends the signed URL to the OCR endpoint for processing."""
log.info("Processing OCR via Mistral API")
url = f"{self.BASE_API_URL}/ocr"
ocr_headers = {
**self.headers,
"Content-Type": "application/json",
"Accept": "application/json",
}
payload = {
"model": "mistral-ocr-latest",
"document": {
"type": "document_url",
"document_url": signed_url,
},
"include_image_base64": False,
}
try:
response = requests.post(url, headers=ocr_headers, json=payload)
ocr_response = self._handle_response(response)
log.info("OCR processing done.")
log.debug("OCR response: %s", ocr_response)
return ocr_response
except Exception as e:
log.error(f"Failed during OCR processing: {e}")
raise
def _delete_file(self, file_id: str) -> None:
"""Deletes the file from Mistral storage."""
log.info(f"Deleting uploaded file ID: {file_id}")
url = f"{self.BASE_API_URL}/files/{file_id}"
# No specific Accept header needed, default or Authorization is usually sufficient
try:
response = requests.delete(url, headers=self.headers)
delete_response = self._handle_response(
response
) # Check status, ignore response body unless needed
log.info(
f"File deleted successfully: {delete_response}"
) # Log the response if available
except Exception as e:
# Log error but don't necessarily halt execution if deletion fails
log.error(f"Failed to delete file ID {file_id}: {e}")
# Depending on requirements, you might choose to raise the error here
def load(self) -> List[Document]:
"""
Executes the full OCR workflow: upload, get URL, process OCR, delete file.
Returns:
A list of Document objects, one for each page processed.
"""
file_id = None
try:
# 1. Upload file
file_id = self._upload_file()
# 2. Get Signed URL
signed_url = self._get_signed_url(file_id)
# 3. Process OCR
ocr_response = self._process_ocr(signed_url)
# 4. Process results
pages_data = ocr_response.get("pages")
if not pages_data:
log.warning("No pages found in OCR response.")
return [Document(page_content="No text content found", metadata={})]
documents = []
total_pages = len(pages_data)
for page_data in pages_data:
page_content = page_data.get("markdown")
page_index = page_data.get("index") # API uses 0-based index
if page_content is not None and page_index is not None:
documents.append(
Document(
page_content=page_content,
metadata={
"page": page_index, # 0-based index from API
"page_label": page_index
+ 1, # 1-based label for convenience
"total_pages": total_pages,
# Add other relevant metadata from page_data if available/needed
# e.g., page_data.get('width'), page_data.get('height')
},
)
)
else:
log.warning(
f"Skipping page due to missing 'markdown' or 'index'. Data: {page_data}"
)
if not documents:
# Case where pages existed but none had valid markdown/index
log.warning(
"OCR response contained pages, but none had valid content/index."
)
return [
Document(
page_content="No text content found in valid pages", metadata={}
)
]
return documents
except Exception as e:
log.error(f"An error occurred during the loading process: {e}")
# Return an empty list or a specific error document on failure
return [Document(page_content=f"Error during processing: {e}", metadata={})]
finally:
# 5. Delete file (attempt even if prior steps failed after upload)
if file_id:
try:
self._delete_file(file_id)
except Exception as del_e:
# Log deletion error, but don't overwrite original error if one occurred
log.error(
f"Cleanup error: Could not delete file ID {file_id}. Reason: {del_e}"
)
@@ -110,7 +110,7 @@ class YoutubeLoader:
transcript = " ".join(
map(
lambda transcript_piece: transcript_piece["text"].strip(" "),
lambda transcript_piece: transcript_piece.text.strip(" "),
transcript_pieces,
)
)
+21 -4
View File
@@ -77,6 +77,7 @@ def query_doc(
collection_name: str, query_embedding: list[float], k: int, user: UserModel = None
):
try:
log.debug(f"query_doc:doc {collection_name}")
result = VECTOR_DB_CLIENT.search(
collection_name=collection_name,
vectors=[query_embedding],
@@ -94,6 +95,7 @@ def query_doc(
def get_doc(collection_name: str, user: UserModel = None):
try:
log.debug(f"get_doc:doc {collection_name}")
result = VECTOR_DB_CLIENT.get(collection_name=collection_name)
if result:
@@ -116,6 +118,7 @@ def query_doc_with_hybrid_search(
r: float,
) -> dict:
try:
log.debug(f"query_doc_with_hybrid_search:doc {collection_name}")
bm25_retriever = BM25Retriever.from_texts(
texts=collection_result.documents[0],
metadatas=collection_result.metadatas[0],
@@ -168,6 +171,7 @@ def query_doc_with_hybrid_search(
)
return result
except Exception as e:
log.exception(f"Error querying doc {collection_name} with hybrid search: {e}")
raise e
@@ -257,6 +261,7 @@ def query_collection(
) -> dict:
results = []
for query in queries:
log.debug(f"query_collection:query {query}")
query_embedding = embedding_function(query, prefix=RAG_EMBEDDING_QUERY_PREFIX)
for collection_name in collection_names:
if collection_name:
@@ -292,6 +297,9 @@ def query_collection_with_hybrid_search(
collection_results = {}
for collection_name in collection_names:
try:
log.debug(
f"query_collection_with_hybrid_search:VECTOR_DB_CLIENT.get:collection {collection_name}"
)
collection_results[collection_name] = VECTOR_DB_CLIENT.get(
collection_name=collection_name
)
@@ -320,10 +328,13 @@ def query_collection_with_hybrid_search(
log.exception(f"Error when querying the collection with hybrid_search: {e}")
return None, e
# Prepare tasks for all collections and queries
# Avoid running any tasks for collections that failed to fetch data (have assigned None)
tasks = [
(collection_name, query)
for collection_name in collection_names
for query in queries
(cn, q)
for cn in collection_names
if collection_results[cn] is not None
for q in queries
]
with ThreadPoolExecutor() as executor:
@@ -354,7 +365,7 @@ def get_embedding_function(
):
if embedding_engine == "":
return lambda query, prefix=None, user=None: embedding_function.encode(
query, prompt=prefix if prefix else None
query, **({"prompt": prefix} if prefix else {})
).tolist()
elif embedding_engine in ["ollama", "openai"]:
func = lambda query, prefix=None, user=None: generate_embeddings(
@@ -610,6 +621,9 @@ def generate_openai_batch_embeddings(
user: UserModel = None,
) -> Optional[list[list[float]]]:
try:
log.debug(
f"generate_openai_batch_embeddings:model {model} batch size: {len(texts)}"
)
json_data = {"input": texts, "model": model}
if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str):
json_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix
@@ -652,6 +666,9 @@ def generate_ollama_batch_embeddings(
user: UserModel = None,
) -> Optional[list[list[float]]]:
try:
log.debug(
f"generate_ollama_batch_embeddings:model {model} batch size: {len(texts)}"
)
json_data = {"input": texts, "model": model}
if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str):
json_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix
@@ -3,6 +3,7 @@ from typing import Optional
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
from duckduckgo_search import DDGS
from duckduckgo_search.exceptions import RatelimitException
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
@@ -22,16 +23,15 @@ def search_duckduckgo(
list[SearchResult]: A list of search results
"""
# Use the DDGS context manager to create a DDGS object
search_results = []
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]
try:
search_results = ddgs.text(
query, safesearch="moderate", max_results=count, backend="lite"
)
except RatelimitException as e:
log.error(f"RatelimitException: {e}")
if filter_list:
search_results = get_filtered_results(search_results, filter_list)
@@ -0,0 +1,60 @@
import logging
import json
from typing import Optional, List
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_sougou(
sougou_api_sid: str,
sougou_api_sk: str,
query: str,
count: int,
filter_list: Optional[List[str]] = None,
) -> List[SearchResult]:
from tencentcloud.common.common_client import CommonClient
from tencentcloud.common import credential
from tencentcloud.common.exception.tencent_cloud_sdk_exception import (
TencentCloudSDKException,
)
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
try:
cred = credential.Credential(sougou_api_sid, sougou_api_sk)
http_profile = HttpProfile()
http_profile.endpoint = "tms.tencentcloudapi.com"
client_profile = ClientProfile()
client_profile.http_profile = http_profile
params = json.dumps({"Query": query, "Cnt": 20})
common_client = CommonClient(
"tms", "2020-12-29", cred, "", profile=client_profile
)
results = [
json.loads(page)
for page in common_client.call_json("SearchPro", json.loads(params))[
"Response"
]["Pages"]
]
sorted_results = sorted(
results, key=lambda x: x.get("scour", 0.0), reverse=True
)
if filter_list:
sorted_results = get_filtered_results(sorted_results, filter_list)
return [
SearchResult(
link=result.get("url"),
title=result.get("title"),
snippet=result.get("passage"),
)
for result in sorted_results[:count]
]
except TencentCloudSDKException as err:
log.error(f"Error in Sougou search: {err}")
return []
+25 -23
View File
@@ -28,9 +28,9 @@ from open_webui.retrieval.loaders.tavily import TavilyLoader
from open_webui.constants import ERROR_MESSAGES
from open_webui.config import (
ENABLE_RAG_LOCAL_WEB_FETCH,
PLAYWRIGHT_WS_URI,
PLAYWRIGHT_WS_URL,
PLAYWRIGHT_TIMEOUT,
RAG_WEB_LOADER_ENGINE,
WEB_LOADER_ENGINE,
FIRECRAWL_API_BASE_URL,
FIRECRAWL_API_KEY,
TAVILY_API_KEY,
@@ -584,13 +584,6 @@ class SafeWebBaseLoader(WebBaseLoader):
return [document async for document in self.alazy_load()]
RAG_WEB_LOADER_ENGINES = defaultdict(lambda: SafeWebBaseLoader)
RAG_WEB_LOADER_ENGINES["playwright"] = SafePlaywrightURLLoader
RAG_WEB_LOADER_ENGINES["safe_web"] = SafeWebBaseLoader
RAG_WEB_LOADER_ENGINES["firecrawl"] = SafeFireCrawlLoader
RAG_WEB_LOADER_ENGINES["tavily"] = SafeTavilyLoader
def get_web_loader(
urls: Union[str, Sequence[str]],
verify_ssl: bool = True,
@@ -608,27 +601,36 @@ def get_web_loader(
"trust_env": trust_env,
}
if RAG_WEB_LOADER_ENGINE.value == "playwright":
if WEB_LOADER_ENGINE.value == "" or WEB_LOADER_ENGINE.value == "safe_web":
WebLoaderClass = SafeWebBaseLoader
if WEB_LOADER_ENGINE.value == "playwright":
WebLoaderClass = SafePlaywrightURLLoader
web_loader_args["playwright_timeout"] = PLAYWRIGHT_TIMEOUT.value * 1000
if PLAYWRIGHT_WS_URI.value:
web_loader_args["playwright_ws_url"] = PLAYWRIGHT_WS_URI.value
if PLAYWRIGHT_WS_URL.value:
web_loader_args["playwright_ws_url"] = PLAYWRIGHT_WS_URL.value
if RAG_WEB_LOADER_ENGINE.value == "firecrawl":
if WEB_LOADER_ENGINE.value == "firecrawl":
WebLoaderClass = SafeFireCrawlLoader
web_loader_args["api_key"] = FIRECRAWL_API_KEY.value
web_loader_args["api_url"] = FIRECRAWL_API_BASE_URL.value
if RAG_WEB_LOADER_ENGINE.value == "tavily":
if WEB_LOADER_ENGINE.value == "tavily":
WebLoaderClass = SafeTavilyLoader
web_loader_args["api_key"] = TAVILY_API_KEY.value
web_loader_args["extract_depth"] = TAVILY_EXTRACT_DEPTH.value
# Create the appropriate WebLoader based on the configuration
WebLoaderClass = RAG_WEB_LOADER_ENGINES[RAG_WEB_LOADER_ENGINE.value]
web_loader = WebLoaderClass(**web_loader_args)
if WebLoaderClass:
web_loader = WebLoaderClass(**web_loader_args)
log.debug(
"Using RAG_WEB_LOADER_ENGINE %s for %s URLs",
web_loader.__class__.__name__,
len(safe_urls),
)
log.debug(
"Using WEB_LOADER_ENGINE %s for %s URLs",
web_loader.__class__.__name__,
len(safe_urls),
)
return web_loader
return web_loader
else:
raise ValueError(
f"Invalid WEB_LOADER_ENGINE: {WEB_LOADER_ENGINE.value}. "
"Please set it to 'safe_web', 'playwright', 'firecrawl', or 'tavily'."
)
+150 -14
View File
@@ -50,6 +50,8 @@ router = APIRouter()
# Constants
MAX_FILE_SIZE_MB = 25
MAX_FILE_SIZE = MAX_FILE_SIZE_MB * 1024 * 1024 # Convert MB to bytes
AZURE_MAX_FILE_SIZE_MB = 200
AZURE_MAX_FILE_SIZE = AZURE_MAX_FILE_SIZE_MB * 1024 * 1024 # Convert MB to bytes
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["AUDIO"])
@@ -68,8 +70,8 @@ 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."""
def get_audio_format(file_path):
"""Check if the given file needs to be converted to a different format."""
if not os.path.isfile(file_path):
log.error(f"File not found: {file_path}")
return False
@@ -80,13 +82,17 @@ def is_mp4_audio(file_path):
and info.get("codec_type") == "audio"
and info.get("codec_tag_string") == "mp4a"
):
return True
return False
return "mp4"
elif info.get("format_name") == "ogg":
return "ogg"
elif info.get("format_name") == "matroska,webm":
return "webm"
return None
def convert_mp4_to_wav(file_path, output_path):
"""Convert MP4 audio file to WAV format."""
audio = AudioSegment.from_file(file_path, format="mp4")
def convert_audio_to_wav(file_path, output_path, conversion_type):
"""Convert MP4/OGG audio file to WAV format."""
audio = AudioSegment.from_file(file_path, format=conversion_type)
audio.export(output_path, format="wav")
log.info(f"Converted {file_path} to {output_path}")
@@ -141,6 +147,9 @@ class STTConfigForm(BaseModel):
MODEL: str
WHISPER_MODEL: str
DEEPGRAM_API_KEY: str
AZURE_API_KEY: str
AZURE_REGION: str
AZURE_LOCALES: str
class AudioConfigUpdateForm(BaseModel):
@@ -169,6 +178,9 @@ async def get_audio_config(request: Request, user=Depends(get_admin_user)):
"MODEL": request.app.state.config.STT_MODEL,
"WHISPER_MODEL": request.app.state.config.WHISPER_MODEL,
"DEEPGRAM_API_KEY": request.app.state.config.DEEPGRAM_API_KEY,
"AZURE_API_KEY": request.app.state.config.AUDIO_STT_AZURE_API_KEY,
"AZURE_REGION": request.app.state.config.AUDIO_STT_AZURE_REGION,
"AZURE_LOCALES": request.app.state.config.AUDIO_STT_AZURE_LOCALES,
},
}
@@ -195,6 +207,9 @@ async def update_audio_config(
request.app.state.config.STT_MODEL = form_data.stt.MODEL
request.app.state.config.WHISPER_MODEL = form_data.stt.WHISPER_MODEL
request.app.state.config.DEEPGRAM_API_KEY = form_data.stt.DEEPGRAM_API_KEY
request.app.state.config.AUDIO_STT_AZURE_API_KEY = form_data.stt.AZURE_API_KEY
request.app.state.config.AUDIO_STT_AZURE_REGION = form_data.stt.AZURE_REGION
request.app.state.config.AUDIO_STT_AZURE_LOCALES = form_data.stt.AZURE_LOCALES
if request.app.state.config.STT_ENGINE == "":
request.app.state.faster_whisper_model = set_faster_whisper_model(
@@ -220,6 +235,9 @@ async def update_audio_config(
"MODEL": request.app.state.config.STT_MODEL,
"WHISPER_MODEL": request.app.state.config.WHISPER_MODEL,
"DEEPGRAM_API_KEY": request.app.state.config.DEEPGRAM_API_KEY,
"AZURE_API_KEY": request.app.state.config.AUDIO_STT_AZURE_API_KEY,
"AZURE_REGION": request.app.state.config.AUDIO_STT_AZURE_REGION,
"AZURE_LOCALES": request.app.state.config.AUDIO_STT_AZURE_LOCALES,
},
}
@@ -312,7 +330,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
detail = f"External: {e}"
raise HTTPException(
status_code=getattr(r, "status", 500),
status_code=getattr(r, "status", 500) if r else 500,
detail=detail if detail else "Open WebUI: Server Connection Error",
)
@@ -366,7 +384,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
detail = f"External: {e}"
raise HTTPException(
status_code=getattr(r, "status", 500),
status_code=getattr(r, "status", 500) if r else 500,
detail=detail if detail else "Open WebUI: Server Connection Error",
)
@@ -422,7 +440,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
detail = f"External: {e}"
raise HTTPException(
status_code=getattr(r, "status", 500),
status_code=getattr(r, "status", 500) if r else 500,
detail=detail if detail else "Open WebUI: Server Connection Error",
)
@@ -496,10 +514,15 @@ def transcribe(request: Request, file_path):
log.debug(data)
return data
elif request.app.state.config.STT_ENGINE == "openai":
if is_mp4_audio(file_path):
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)
audio_format = get_audio_format(file_path)
if audio_format:
os.rename(file_path, file_path.replace(".wav", f".{audio_format}"))
# Convert unsupported audio file to WAV format
convert_audio_to_wav(
file_path.replace(".wav", f".{audio_format}"),
file_path,
audio_format,
)
r = None
try:
@@ -598,6 +621,119 @@ def transcribe(request: Request, file_path):
detail = f"External: {e}"
raise Exception(detail if detail else "Open WebUI: Server Connection Error")
elif request.app.state.config.STT_ENGINE == "azure":
# Check file exists and size
if not os.path.exists(file_path):
raise HTTPException(status_code=400, detail="Audio file not found")
# Check file size (Azure has a larger limit of 200MB)
file_size = os.path.getsize(file_path)
if file_size > AZURE_MAX_FILE_SIZE:
raise HTTPException(
status_code=400,
detail=f"File size exceeds Azure's limit of {AZURE_MAX_FILE_SIZE_MB}MB",
)
api_key = request.app.state.config.AUDIO_STT_AZURE_API_KEY
region = request.app.state.config.AUDIO_STT_AZURE_REGION
locales = request.app.state.config.AUDIO_STT_AZURE_LOCALES
# IF NO LOCALES, USE DEFAULTS
if len(locales) < 2:
locales = [
"en-US",
"es-ES",
"es-MX",
"fr-FR",
"hi-IN",
"it-IT",
"de-DE",
"en-GB",
"en-IN",
"ja-JP",
"ko-KR",
"pt-BR",
"zh-CN",
]
locales = ",".join(locales)
if not api_key or not region:
raise HTTPException(
status_code=400,
detail="Azure API key and region are required for Azure STT",
)
r = None
try:
# Prepare the request
data = {
"definition": json.dumps(
{
"locales": locales.split(","),
"diarization": {"maxSpeakers": 3, "enabled": True},
}
if locales
else {}
)
}
url = f"https://{region}.api.cognitive.microsoft.com/speechtotext/transcriptions:transcribe?api-version=2024-11-15"
# Use context manager to ensure file is properly closed
with open(file_path, "rb") as audio_file:
r = requests.post(
url=url,
files={"audio": audio_file},
data=data,
headers={
"Ocp-Apim-Subscription-Key": api_key,
},
)
r.raise_for_status()
response = r.json()
# Extract transcript from response
if not response.get("combinedPhrases"):
raise ValueError("No transcription found in response")
# Get the full transcript from combinedPhrases
transcript = response["combinedPhrases"][0].get("text", "").strip()
if not transcript:
raise ValueError("Empty transcript in response")
data = {"text": transcript}
# Save transcript to json file (consistent with other providers)
transcript_file = f"{file_dir}/{id}.json"
with open(transcript_file, "w") as f:
json.dump(data, f)
log.debug(data)
return data
except (KeyError, IndexError, ValueError) as e:
log.exception("Error parsing Azure response")
raise HTTPException(
status_code=500,
detail=f"Failed to parse Azure response: {str(e)}",
)
except requests.exceptions.RequestException as e:
log.exception(e)
detail = None
try:
if r is not None and r.status_code != 200:
res = r.json()
if "error" in res:
detail = f"External: {res['error'].get('message', '')}"
except Exception:
detail = f"External: {e}"
raise HTTPException(
status_code=getattr(r, "status_code", 500) if r else 500,
detail=detail if detail else "Open WebUI: Server Connection Error",
)
def compress_audio(file_path):
if os.path.getsize(file_path) > MAX_FILE_SIZE:
+34 -21
View File
@@ -194,8 +194,8 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
ciphers=LDAP_CIPHERS,
)
except Exception as e:
log.error(f"An error occurred on TLS: {str(e)}")
raise HTTPException(400, detail=str(e))
log.error(f"TLS configuration error: {str(e)}")
raise HTTPException(400, detail="Failed to configure TLS for LDAP connection.")
try:
server = Server(
@@ -230,11 +230,13 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
entry = connection_app.entries[0]
username = str(entry[f"{LDAP_ATTRIBUTE_FOR_USERNAME}"]).lower()
email = str(entry[f"{LDAP_ATTRIBUTE_FOR_MAIL}"])
if not email or email == "" or email == "[]":
raise HTTPException(400, f"User {form_data.user} does not have email.")
else:
email = entry[f"{LDAP_ATTRIBUTE_FOR_MAIL}"]
if not email:
raise HTTPException(400, "User does not have a valid email address.")
elif isinstance(email, str):
email = email.lower()
elif isinstance(email, list):
email = email[0].lower()
cn = str(entry["cn"])
user_dn = entry.entry_dn
@@ -248,7 +250,7 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
authentication="SIMPLE",
)
if not connection_user.bind():
raise HTTPException(400, f"Authentication failed for {form_data.user}")
raise HTTPException(400, "Authentication failed.")
user = Users.get_user_by_email(email)
if not user:
@@ -276,7 +278,10 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
except HTTPException:
raise
except Exception as err:
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
log.error(f"LDAP user creation error: {str(err)}")
raise HTTPException(
500, detail="Internal error occurred during LDAP user creation."
)
user = Auths.authenticate_user_by_trusted_header(email)
@@ -312,12 +317,10 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
else:
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
else:
raise HTTPException(
400,
f"User {form_data.user} does not match the record. Search result: {str(entry[f'{LDAP_ATTRIBUTE_FOR_USERNAME}'])}",
)
raise HTTPException(400, "User record mismatch.")
except Exception as e:
raise HTTPException(400, detail=str(e))
log.error(f"LDAP authentication error: {str(e)}")
raise HTTPException(400, detail="LDAP authentication failed.")
############################
@@ -453,6 +456,13 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
# Disable signup after the first user is created
request.app.state.config.ENABLE_SIGNUP = False
# The password passed to bcrypt must be 72 bytes or fewer. If it is longer, it will be truncated before hashing.
if len(form_data.password.encode("utf-8")) > 72:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.PASSWORD_TOO_LONG,
)
hashed = get_password_hash(form_data.password)
user = Auths.insert_new_auth(
form_data.email.lower(),
@@ -519,7 +529,8 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
else:
raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR)
except Exception as err:
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
log.error(f"Signup error: {str(err)}")
raise HTTPException(500, detail="An internal error occurred during signup.")
@router.get("/signout")
@@ -547,7 +558,11 @@ async def signout(request: Request, response: Response):
detail="Failed to fetch OpenID configuration",
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
log.error(f"OpenID signout error: {str(e)}")
raise HTTPException(
status_code=500,
detail="Failed to sign out from the OpenID provider.",
)
return {"status": True}
@@ -591,7 +606,10 @@ async def add_user(form_data: AddUserForm, user=Depends(get_admin_user)):
else:
raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR)
except Exception as err:
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
log.error(f"Add user error: {str(err)}")
raise HTTPException(
500, detail="An internal error occurred while adding the user."
)
############################
@@ -764,11 +782,6 @@ async def update_ldap_server(
if not value:
raise HTTPException(400, detail=f"Required field {key} is empty")
if form_data.use_tls and not form_data.certificate_path:
raise HTTPException(
400, detail="TLS is enabled but certificate file path is missing"
)
request.app.state.config.LDAP_SERVER_LABEL = form_data.label
request.app.state.config.LDAP_SERVER_HOST = form_data.host
request.app.state.config.LDAP_SERVER_PORT = form_data.port
+6 -1
View File
@@ -579,7 +579,12 @@ async def clone_chat_by_id(
@router.post("/{id}/clone/shared", response_model=Optional[ChatResponse])
async def clone_shared_chat_by_id(id: str, user=Depends(get_verified_user)):
chat = Chats.get_chat_by_share_id(id)
if user.role == "admin":
chat = Chats.get_chat_by_id(id)
else:
chat = Chats.get_chat_by_share_id(id)
if chat:
updated_chat = {
**chat.chat,
+73 -2
View File
@@ -1,5 +1,5 @@
from fastapi import APIRouter, Depends, Request
from pydantic import BaseModel
from fastapi import APIRouter, Depends, Request, HTTPException
from pydantic import BaseModel, ConfigDict
from typing import Optional
@@ -7,6 +7,8 @@ from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.config import get_config, save_config
from open_webui.config import BannerModel
from open_webui.utils.tools import get_tool_server_data, get_tool_servers_data
router = APIRouter()
@@ -66,6 +68,75 @@ async def set_direct_connections_config(
}
############################
# ToolServers Config
############################
class ToolServerConnection(BaseModel):
url: str
path: str
auth_type: Optional[str]
key: Optional[str]
config: Optional[dict]
model_config = ConfigDict(extra="allow")
class ToolServersConfigForm(BaseModel):
TOOL_SERVER_CONNECTIONS: list[ToolServerConnection]
@router.get("/tool_servers", response_model=ToolServersConfigForm)
async def get_tool_servers_config(request: Request, user=Depends(get_admin_user)):
return {
"TOOL_SERVER_CONNECTIONS": request.app.state.config.TOOL_SERVER_CONNECTIONS,
}
@router.post("/tool_servers", response_model=ToolServersConfigForm)
async def set_tool_servers_config(
request: Request,
form_data: ToolServersConfigForm,
user=Depends(get_admin_user),
):
request.app.state.config.TOOL_SERVER_CONNECTIONS = [
connection.model_dump() for connection in form_data.TOOL_SERVER_CONNECTIONS
]
request.app.state.TOOL_SERVERS = await get_tool_servers_data(
request.app.state.config.TOOL_SERVER_CONNECTIONS
)
return {
"TOOL_SERVER_CONNECTIONS": request.app.state.config.TOOL_SERVER_CONNECTIONS,
}
@router.post("/tool_servers/verify")
async def verify_tool_servers_config(
request: Request, form_data: ToolServerConnection, user=Depends(get_admin_user)
):
"""
Verify the connection to the tool server.
"""
try:
token = None
if form_data.auth_type == "bearer":
token = form_data.key
elif form_data.auth_type == "session":
token = request.state.token.credentials
url = f"{form_data.url}/{form_data.path}"
return await get_tool_server_data(token, url)
except Exception as e:
raise HTTPException(
status_code=400,
detail=f"Failed to connect to the tool server: {str(e)}",
)
############################
# CodeInterpreterConfig
############################
+51 -2
View File
@@ -1,6 +1,7 @@
import logging
import os
import uuid
from fnmatch import fnmatch
from pathlib import Path
from typing import Optional
from urllib.parse import quote
@@ -122,6 +123,7 @@ def upload_file(
]:
file_path = Storage.get_file(file_path)
result = transcribe(request, file_path)
process_file(
request,
ProcessFileForm(file_id=id, content=result.get("text", "")),
@@ -129,7 +131,8 @@ def upload_file(
)
elif file.content_type not in ["image/png", "image/jpeg", "image/gif"]:
process_file(request, ProcessFileForm(file_id=id), user=user)
file_item = Files.get_file_by_id(id=id)
file_item = Files.get_file_by_id(id=id)
except Exception as e:
log.exception(e)
log.error(f"Error processing file: {file_item.id}")
@@ -162,14 +165,60 @@ def upload_file(
@router.get("/", response_model=list[FileModelResponse])
async def list_files(user=Depends(get_verified_user)):
async def list_files(user=Depends(get_verified_user), content: bool = Query(True)):
if user.role == "admin":
files = Files.get_files()
else:
files = Files.get_files_by_user_id(user.id)
if not content:
for file in files:
del file.data["content"]
return files
############################
# Search Files
############################
@router.get("/search", response_model=list[FileModelResponse])
async def search_files(
filename: str = Query(
...,
description="Filename pattern to search for. Supports wildcards such as '*.txt'",
),
content: bool = Query(True),
user=Depends(get_verified_user),
):
"""
Search for files by filename with support for wildcard patterns.
"""
# Get files according to user role
if user.role == "admin":
files = Files.get_files()
else:
files = Files.get_files_by_user_id(user.id)
# Get matching files
matching_files = [
file for file in files if fnmatch(file.filename.lower(), filename.lower())
]
if not matching_files:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No files found matching the pattern.",
)
if not content:
for file in matching_files:
del file.data["content"]
return matching_files
############################
# Delete All Files
############################
+2 -1
View File
@@ -236,7 +236,8 @@ async def delete_folder_by_id(
chat_delete_permission = has_permission(
user.id, "chat.delete", request.app.state.config.USER_PERMISSIONS
)
if not chat_delete_permission:
if user.role != "admin" and not chat_delete_permission:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
+68
View File
@@ -161,6 +161,74 @@ async def create_new_knowledge(
)
############################
# ReindexKnowledgeFiles
############################
@router.post("/reindex", response_model=bool)
async def reindex_knowledge_files(request: Request, user=Depends(get_verified_user)):
if user.role != "admin":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
knowledge_bases = Knowledges.get_knowledge_bases()
log.info(f"Starting reindexing for {len(knowledge_bases)} knowledge bases")
for knowledge_base in knowledge_bases:
try:
files = Files.get_files_by_ids(knowledge_base.data.get("file_ids", []))
try:
if VECTOR_DB_CLIENT.has_collection(collection_name=knowledge_base.id):
VECTOR_DB_CLIENT.delete_collection(
collection_name=knowledge_base.id
)
except Exception as e:
log.error(f"Error deleting collection {knowledge_base.id}: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error deleting vector DB collection",
)
failed_files = []
for file in files:
try:
process_file(
request,
ProcessFileForm(
file_id=file.id, collection_name=knowledge_base.id
),
user=user,
)
except Exception as e:
log.error(
f"Error processing file {file.filename} (ID: {file.id}): {str(e)}"
)
failed_files.append({"file_id": file.id, "error": str(e)})
continue
except Exception as e:
log.error(f"Error processing knowledge base {knowledge_base.id}: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error processing knowledge base",
)
if failed_files:
log.warning(
f"Failed to process {len(failed_files)} files in knowledge base {knowledge_base.id}"
)
for failed in failed_files:
log.warning(f"File ID: {failed['file_id']}, Error: {failed['error']}")
log.info("Reindexing completed successfully")
return True
############################
# GetKnowledgeById
############################
+3 -1
View File
@@ -153,7 +153,9 @@ async def update_memory_by_id(
form_data: MemoryUpdateModel,
user=Depends(get_verified_user),
):
memory = Memories.update_memory_by_id(memory_id, form_data.content)
memory = Memories.update_memory_by_id_and_user_id(
memory_id, user.id, form_data.content
)
if memory is None:
raise HTTPException(status_code=404, detail="Memory not found")
+1 -1
View File
@@ -1197,7 +1197,7 @@ class OpenAIChatMessageContent(BaseModel):
class OpenAIChatMessage(BaseModel):
role: str
content: Union[str, list[OpenAIChatMessageContent]]
content: Union[Optional[str], list[OpenAIChatMessageContent]]
model_config = ConfigDict(extra="allow")
File diff suppressed because it is too large Load Diff
+1 -12
View File
@@ -653,17 +653,6 @@ async def generate_moa_response(
detail="Model not found",
)
# Check if the user has a custom task model
# If the user has a custom task model, use that model
task_model_id = get_task_model_id(
model_id,
request.app.state.config.TASK_MODEL,
request.app.state.config.TASK_MODEL_EXTERNAL,
models,
)
log.debug(f"generating MOA model {task_model_id} for user {user.email} ")
template = DEFAULT_MOA_GENERATION_PROMPT_TEMPLATE
content = moa_response_generation_template(
@@ -673,7 +662,7 @@ async def generate_moa_response(
)
payload = {
"model": task_model_id,
"model": model_id,
"messages": [{"role": "user", "content": content}],
"stream": form_data.get("stream", False),
"metadata": {
+60 -19
View File
@@ -1,6 +1,7 @@
import logging
from pathlib import Path
from typing import Optional
import time
from open_webui.models.tools import (
ToolForm,
@@ -9,15 +10,17 @@ from open_webui.models.tools import (
ToolUserResponse,
Tools,
)
from open_webui.utils.plugin import load_tools_module_by_id, replace_imports
from open_webui.utils.plugin import load_tool_module_by_id, replace_imports
from open_webui.config import CACHE_DIR
from open_webui.constants import ERROR_MESSAGES
from fastapi import APIRouter, Depends, HTTPException, Request, status
from open_webui.utils.tools import get_tools_specs
from open_webui.utils.tools import get_tool_specs
from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.utils.access_control import has_access, has_permission
from open_webui.env import SRC_LOG_LEVELS
from open_webui.utils.tools import get_tool_servers_data
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"])
@@ -30,11 +33,51 @@ router = APIRouter()
@router.get("/", response_model=list[ToolUserResponse])
async def get_tools(user=Depends(get_verified_user)):
if user.role == "admin":
tools = Tools.get_tools()
else:
tools = Tools.get_tools_by_user_id(user.id, "read")
async def get_tools(request: Request, user=Depends(get_verified_user)):
if not request.app.state.TOOL_SERVERS:
# If the tool servers are not set, we need to set them
# This is done only once when the server starts
# This is done to avoid loading the tool servers every time
request.app.state.TOOL_SERVERS = await get_tool_servers_data(
request.app.state.config.TOOL_SERVER_CONNECTIONS
)
tools = Tools.get_tools()
for server in request.app.state.TOOL_SERVERS:
tools.append(
ToolUserResponse(
**{
"id": f"server:{server['idx']}",
"user_id": f"server:{server['idx']}",
"name": server["openapi"]
.get("info", {})
.get("title", "Tool Server"),
"meta": {
"description": server["openapi"]
.get("info", {})
.get("description", ""),
},
"access_control": request.app.state.config.TOOL_SERVER_CONNECTIONS[
server["idx"]
]
.get("config", {})
.get("access_control", None),
"updated_at": int(time.time()),
"created_at": int(time.time()),
}
)
)
if user.role != "admin":
tools = [
tool
for tool in tools
if tool.user_id == user.id
or has_access(user.id, "read", tool.access_control)
]
return tools
@@ -94,15 +137,15 @@ async def create_new_tools(
if tools is None:
try:
form_data.content = replace_imports(form_data.content)
tools_module, frontmatter = load_tools_module_by_id(
tool_module, frontmatter = load_tool_module_by_id(
form_data.id, content=form_data.content
)
form_data.meta.manifest = frontmatter
TOOLS = request.app.state.TOOLS
TOOLS[form_data.id] = tools_module
TOOLS[form_data.id] = tool_module
specs = get_tools_specs(TOOLS[form_data.id])
specs = get_tool_specs(TOOLS[form_data.id])
tools = Tools.insert_new_tool(user.id, form_data, specs)
tool_cache_dir = CACHE_DIR / "tools" / form_data.id
@@ -183,15 +226,13 @@ async def update_tools_by_id(
try:
form_data.content = replace_imports(form_data.content)
tools_module, frontmatter = load_tools_module_by_id(
id, content=form_data.content
)
tool_module, frontmatter = load_tool_module_by_id(id, content=form_data.content)
form_data.meta.manifest = frontmatter
TOOLS = request.app.state.TOOLS
TOOLS[id] = tools_module
TOOLS[id] = tool_module
specs = get_tools_specs(TOOLS[id])
specs = get_tool_specs(TOOLS[id])
updated = {
**form_data.model_dump(exclude={"id"}),
@@ -289,7 +330,7 @@ async def get_tools_valves_spec_by_id(
if id in request.app.state.TOOLS:
tools_module = request.app.state.TOOLS[id]
else:
tools_module, _ = load_tools_module_by_id(id)
tools_module, _ = load_tool_module_by_id(id)
request.app.state.TOOLS[id] = tools_module
if hasattr(tools_module, "Valves"):
@@ -332,7 +373,7 @@ async def update_tools_valves_by_id(
if id in request.app.state.TOOLS:
tools_module = request.app.state.TOOLS[id]
else:
tools_module, _ = load_tools_module_by_id(id)
tools_module, _ = load_tool_module_by_id(id)
request.app.state.TOOLS[id] = tools_module
if not hasattr(tools_module, "Valves"):
@@ -388,7 +429,7 @@ async def get_tools_user_valves_spec_by_id(
if id in request.app.state.TOOLS:
tools_module = request.app.state.TOOLS[id]
else:
tools_module, _ = load_tools_module_by_id(id)
tools_module, _ = load_tool_module_by_id(id)
request.app.state.TOOLS[id] = tools_module
if hasattr(tools_module, "UserValves"):
@@ -412,7 +453,7 @@ async def update_tools_user_valves_by_id(
if id in request.app.state.TOOLS:
tools_module = request.app.state.TOOLS[id]
else:
tools_module, _ = load_tools_module_by_id(id)
tools_module, _ = load_tool_module_by_id(id)
request.app.state.TOOLS[id] = tools_module
if hasattr(tools_module, "UserValves"):
+2
View File
@@ -88,11 +88,13 @@ class ChatPermissions(BaseModel):
file_upload: bool = True
delete: bool = True
edit: bool = True
multiple_models: bool = True
temporary: bool = True
temporary_enforced: bool = False
class FeaturesPermissions(BaseModel):
direct_tool_servers: bool = False
web_search: bool = True
image_generation: bool = True
code_interpreter: bool = True
+5 -11
View File
@@ -9,9 +9,8 @@ from open_webui.models.users import Users, UserNameResponse
from open_webui.models.channels import Channels
from open_webui.models.chats import Chats
from open_webui.utils.redis import (
parse_redis_sentinel_url,
get_sentinels_from_env,
AsyncRedisSentinelManager,
get_sentinel_url_from_env,
)
from open_webui.env import (
@@ -38,15 +37,10 @@ log.setLevel(SRC_LOG_LEVELS["SOCKET"])
if WEBSOCKET_MANAGER == "redis":
if WEBSOCKET_SENTINEL_HOSTS:
redis_config = parse_redis_sentinel_url(WEBSOCKET_REDIS_URL)
mgr = AsyncRedisSentinelManager(
WEBSOCKET_SENTINEL_HOSTS.split(","),
sentinel_port=int(WEBSOCKET_SENTINEL_PORT),
redis_port=redis_config["port"],
service=redis_config["service"],
db=redis_config["db"],
username=redis_config["username"],
password=redis_config["password"],
mgr = socketio.AsyncRedisManager(
get_sentinel_url_from_env(
WEBSOCKET_REDIS_URL, WEBSOCKET_SENTINEL_HOSTS, WEBSOCKET_SENTINEL_PORT
)
)
else:
mgr = socketio.AsyncRedisManager(WEBSOCKET_REDIS_URL)
+24 -4
View File
@@ -5,16 +5,23 @@ from uuid import uuid4
# A dictionary to keep track of active tasks
tasks: Dict[str, asyncio.Task] = {}
chat_tasks = {}
def cleanup_task(task_id: str):
def cleanup_task(task_id: str, id=None):
"""
Remove a completed or canceled task from the global `tasks` dictionary.
"""
tasks.pop(task_id, None) # Remove the task if it exists
# If an ID is provided, remove the task from the chat_tasks dictionary
if id and task_id in chat_tasks.get(id, []):
chat_tasks[id].remove(task_id)
if not chat_tasks[id]: # If no tasks left for this ID, remove the entry
chat_tasks.pop(id, None)
def create_task(coroutine):
def create_task(coroutine, id=None):
"""
Create a new asyncio task and add it to the global task dictionary.
"""
@@ -22,9 +29,15 @@ def create_task(coroutine):
task = asyncio.create_task(coroutine) # Create the task
# Add a done callback for cleanup
task.add_done_callback(lambda t: cleanup_task(task_id))
task.add_done_callback(lambda t: cleanup_task(task_id, id))
tasks[task_id] = task
# If an ID is provided, associate the task with that ID
if chat_tasks.get(id):
chat_tasks[id].append(task_id)
else:
chat_tasks[id] = [task_id]
return task_id, task
@@ -42,6 +55,13 @@ def list_tasks():
return list(tasks.keys())
def list_task_ids_by_chat_id(id):
"""
List all tasks associated with a specific ID.
"""
return chat_tasks.get(id, [])
async def stop_task(task_id: str):
"""
Cancel a running task and remove it from the global task list.
+13 -4
View File
@@ -8,7 +8,9 @@ import requests
import os
from datetime import UTC, datetime, timedelta
from datetime import datetime, timedelta
import pytz
from pytz import UTC
from typing import Optional, Union, List, Dict
from open_webui.models.users import Users
@@ -141,12 +143,14 @@ def create_api_key():
return f"sk-{key}"
def get_http_authorization_cred(auth_header: str):
def get_http_authorization_cred(auth_header: Optional[str]):
if not auth_header:
return None
try:
scheme, credentials = auth_header.split(" ")
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
except Exception:
raise ValueError(ERROR_MESSAGES.INVALID_TOKEN)
return None
def get_current_user(
@@ -180,7 +184,12 @@ def get_current_user(
).split(",")
]
if request.url.path not in allowed_paths:
# Check if the request path matches any allowed endpoint.
if not any(
request.url.path == allowed
or request.url.path.startswith(allowed + "/")
for allowed in allowed_paths
):
raise HTTPException(
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.API_KEY_NOT_ALLOWED
)
+3 -3
View File
@@ -12,9 +12,9 @@ log.setLevel(SRC_LOG_LEVELS["MAIN"])
def get_sorted_filter_ids(model: dict):
def get_priority(function_id):
function = Functions.get_function_by_id(function_id)
if function is not None and hasattr(function, "valves"):
# TODO: Fix FunctionModel to include vavles
return (function.valves if function.valves else {}).get("priority", 0)
if function is not None:
valves = Functions.get_function_valves_by_id(function_id)
return valves.get("priority", 0) if valves else 0
return 0
filter_ids = [function.id for function in Functions.get_global_filter_functions()]
+130 -74
View File
@@ -221,53 +221,51 @@ async def chat_completion_tools_handler(
except Exception as e:
tool_result = str(e)
tool_result_files = []
if isinstance(tool_result, list):
for item in tool_result:
# check if string
if isinstance(item, str) and item.startswith("data:"):
tool_result_files.append(item)
tool_result.remove(item)
if isinstance(tool_result, dict) or isinstance(tool_result, list):
tool_result = json.dumps(tool_result, indent=2)
if isinstance(tool_result, str):
tool = tools[tool_function_name]
tool_id = tool.get("toolkit_id", "")
if tool.get("citation", False) or tool.get("direct", False):
tool_id = tool.get("tool_id", "")
tool_name = (
f"{tool_id}/{tool_function_name}"
if tool_id
else f"{tool_function_name}"
)
if tool.get("metadata", {}).get("citation", False) or tool.get(
"direct", False
):
# Citation is enabled for this tool
sources.append(
{
"source": {
"name": (
f"TOOL:" + f"{tool_id}/{tool_function_name}"
if tool_id
else f"{tool_function_name}"
),
"name": (f"TOOL:{tool_name}"),
},
"document": [tool_result],
"metadata": [
{
"source": (
f"TOOL:" + f"{tool_id}/{tool_function_name}"
if tool_id
else f"{tool_function_name}"
)
}
],
"metadata": [{"source": (f"TOOL:{tool_name}")}],
}
)
else:
sources.append(
{
"source": {},
"document": [tool_result],
"metadata": [
{
"source": (
f"TOOL:" + f"{tool_id}/{tool_function_name}"
if tool_id
else f"{tool_function_name}"
)
}
],
}
# Citation is not enabled for this tool
body["messages"] = add_or_update_user_message(
f"\nTool `{tool_name}` Output: {tool_result}",
body["messages"],
)
if tools[tool_function_name].get("file_handler", False):
if (
tools[tool_function_name]
.get("metadata", {})
.get("file_handler", False)
):
skip_files = True
# check if "tool_calls" in result
@@ -385,24 +383,44 @@ async def chat_web_search_handler(
all_results.append(results)
files = form_data.get("files", [])
if results.get("collection_name"):
files.append(
{
"collection_name": results["collection_name"],
"name": searchQuery,
"type": "web_search",
"urls": results["filenames"],
}
)
if results.get("collection_names"):
for col_idx, collection_name in enumerate(
results.get("collection_names")
):
files.append(
{
"collection_name": collection_name,
"name": searchQuery,
"type": "web_search",
"urls": [results["filenames"][col_idx]],
}
)
elif results.get("docs"):
files.append(
{
"docs": results.get("docs", []),
"name": searchQuery,
"type": "web_search",
"urls": results["filenames"],
}
)
# Invoked when bypass embedding and retrieval is set to True
docs = results["docs"]
if len(docs) == len(results["filenames"]):
# the number of docs and filenames (urls) should be the same
for doc_idx, doc in enumerate(docs):
files.append(
{
"docs": [doc],
"name": searchQuery,
"type": "web_search",
"urls": [results["filenames"][doc_idx]],
}
)
else:
# edge case when the number of docs and filenames (urls) are not the same
# this should not happen, but if it does, we will just append the docs
files.append(
{
"docs": results.get("docs", []),
"name": searchQuery,
"type": "web_search",
"urls": results["filenames"],
}
)
form_data["files"] = files
except Exception as e:
@@ -625,27 +643,28 @@ def apply_params_to_form_data(form_data, model):
if "keep_alive" in params:
form_data["keep_alive"] = params["keep_alive"]
else:
if "seed" in params:
if "seed" in params and params["seed"] is not None:
form_data["seed"] = params["seed"]
if "stop" in params:
if "stop" in params and params["stop"] is not None:
form_data["stop"] = params["stop"]
if "temperature" in params:
if "temperature" in params and params["temperature"] is not None:
form_data["temperature"] = params["temperature"]
if "max_tokens" in params:
if "max_tokens" in params and params["max_tokens"] is not None:
form_data["max_tokens"] = params["max_tokens"]
if "top_p" in params:
if "top_p" in params and params["top_p"] is not None:
form_data["top_p"] = params["top_p"]
if "frequency_penalty" in params:
if "frequency_penalty" in params and params["frequency_penalty"] is not None:
form_data["frequency_penalty"] = params["frequency_penalty"]
if "reasoning_effort" in params:
if "reasoning_effort" in params and params["reasoning_effort"] is not None:
form_data["reasoning_effort"] = params["reasoning_effort"]
if "logit_bias" in params:
if "logit_bias" in params and params["logit_bias"] is not None:
try:
form_data["logit_bias"] = json.loads(
convert_logit_bias_input_to_json(params["logit_bias"])
@@ -862,10 +881,16 @@ async def process_chat_payload(request, form_data, user, metadata, model):
# If context is not empty, insert it into the messages
if len(sources) > 0:
context_string = ""
for source_idx, source in enumerate(sources):
citated_file_idx = {}
for _, source in enumerate(sources, 1):
if "document" in source:
for doc_idx, doc_context in enumerate(source["document"]):
context_string += f"<source><source_id>{source_idx + 1}</source_id><source_context>{doc_context}</source_context></source>\n"
for doc_context, doc_meta in zip(
source["document"], source["metadata"]
):
file_id = doc_meta.get("file_id")
if file_id not in citated_file_idx:
citated_file_idx[file_id] = len(citated_file_idx) + 1
context_string += f'<source id="{citated_file_idx[file_id]}">{doc_context}</source>\n'
context_string = context_string.strip()
prompt = get_last_user_message(form_data["messages"])
@@ -1198,13 +1223,15 @@ async def process_chat_response(
)
tool_result = None
tool_result_files = None
for result in results:
if tool_call_id == result.get("tool_call_id", ""):
tool_result = result.get("content", None)
tool_result_files = result.get("files", None)
break
if tool_result:
tool_calls_display_content = f'{tool_calls_display_content}\n<details type="tool_calls" done="true" id="{tool_call_id}" name="{tool_name}" arguments="{html.escape(json.dumps(tool_arguments))}" result="{html.escape(json.dumps(tool_result))}">\n<summary>Tool Executed</summary>\n</details>'
tool_calls_display_content = f'{tool_calls_display_content}\n<details type="tool_calls" done="true" id="{tool_call_id}" name="{tool_name}" arguments="{html.escape(json.dumps(tool_arguments))}" result="{html.escape(json.dumps(tool_result))}" files="{html.escape(json.dumps(tool_result_files)) if tool_result_files else ""}">\n<summary>Tool Executed</summary>\n</details>\n'
else:
tool_calls_display_content = f'{tool_calls_display_content}\n<details type="tool_calls" done="false" id="{tool_call_id}" name="{tool_name}" arguments="{html.escape(json.dumps(tool_arguments))}">\n<summary>Executing...</summary>\n</details>'
@@ -1570,6 +1597,9 @@ async def process_chat_response(
)
if data:
if "event" in data:
await event_emitter(data.get("event", {}))
if "selected_model_id" in data:
model_id = data["selected_model_id"]
Chats.upsert_message_to_chat_by_id_and_message_id(
@@ -1614,14 +1644,27 @@ async def process_chat_response(
)
if tool_call_index is not None:
if (
len(response_tool_calls)
<= tool_call_index
):
# Check if the tool call already exists
current_response_tool_call = None
for (
response_tool_call
) in response_tool_calls:
if (
response_tool_call.get("index")
== tool_call_index
):
current_response_tool_call = (
response_tool_call
)
break
if current_response_tool_call is None:
# Add the new tool call
response_tool_calls.append(
delta_tool_call
)
else:
# Update the existing tool call
delta_name = delta_tool_call.get(
"function", {}
).get("name")
@@ -1632,16 +1675,14 @@ async def process_chat_response(
)
if delta_name:
response_tool_calls[
tool_call_index
]["function"][
"name"
] += delta_name
current_response_tool_call[
"function"
]["name"] += delta_name
if delta_arguments:
response_tool_calls[
tool_call_index
]["function"][
current_response_tool_call[
"function"
][
"arguments"
] += delta_arguments
@@ -1805,7 +1846,7 @@ async def process_chat_response(
await stream_body_handler(response)
MAX_TOOL_CALL_RETRIES = 5
MAX_TOOL_CALL_RETRIES = 10
tool_call_retries = 0
while len(tool_calls) > 0 and tool_call_retries < MAX_TOOL_CALL_RETRIES:
@@ -1898,6 +1939,14 @@ async def process_chat_response(
except Exception as e:
tool_result = str(e)
tool_result_files = []
if isinstance(tool_result, list):
for item in tool_result:
# check if string
if isinstance(item, str) and item.startswith("data:"):
tool_result_files.append(item)
tool_result.remove(item)
if isinstance(tool_result, dict) or isinstance(
tool_result, list
):
@@ -1907,6 +1956,11 @@ async def process_chat_response(
{
"tool_call_id": tool_call_id,
"content": tool_result,
**(
{"files": tool_result_files}
if tool_result_files
else {}
),
}
)
@@ -2191,7 +2245,9 @@ async def process_chat_response(
await response.background()
# background_tasks.add_task(post_response_handler, response, events)
task_id, _ = create_task(post_response_handler(response, events))
task_id, _ = create_task(
post_response_handler(response, events), id=metadata["chat_id"]
)
return {"status": True, "task_id": task_id}
else:
+6 -3
View File
@@ -114,9 +114,12 @@ async def get_all_models(request, user: UserModel = None):
for custom_model in custom_models:
if custom_model.base_model_id is None:
for model in models:
if (
custom_model.id == model["id"]
or custom_model.id == model["id"].split(":")[0]
if custom_model.id == model["id"] or (
model.get("owned_by") == "ollama"
and custom_model.id
== model["id"].split(":")[
0
] # Ollama may return model ids in different formats (e.g., 'llama3' vs. 'llama3:7b')
):
if custom_model.is_active:
model["name"] = custom_model.name
+38 -33
View File
@@ -326,40 +326,45 @@ class OAuthManager:
raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
picture_claim = auth_manager_config.OAUTH_PICTURE_CLAIM
picture_url = user_data.get(
picture_claim, OAUTH_PROVIDERS[provider].get("picture_url", "")
)
if picture_url:
# Download the profile image into a base64 string
try:
access_token = token.get("access_token")
get_kwargs = {}
if access_token:
get_kwargs["headers"] = {
"Authorization": f"Bearer {access_token}",
}
async with aiohttp.ClientSession() as session:
async with session.get(picture_url, **get_kwargs) as resp:
if resp.ok:
picture = await resp.read()
base64_encoded_picture = base64.b64encode(
picture
).decode("utf-8")
guessed_mime_type = mimetypes.guess_type(
picture_url
)[0]
if guessed_mime_type is None:
# assume JPG, browsers are tolerant enough of image formats
guessed_mime_type = "image/jpeg"
picture_url = f"data:{guessed_mime_type};base64,{base64_encoded_picture}"
else:
picture_url = "/user.png"
except Exception as e:
log.error(
f"Error downloading profile image '{picture_url}': {e}"
)
if picture_claim:
picture_url = user_data.get(
picture_claim, OAUTH_PROVIDERS[provider].get("picture_url", "")
)
if picture_url:
# Download the profile image into a base64 string
try:
access_token = token.get("access_token")
get_kwargs = {}
if access_token:
get_kwargs["headers"] = {
"Authorization": f"Bearer {access_token}",
}
async with aiohttp.ClientSession() as session:
async with session.get(
picture_url, **get_kwargs
) as resp:
if resp.ok:
picture = await resp.read()
base64_encoded_picture = base64.b64encode(
picture
).decode("utf-8")
guessed_mime_type = mimetypes.guess_type(
picture_url
)[0]
if guessed_mime_type is None:
# assume JPG, browsers are tolerant enough of image formats
guessed_mime_type = "image/jpeg"
picture_url = f"data:{guessed_mime_type};base64,{base64_encoded_picture}"
else:
picture_url = "/user.png"
except Exception as e:
log.error(
f"Error downloading profile image '{picture_url}': {e}"
)
picture_url = "/user.png"
if not picture_url:
picture_url = "/user.png"
if not picture_url:
else:
picture_url = "/user.png"
username_claim = auth_manager_config.OAUTH_USERNAME_CLAIM
+6 -6
View File
@@ -68,23 +68,23 @@ def replace_imports(content):
return content
def load_tools_module_by_id(toolkit_id, content=None):
def load_tool_module_by_id(tool_id, content=None):
if content is None:
tool = Tools.get_tool_by_id(toolkit_id)
tool = Tools.get_tool_by_id(tool_id)
if not tool:
raise Exception(f"Toolkit not found: {toolkit_id}")
raise Exception(f"Toolkit not found: {tool_id}")
content = tool.content
content = replace_imports(content)
Tools.update_tool_by_id(toolkit_id, {"content": content})
Tools.update_tool_by_id(tool_id, {"content": content})
else:
frontmatter = extract_frontmatter(content)
# Install required packages found within the frontmatter
install_frontmatter_requirements(frontmatter.get("requirements", ""))
module_name = f"tool_{toolkit_id}"
module_name = f"tool_{tool_id}"
module = types.ModuleType(module_name)
sys.modules[module_name] = module
@@ -108,7 +108,7 @@ def load_tools_module_by_id(toolkit_id, content=None):
else:
raise Exception("No Tools class found in the module")
except Exception as e:
log.error(f"Error loading module: {toolkit_id}: {e}")
log.error(f"Error loading module: {tool_id}: {e}")
del sys.modules[module_name] # Clean up
raise e
finally:
+13 -64
View File
@@ -4,7 +4,7 @@ from redis import asyncio as aioredis
from urllib.parse import urlparse
def parse_redis_sentinel_url(redis_url):
def parse_redis_service_url(redis_url):
parsed_url = urlparse(redis_url)
if parsed_url.scheme != "redis":
raise ValueError("Invalid Redis URL scheme. Must be 'redis'.")
@@ -20,7 +20,7 @@ def parse_redis_sentinel_url(redis_url):
def get_redis_connection(redis_url, redis_sentinels, decode_responses=True):
if redis_sentinels:
redis_config = parse_redis_sentinel_url(redis_url)
redis_config = parse_redis_service_url(redis_url)
sentinel = redis.sentinel.Sentinel(
redis_sentinels,
port=redis_config["port"],
@@ -45,65 +45,14 @@ def get_sentinels_from_env(sentinel_hosts_env, sentinel_port_env):
return []
class AsyncRedisSentinelManager(socketio.AsyncRedisManager):
def __init__(
self,
sentinel_hosts,
sentinel_port=26379,
redis_port=6379,
service="mymaster",
db=0,
username=None,
password=None,
channel="socketio",
write_only=False,
logger=None,
redis_options=None,
):
"""
Initialize the Redis Sentinel Manager.
This implementation mostly replicates the __init__ of AsyncRedisManager and
overrides _redis_connect() with a version that uses Redis Sentinel
:param sentinel_hosts: List of Sentinel hosts
:param sentinel_port: Sentinel Port
:param redis_port: Redis Port (currently unsupported by aioredis!)
:param service: Master service name in Sentinel
:param db: Redis database to use
:param username: Redis username (if any) (currently unsupported by aioredis!)
:param password: Redis password (if any)
:param channel: The channel name on which the server sends and receives
notifications. Must be the same in all the servers.
:param write_only: If set to ``True``, only initialize to emit events. The
default of ``False`` initializes the class for emitting
and receiving.
:param redis_options: additional keyword arguments to be passed to
``aioredis.from_url()``.
"""
self._sentinels = [(host, sentinel_port) for host in sentinel_hosts]
self._redis_port = redis_port
self._service = service
self._db = db
self._username = username
self._password = password
self._channel = channel
self.redis_options = redis_options or {}
# connect and call grandparent constructor
self._redis_connect()
super(socketio.AsyncRedisManager, self).__init__(
channel=channel, write_only=write_only, logger=logger
)
def _redis_connect(self):
"""Establish connections to Redis through Sentinel."""
sentinel = aioredis.sentinel.Sentinel(
self._sentinels,
port=self._redis_port,
db=self._db,
password=self._password,
**self.redis_options,
)
self.redis = sentinel.master_for(self._service)
self.pubsub = self.redis.pubsub(ignore_subscribe_messages=True)
def get_sentinel_url_from_env(redis_url, sentinel_hosts_env, sentinel_port_env):
redis_config = parse_redis_service_url(redis_url)
username = redis_config["username"] or ""
password = redis_config["password"] or ""
auth_part = ""
if username or password:
auth_part = f"{username}:{password}@"
hosts_part = ",".join(
f"{host}:{sentinel_port_env}" for host in sentinel_hosts_env.split(",")
)
return f"redis+sentinel://{auth_part}{hosts_part}/{redis_config['db']}/{redis_config['service']}"
+2
View File
@@ -152,6 +152,8 @@ def rag_template(template: str, context: str, query: str):
if template.strip() == "":
template = DEFAULT_RAG_TEMPLATE
template = prompt_template(template)
if "[context]" not in template and "{{CONTEXT}}" not in template:
log.debug(
"WARNING: The RAG template does not contain the '[context]' or '{{CONTEXT}}' placeholder."
+457 -79
View File
@@ -2,111 +2,203 @@ import inspect
import logging
import re
import inspect
import uuid
import aiohttp
import asyncio
import yaml
from typing import Any, Awaitable, Callable, get_type_hints
from pydantic import BaseModel
from pydantic.fields import FieldInfo
from typing import (
Any,
Awaitable,
Callable,
get_type_hints,
get_args,
get_origin,
Dict,
List,
Tuple,
Union,
Optional,
Type,
)
from functools import update_wrapper, partial
from fastapi import Request
from pydantic import BaseModel, Field, create_model
from langchain_core.utils.function_calling import convert_to_openai_function
from langchain_core.utils.function_calling import (
convert_to_openai_function as convert_pydantic_model_to_openai_function_spec,
)
from open_webui.models.tools import Tools
from open_webui.models.users import UserModel
from open_webui.utils.plugin import load_tools_module_by_id
from open_webui.utils.plugin import load_tool_module_by_id
from open_webui.env import AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA
import copy
log = logging.getLogger(__name__)
def apply_extra_params_to_tool_function(
def get_async_tool_function_and_apply_extra_params(
function: Callable, extra_params: dict
) -> Callable[..., Awaitable]:
sig = inspect.signature(function)
extra_params = {k: v for k, v in extra_params.items() if k in sig.parameters}
partial_func = partial(function, **extra_params)
if inspect.iscoroutinefunction(function):
update_wrapper(partial_func, function)
return partial_func
else:
# Make it a coroutine function
async def new_function(*args, **kwargs):
return partial_func(*args, **kwargs)
async def new_function(*args, **kwargs):
return partial_func(*args, **kwargs)
update_wrapper(new_function, function)
return new_function
update_wrapper(new_function, function)
return new_function
# Mutation on extra_params
def get_tools(
request: Request, tool_ids: list[str], user: UserModel, extra_params: dict
) -> dict[str, dict]:
tools_dict = {}
for tool_id in tool_ids:
tools = Tools.get_tool_by_id(tool_id)
if tools is None:
continue
tool = Tools.get_tool_by_id(tool_id)
if tool is None:
if tool_id.startswith("server:"):
server_idx = int(tool_id.split(":")[1])
tool_server_connection = (
request.app.state.config.TOOL_SERVER_CONNECTIONS[server_idx]
)
tool_server_data = None
for server in request.app.state.TOOL_SERVERS:
if server["idx"] == server_idx:
tool_server_data = server
break
assert tool_server_data is not None
specs = tool_server_data.get("specs", [])
module = request.app.state.TOOLS.get(tool_id, None)
if module is None:
module, _ = load_tools_module_by_id(tool_id)
request.app.state.TOOLS[tool_id] = module
for spec in specs:
function_name = spec["name"]
extra_params["__id__"] = tool_id
if hasattr(module, "valves") and hasattr(module, "Valves"):
valves = Tools.get_tool_valves_by_id(tool_id) or {}
module.valves = module.Valves(**valves)
auth_type = tool_server_connection.get("auth_type", "bearer")
token = None
if hasattr(module, "UserValves"):
extra_params["__user__"]["valves"] = module.UserValves( # type: ignore
**Tools.get_user_valves_by_id_and_user_id(tool_id, user.id)
)
if auth_type == "bearer":
token = tool_server_connection.get("key", "")
elif auth_type == "session":
token = request.state.token.credentials
for spec in tools.specs:
# TODO: Fix hack for OpenAI API
# Some times breaks OpenAI but others don't. Leaving the comment
for val in spec.get("parameters", {}).get("properties", {}).values():
if val["type"] == "str":
val["type"] = "string"
def make_tool_function(function_name, token, tool_server_data):
async def tool_function(**kwargs):
print(
f"Executing tool function {function_name} with params: {kwargs}"
)
return await execute_tool_server(
token=token,
url=tool_server_data["url"],
name=function_name,
params=kwargs,
server_data=tool_server_data,
)
# Remove internal parameters
spec["parameters"]["properties"] = {
key: val
for key, val in spec["parameters"]["properties"].items()
if not key.startswith("__")
}
return tool_function
function_name = spec["name"]
tool_function = make_tool_function(
function_name, token, tool_server_data
)
# convert to function that takes only model params and inserts custom params
original_func = getattr(module, function_name)
callable = apply_extra_params_to_tool_function(original_func, extra_params)
callable = get_async_tool_function_and_apply_extra_params(
tool_function,
{},
)
if callable.__doc__ and callable.__doc__.strip() != "":
s = re.split(":(param|return)", callable.__doc__, 1)
spec["description"] = s[0]
tool_dict = {
"tool_id": tool_id,
"callable": callable,
"spec": spec,
}
# TODO: if collision, prepend toolkit name
if function_name in tools_dict:
log.warning(
f"Tool {function_name} already exists in another tools!"
)
log.warning(f"Discarding {tool_id}.{function_name}")
else:
tools_dict[function_name] = tool_dict
else:
spec["description"] = function_name
continue
else:
module = request.app.state.TOOLS.get(tool_id, None)
if module is None:
module, _ = load_tool_module_by_id(tool_id)
request.app.state.TOOLS[tool_id] = module
# TODO: This needs to be a pydantic model
tool_dict = {
"spec": spec,
"callable": callable,
"toolkit_id": tool_id,
"pydantic_model": function_to_pydantic_model(callable),
# Misc info
"file_handler": hasattr(module, "file_handler") and module.file_handler,
"citation": hasattr(module, "citation") and module.citation,
}
extra_params["__id__"] = tool_id
# TODO: if collision, prepend toolkit name
if function_name in tools_dict:
log.warning(f"Tool {function_name} already exists in another tools!")
log.warning(f"Collision between {tools} and {tool_id}.")
log.warning(f"Discarding {tools}.{function_name}")
else:
tools_dict[function_name] = tool_dict
# Set valves for the tool
if hasattr(module, "valves") and hasattr(module, "Valves"):
valves = Tools.get_tool_valves_by_id(tool_id) or {}
module.valves = module.Valves(**valves)
if hasattr(module, "UserValves"):
extra_params["__user__"]["valves"] = module.UserValves( # type: ignore
**Tools.get_user_valves_by_id_and_user_id(tool_id, user.id)
)
for spec in tool.specs:
# TODO: Fix hack for OpenAI API
# Some times breaks OpenAI but others don't. Leaving the comment
for val in spec.get("parameters", {}).get("properties", {}).values():
if val["type"] == "str":
val["type"] = "string"
# Remove internal reserved parameters (e.g. __id__, __user__)
spec["parameters"]["properties"] = {
key: val
for key, val in spec["parameters"]["properties"].items()
if not key.startswith("__")
}
# convert to function that takes only model params and inserts custom params
function_name = spec["name"]
tool_function = getattr(module, function_name)
callable = get_async_tool_function_and_apply_extra_params(
tool_function, extra_params
)
# TODO: Support Pydantic models as parameters
if callable.__doc__ and callable.__doc__.strip() != "":
s = re.split(":(param|return)", callable.__doc__, 1)
spec["description"] = s[0]
else:
spec["description"] = function_name
tool_dict = {
"tool_id": tool_id,
"callable": callable,
"spec": spec,
# Misc info
"metadata": {
"file_handler": hasattr(module, "file_handler")
and module.file_handler,
"citation": hasattr(module, "citation") and module.citation,
},
}
# TODO: if collision, prepend toolkit name
if function_name in tools_dict:
log.warning(
f"Tool {function_name} already exists in another tools!"
)
log.warning(f"Discarding {tool_id}.{function_name}")
else:
tools_dict[function_name] = tool_dict
return tools_dict
@@ -166,7 +258,7 @@ def parse_docstring(docstring):
return param_descriptions
def function_to_pydantic_model(func: Callable) -> type[BaseModel]:
def convert_function_to_pydantic_model(func: Callable) -> type[BaseModel]:
"""
Converts a Python function's type hints and docstring to a Pydantic model,
including support for nested types, default values, and descriptions.
@@ -183,37 +275,323 @@ def function_to_pydantic_model(func: Callable) -> type[BaseModel]:
parameters = signature.parameters
docstring = func.__doc__
descriptions = parse_docstring(docstring)
tool_description = parse_description(docstring)
description = parse_description(docstring)
function_descriptions = parse_docstring(docstring)
field_defs = {}
for name, param in parameters.items():
type_hint = type_hints.get(name, Any)
default_value = param.default if param.default is not param.empty else ...
description = descriptions.get(name, None)
if not description:
description = function_descriptions.get(name, None)
if description:
field_defs[name] = type_hint, Field(default_value, description=description)
else:
field_defs[name] = type_hint, default_value
continue
field_defs[name] = type_hint, Field(default_value, description=description)
model = create_model(func.__name__, **field_defs)
model.__doc__ = tool_description
model.__doc__ = description
return model
def get_callable_attributes(tool: object) -> list[Callable]:
def get_functions_from_tool(tool: object) -> list[Callable]:
return [
getattr(tool, func)
for func in dir(tool)
if callable(getattr(tool, func))
and not func.startswith("__")
and not inspect.isclass(getattr(tool, func))
if callable(
getattr(tool, func)
) # checks if the attribute is callable (a method or function).
and not func.startswith(
"__"
) # filters out special (dunder) methods like init, str, etc. — these are usually built-in functions of an object that you might not need to use directly.
and not inspect.isclass(
getattr(tool, func)
) # ensures that the callable is not a class itself, just a method or function.
]
def get_tools_specs(tool_class: object) -> list[dict]:
function_list = get_callable_attributes(tool_class)
models = map(function_to_pydantic_model, function_list)
return [convert_to_openai_function(tool) for tool in models]
def get_tool_specs(tool_module: object) -> list[dict]:
function_models = map(
convert_function_to_pydantic_model, get_functions_from_tool(tool_module)
)
specs = [
convert_pydantic_model_to_openai_function_spec(function_model)
for function_model in function_models
]
return specs
def resolve_schema(schema, components):
"""
Recursively resolves a JSON schema using OpenAPI components.
"""
if not schema:
return {}
if "$ref" in schema:
ref_path = schema["$ref"]
ref_parts = ref_path.strip("#/").split("/")
resolved = components
for part in ref_parts[1:]: # Skip the initial 'components'
resolved = resolved.get(part, {})
return resolve_schema(resolved, components)
resolved_schema = copy.deepcopy(schema)
# Recursively resolve inner schemas
if "properties" in resolved_schema:
for prop, prop_schema in resolved_schema["properties"].items():
resolved_schema["properties"][prop] = resolve_schema(
prop_schema, components
)
if "items" in resolved_schema:
resolved_schema["items"] = resolve_schema(resolved_schema["items"], components)
return resolved_schema
def convert_openapi_to_tool_payload(openapi_spec):
"""
Converts an OpenAPI specification into a custom tool payload structure.
Args:
openapi_spec (dict): The OpenAPI specification as a Python dict.
Returns:
list: A list of tool payloads.
"""
tool_payload = []
for path, methods in openapi_spec.get("paths", {}).items():
for method, operation in methods.items():
tool = {
"type": "function",
"name": operation.get("operationId"),
"description": operation.get(
"description", operation.get("summary", "No description available.")
),
"parameters": {"type": "object", "properties": {}, "required": []},
}
# Extract path and query parameters
for param in operation.get("parameters", []):
param_name = param["name"]
param_schema = param.get("schema", {})
tool["parameters"]["properties"][param_name] = {
"type": param_schema.get("type"),
"description": param_schema.get("description", ""),
}
if param.get("required"):
tool["parameters"]["required"].append(param_name)
# Extract and resolve requestBody if available
request_body = operation.get("requestBody")
if request_body:
content = request_body.get("content", {})
json_schema = content.get("application/json", {}).get("schema")
if json_schema:
resolved_schema = resolve_schema(
json_schema, openapi_spec.get("components", {})
)
if resolved_schema.get("properties"):
tool["parameters"]["properties"].update(
resolved_schema["properties"]
)
if "required" in resolved_schema:
tool["parameters"]["required"] = list(
set(
tool["parameters"]["required"]
+ resolved_schema["required"]
)
)
elif resolved_schema.get("type") == "array":
tool["parameters"] = resolved_schema # special case for array
tool_payload.append(tool)
return tool_payload
async def get_tool_server_data(token: str, url: str) -> Dict[str, Any]:
headers = {
"Accept": "application/json",
"Content-Type": "application/json",
}
if token:
headers["Authorization"] = f"Bearer {token}"
error = None
try:
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(url, headers=headers) as response:
if response.status != 200:
error_body = await response.json()
raise Exception(error_body)
# Check if URL ends with .yaml or .yml to determine format
if url.lower().endswith((".yaml", ".yml")):
text_content = await response.text()
res = yaml.safe_load(text_content)
else:
res = await response.json()
except Exception as err:
log.exception(f"Could not fetch tool server spec from {url}")
if isinstance(err, dict) and "detail" in err:
error = err["detail"]
else:
error = str(err)
raise Exception(error)
data = {
"openapi": res,
"info": res.get("info", {}),
"specs": convert_openapi_to_tool_payload(res),
}
print("Fetched data:", data)
return data
async def get_tool_servers_data(
servers: List[Dict[str, Any]], session_token: Optional[str] = None
) -> List[Dict[str, Any]]:
# Prepare list of enabled servers along with their original index
server_entries = []
for idx, server in enumerate(servers):
if server.get("config", {}).get("enable"):
url_path = server.get("path", "openapi.json")
full_url = f"{server.get('url')}/{url_path}"
auth_type = server.get("auth_type", "bearer")
token = None
if auth_type == "bearer":
token = server.get("key", "")
elif auth_type == "session":
token = session_token
server_entries.append((idx, server, full_url, token))
# Create async tasks to fetch data
tasks = [get_tool_server_data(token, url) for (_, _, url, token) in server_entries]
# Execute tasks concurrently
responses = await asyncio.gather(*tasks, return_exceptions=True)
# Build final results with index and server metadata
results = []
for (idx, server, url, _), response in zip(server_entries, responses):
if isinstance(response, Exception):
print(f"Failed to connect to {url} OpenAPI tool server")
continue
results.append(
{
"idx": idx,
"url": server.get("url"),
"openapi": response.get("openapi"),
"info": response.get("info"),
"specs": response.get("specs"),
}
)
return results
async def execute_tool_server(
token: str, url: str, name: str, params: Dict[str, Any], server_data: Dict[str, Any]
) -> Any:
error = None
try:
openapi = server_data.get("openapi", {})
paths = openapi.get("paths", {})
matching_route = None
for route_path, methods in paths.items():
for http_method, operation in methods.items():
if isinstance(operation, dict) and operation.get("operationId") == name:
matching_route = (route_path, methods)
break
if matching_route:
break
if not matching_route:
raise Exception(f"No matching route found for operationId: {name}")
route_path, methods = matching_route
method_entry = None
for http_method, operation in methods.items():
if operation.get("operationId") == name:
method_entry = (http_method.lower(), operation)
break
if not method_entry:
raise Exception(f"No matching method found for operationId: {name}")
http_method, operation = method_entry
path_params = {}
query_params = {}
body_params = {}
for param in operation.get("parameters", []):
param_name = param["name"]
param_in = param["in"]
if param_name in params:
if param_in == "path":
path_params[param_name] = params[param_name]
elif param_in == "query":
query_params[param_name] = params[param_name]
final_url = f"{url}{route_path}"
for key, value in path_params.items():
final_url = final_url.replace(f"{{{key}}}", str(value))
if query_params:
query_string = "&".join(f"{k}={v}" for k, v in query_params.items())
final_url = f"{final_url}?{query_string}"
if operation.get("requestBody", {}).get("content"):
if params:
body_params = params
else:
raise Exception(
f"Request body expected for operation '{name}' but none found."
)
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
async with aiohttp.ClientSession() as session:
request_method = getattr(session, http_method.lower())
if http_method in ["post", "put", "patch"]:
async with request_method(
final_url, json=body_params, headers=headers
) as response:
if response.status >= 400:
text = await response.text()
raise Exception(f"HTTP error {response.status}: {text}")
return await response.json()
else:
async with request_method(final_url, headers=headers) as response:
if response.status >= 400:
text = await response.text()
raise Exception(f"HTTP error {response.status}: {text}")
return await response.json()
except Exception as err:
error = str(err)
print("API Request Error:", error)
return {"error": error}
+26 -20
View File
@@ -1,9 +1,9 @@
fastapi==0.115.7
uvicorn[standard]==0.34.0
pydantic==2.10.6
python-multipart==0.0.18
python-multipart==0.0.20
python-socketio==5.11.3
python-socketio==5.13.0
python-jose==3.4.0
passlib[bcrypt]==1.7.4
@@ -18,7 +18,7 @@ alembic==1.14.0
peewee==3.17.9
peewee-migrate==1.12.2
psycopg2-binary==2.9.9
pgvector==0.3.5
pgvector==0.4.0
PyMySQL==1.1.1
bcrypt==4.3.0
@@ -44,7 +44,7 @@ langchain==0.3.19
langchain-community==0.3.18
fake-useragent==2.1.0
chromadb==0.6.2
chromadb==0.6.3
pymilvus==2.5.0
qdrant-client~=1.12.0
opensearch-py==2.8.0
@@ -54,8 +54,9 @@ elasticsearch==8.17.1
transformers
sentence-transformers==3.3.1
accelerate
colbert-ai==0.2.21
einops==0.8.0
einops==0.8.1
ftfy==6.2.3
@@ -67,7 +68,7 @@ python-pptx==1.0.0
unstructured==0.16.17
nltk==3.9.1
Markdown==3.7
pypandoc==1.13
pypandoc==1.15
pandas==2.2.3
openpyxl==3.1.5
pyxlsb==1.0.10
@@ -83,6 +84,8 @@ opencv-python-headless==4.11.0.86
rapidocr-onnxruntime==1.3.24
rank-bm25==0.2.2
onnxruntime==1.20.1
faster-whisper==1.1.1
PyJWT[crypto]==2.10.1
@@ -90,12 +93,12 @@ authlib==1.4.1
black==25.1.0
langfuse==2.44.0
youtube-transcript-api==0.6.3
youtube-transcript-api==1.0.3
pytube==15.0.0
extract_msg
pydub
duckduckgo-search~=7.3.2
duckduckgo-search~=8.0.0
## Google Drive
google-api-python-client
@@ -110,7 +113,7 @@ pytest-docker~=3.1.1
googleapis-common-protos==1.63.2
google-cloud-storage==2.19.0
azure-identity==1.20.0
azure-identity==1.21.0
azure-storage-blob==12.24.1
@@ -120,15 +123,18 @@ ldap3==2.9.1
## Firecrawl
firecrawl-py==1.12.0
# Sougou API SDK(Tencentcloud SDK)
tencentcloud-sdk-python==3.0.1336
## Trace
opentelemetry-api==1.30.0
opentelemetry-sdk==1.30.0
opentelemetry-exporter-otlp==1.30.0
opentelemetry-instrumentation==0.51b0
opentelemetry-instrumentation-fastapi==0.51b0
opentelemetry-instrumentation-sqlalchemy==0.51b0
opentelemetry-instrumentation-redis==0.51b0
opentelemetry-instrumentation-requests==0.51b0
opentelemetry-instrumentation-logging==0.51b0
opentelemetry-instrumentation-httpx==0.51b0
opentelemetry-instrumentation-aiohttp-client==0.51b0
opentelemetry-api==1.31.1
opentelemetry-sdk==1.31.1
opentelemetry-exporter-otlp==1.31.1
opentelemetry-instrumentation==0.52b1
opentelemetry-instrumentation-fastapi==0.52b1
opentelemetry-instrumentation-sqlalchemy==0.52b1
opentelemetry-instrumentation-redis==0.52b1
opentelemetry-instrumentation-requests==0.52b1
opentelemetry-instrumentation-logging==0.52b1
opentelemetry-instrumentation-httpx==0.52b1
opentelemetry-instrumentation-aiohttp-client==0.52b1
+3 -3
View File
@@ -4,8 +4,8 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
cd "$SCRIPT_DIR" || exit
# Add conditional Playwright browser installation
if [[ "${RAG_WEB_LOADER_ENGINE,,}" == "playwright" ]]; then
if [[ -z "${PLAYWRIGHT_WS_URI}" ]]; then
if [[ "${WEB_LOADER_ENGINE,,}" == "playwright" ]]; then
if [[ -z "${PLAYWRIGHT_WS_URL}" ]]; then
echo "Installing Playwright browsers..."
playwright install chromium
playwright install-deps chromium
@@ -65,4 +65,4 @@ if [ -n "$SPACE_ID" ]; then
export WEBUI_URL=${SPACE_HOST}
fi
WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec uvicorn open_webui.main:app --host "$HOST" --port "$PORT" --forwarded-allow-ips '*'
WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec uvicorn open_webui.main:app --host "$HOST" --port "$PORT" --forwarded-allow-ips '*' --workers "${UVICORN_WORKERS:-1}"
+4 -3
View File
@@ -7,8 +7,8 @@ SET "SCRIPT_DIR=%~dp0"
cd /d "%SCRIPT_DIR%" || exit /b
:: Add conditional Playwright browser installation
IF /I "%RAG_WEB_LOADER_ENGINE%" == "playwright" (
IF "%PLAYWRIGHT_WS_URI%" == "" (
IF /I "%WEB_LOADER_ENGINE%" == "playwright" (
IF "%PLAYWRIGHT_WS_URL%" == "" (
echo Installing Playwright browsers...
playwright install chromium
playwright install-deps chromium
@@ -41,5 +41,6 @@ IF "%WEBUI_SECRET_KEY%%WEBUI_JWT_SECRET_KEY%" == " " (
:: Execute uvicorn
SET "WEBUI_SECRET_KEY=%WEBUI_SECRET_KEY%"
uvicorn open_webui.main:app --host "%HOST%" --port "%PORT%" --forwarded-allow-ips '*' --ws auto
IF "%UVICORN_WORKERS%"=="" SET UVICORN_WORKERS=1
uvicorn open_webui.main:app --host "%HOST%" --port "%PORT%" --forwarded-allow-ips '*' --workers %UVICORN_WORKERS% --ws auto
:: For ssl user uvicorn open_webui.main:app --host "%HOST%" --port "%PORT%" --forwarded-allow-ips '*' --ssl-keyfile "key.pem" --ssl-certfile "cert.pem" --ws auto
+2 -2
View File
@@ -6,5 +6,5 @@ services:
open-webui:
environment:
- 'RAG_WEB_LOADER_ENGINE=playwright'
- 'PLAYWRIGHT_WS_URI=ws://playwright:3000'
- 'WEB_LOADER_ENGINE=playwright'
- 'PLAYWRIGHT_WS_URL=ws://playwright:3000'
+62 -28
View File
@@ -1,12 +1,12 @@
{
"name": "open-webui",
"version": "0.6.0",
"version": "0.6.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "open-webui",
"version": "0.6.0",
"version": "0.6.4",
"dependencies": {
"@azure/msal-browser": "^4.5.0",
"@codemirror/lang-javascript": "^6.2.2",
@@ -23,12 +23,13 @@
"@tiptap/extension-highlight": "^2.10.0",
"@tiptap/extension-placeholder": "^2.10.0",
"@tiptap/extension-typography": "^2.10.0",
"@tiptap/pm": "^2.10.0",
"@tiptap/pm": "^2.11.7",
"@tiptap/starter-kit": "^2.10.0",
"@xyflow/svelte": "^0.1.19",
"async": "^3.2.5",
"bits-ui": "^0.19.7",
"codemirror": "^6.0.1",
"codemirror-lang-elixir": "^4.0.0",
"codemirror-lang-hcl": "^0.0.0-beta.2",
"crc-32": "^1.2.2",
"dayjs": "^1.11.10",
@@ -69,7 +70,8 @@
"turndown": "^7.2.0",
"undici": "^7.3.0",
"uuid": "^9.0.1",
"vite-plugin-static-copy": "^2.2.0"
"vite-plugin-static-copy": "^2.2.0",
"yaml": "^2.7.1"
},
"devDependencies": {
"@sveltejs/adapter-auto": "3.2.2",
@@ -3042,9 +3044,9 @@
}
},
"node_modules/@tiptap/pm": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.10.0.tgz",
"integrity": "sha512-ohshlWf4MlW6D3rQkNQnhmiQ2w4pwRoQcJmTPt8UJoIDGkeKmZh494fQp4Aeh80XuGd81SsCv//1HJeyaeHJYQ==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.11.7.tgz",
"integrity": "sha512-7gEEfz2Q6bYKXM07vzLUD0vqXFhC5geWRA6LCozTiLdVFDdHWiBrvb2rtkL5T7mfLq03zc1QhH7rI3F6VntOEA==",
"license": "MIT",
"dependencies": {
"prosemirror-changeset": "^2.2.1",
@@ -3061,10 +3063,10 @@
"prosemirror-schema-basic": "^1.2.3",
"prosemirror-schema-list": "^1.4.1",
"prosemirror-state": "^1.4.3",
"prosemirror-tables": "^1.6.1",
"prosemirror-tables": "^1.6.4",
"prosemirror-trailing-node": "^3.0.0",
"prosemirror-transform": "^1.10.2",
"prosemirror-view": "^1.36.0"
"prosemirror-view": "^1.37.0"
},
"funding": {
"type": "github",
@@ -4625,6 +4627,15 @@
"@codemirror/view": "^6.0.0"
}
},
"node_modules/codemirror-lang-elixir": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/codemirror-lang-elixir/-/codemirror-lang-elixir-4.0.0.tgz",
"integrity": "sha512-mzFesxo/t6KOxwnkqVd34R/q7yk+sMtHh6vUKGAvjwHmpL7bERHB+vQAsmU/nqrndkwVeJEHWGw/z/ybfdiudA==",
"dependencies": {
"@codemirror/language": "^6.0.0",
"lezer-elixir": "^1.0.0"
}
},
"node_modules/codemirror-lang-hcl": {
"version": "0.0.0-beta.2",
"resolved": "https://registry.npmjs.org/codemirror-lang-hcl/-/codemirror-lang-hcl-0.0.0-beta.2.tgz",
@@ -7611,6 +7622,15 @@
"node": ">= 0.8.0"
}
},
"node_modules/lezer-elixir": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/lezer-elixir/-/lezer-elixir-1.1.2.tgz",
"integrity": "sha512-K3yPMJcNhqCL6ugr5NkgOC1g37rcOM38XZezO9lBXy0LwWFd8zdWXfmRbY829vZVk0OGCQoI02yDWp9FF2OWZA==",
"dependencies": {
"@lezer/highlight": "^1.2.0",
"@lezer/lr": "^1.3.0"
}
},
"node_modules/lightningcss": {
"version": "1.29.1",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.1.tgz",
@@ -9495,6 +9515,16 @@
"node": ">=10"
}
},
"node_modules/postcss-load-config/node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">= 6"
}
},
"node_modules/postcss-safe-parser": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz",
@@ -9783,9 +9813,10 @@
}
},
"node_modules/prosemirror-model": {
"version": "1.23.0",
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.23.0.tgz",
"integrity": "sha512-Q/fgsgl/dlOAW9ILu4OOhYWQbc7TQd4BwKH/RwmUjyVf8682Be4zj3rOYdLnYEcGzyg8LL9Q5IWYKD8tdToreQ==",
"version": "1.25.0",
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.0.tgz",
"integrity": "sha512-/8XUmxWf0pkj2BmtqZHYJipTBMHIdVjuvFzMvEoxrtyGNmfvdhBiRwYt/eFwy2wA9DtBW3RLqvZnjurEkHaFCw==",
"license": "MIT",
"dependencies": {
"orderedmap": "^2.0.0"
}
@@ -9819,16 +9850,16 @@
}
},
"node_modules/prosemirror-tables": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.6.1.tgz",
"integrity": "sha512-p8WRJNA96jaNQjhJolmbxTzd6M4huRE5xQ8OxjvMhQUP0Nzpo4zz6TztEiwk6aoqGBhz9lxRWR1yRZLlpQN98w==",
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.6.4.tgz",
"integrity": "sha512-TkDY3Gw52gRFRfRn2f4wJv5WOgAOXLJA2CQJYIJ5+kdFbfj3acR4JUW6LX2e1hiEBiUwvEhzH5a3cZ5YSztpIA==",
"license": "MIT",
"dependencies": {
"prosemirror-keymap": "^1.1.2",
"prosemirror-model": "^1.8.1",
"prosemirror-state": "^1.3.1",
"prosemirror-transform": "^1.2.1",
"prosemirror-view": "^1.13.3"
"prosemirror-keymap": "^1.2.2",
"prosemirror-model": "^1.24.1",
"prosemirror-state": "^1.4.3",
"prosemirror-transform": "^1.10.2",
"prosemirror-view": "^1.37.2"
}
},
"node_modules/prosemirror-trailing-node": {
@@ -9856,9 +9887,9 @@
}
},
"node_modules/prosemirror-view": {
"version": "1.36.0",
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.36.0.tgz",
"integrity": "sha512-U0GQd5yFvV5qUtT41X1zCQfbw14vkbbKwLlQXhdylEmgpYVHkefXYcC4HHwWOfZa3x6Y8wxDLUBv7dxN5XQ3nA==",
"version": "1.39.1",
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.39.1.tgz",
"integrity": "sha512-GhLxH1xwnqa5VjhJ29LfcQITNDp+f1jzmMPXQfGW9oNrF0lfjPzKvV5y/bjIQkyKpwCX3Fp+GA4dBpMMk8g+ZQ==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.20.0",
@@ -13011,12 +13042,15 @@
}
},
"node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"dev": true,
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz",
"integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 6"
"node": ">= 14"
}
},
"node_modules/yauzl": {
+5 -3
View File
@@ -1,6 +1,6 @@
{
"name": "open-webui",
"version": "0.6.0",
"version": "0.6.4",
"private": true,
"scripts": {
"dev": "npm run pyodide:fetch && vite dev --host",
@@ -66,13 +66,14 @@
"@tiptap/extension-highlight": "^2.10.0",
"@tiptap/extension-placeholder": "^2.10.0",
"@tiptap/extension-typography": "^2.10.0",
"@tiptap/pm": "^2.10.0",
"@tiptap/pm": "^2.11.7",
"@tiptap/starter-kit": "^2.10.0",
"@xyflow/svelte": "^0.1.19",
"async": "^3.2.5",
"bits-ui": "^0.19.7",
"codemirror": "^6.0.1",
"codemirror-lang-hcl": "^0.0.0-beta.2",
"codemirror-lang-elixir": "^4.0.0",
"crc-32": "^1.2.2",
"dayjs": "^1.11.10",
"dompurify": "^3.1.6",
@@ -112,7 +113,8 @@
"turndown": "^7.2.0",
"undici": "^7.3.0",
"uuid": "^9.0.1",
"vite-plugin-static-copy": "^2.2.0"
"vite-plugin-static-copy": "^2.2.0",
"yaml": "^2.7.1"
},
"engines": {
"node": ">=18.13.0 <=22.x.x",
+17 -10
View File
@@ -9,9 +9,9 @@ dependencies = [
"fastapi==0.115.7",
"uvicorn[standard]==0.34.0",
"pydantic==2.10.6",
"python-multipart==0.0.18",
"python-multipart==0.0.20",
"python-socketio==5.11.3",
"python-socketio==5.13.0",
"python-jose==3.4.0",
"passlib[bcrypt]==1.7.4",
@@ -26,7 +26,7 @@ dependencies = [
"peewee==3.17.9",
"peewee-migrate==1.12.2",
"psycopg2-binary==2.9.9",
"pgvector==0.3.5",
"pgvector==0.4.0",
"PyMySQL==1.1.1",
"bcrypt==4.3.0",
@@ -45,14 +45,14 @@ dependencies = [
"openai",
"anthropic",
"google-generativeai==0.7.2",
"google-generativeai==0.8.4",
"tiktoken",
"langchain==0.3.19",
"langchain-community==0.3.18",
"fake-useragent==2.1.0",
"chromadb==0.6.2",
"chromadb==0.6.3",
"pymilvus==2.5.0",
"qdrant-client~=1.12.0",
"opensearch-py==2.8.0",
@@ -61,8 +61,9 @@ dependencies = [
"transformers",
"sentence-transformers==3.3.1",
"accelerate",
"colbert-ai==0.2.21",
"einops==0.8.0",
"einops==0.8.1",
"ftfy==6.2.3",
"pypdf==4.3.1",
@@ -73,7 +74,7 @@ dependencies = [
"unstructured==0.16.17",
"nltk==3.9.1",
"Markdown==3.7",
"pypandoc==1.13",
"pypandoc==1.15",
"pandas==2.2.3",
"openpyxl==3.1.5",
"pyxlsb==1.0.10",
@@ -89,6 +90,8 @@ dependencies = [
"rapidocr-onnxruntime==1.3.24",
"rank-bm25==0.2.2",
"onnxruntime==1.20.1",
"faster-whisper==1.1.1",
"PyJWT[crypto]==2.10.1",
@@ -96,12 +99,12 @@ dependencies = [
"black==25.1.0",
"langfuse==2.44.0",
"youtube-transcript-api==0.6.3",
"youtube-transcript-api==1.0.3",
"pytube==15.0.0",
"extract_msg",
"pydub",
"duckduckgo-search~=7.3.2",
"duckduckgo-search~=8.0.0",
"google-api-python-client",
"google-auth-httplib2",
@@ -110,7 +113,6 @@ dependencies = [
"docker~=7.1.0",
"pytest~=8.3.2",
"pytest-docker~=3.1.1",
"moto[s3]>=5.0.26",
"googleapis-common-protos==1.63.2",
"google-cloud-storage==2.19.0",
@@ -122,7 +124,12 @@ dependencies = [
"firecrawl-py==1.12.0",
"tencentcloud-sdk-python==3.0.1336",
"gcp-storage-emulator>=2024.8.3",
"moto[s3]>=5.0.26",
]
readme = "README.md"
requires-python = ">= 3.11, < 3.13.0a1"
+30 -4
View File
@@ -46,6 +46,14 @@ math {
@apply rounded-lg;
}
input::placeholder {
direction: auto;
}
textarea::placeholder {
direction: auto;
}
.input-prose {
@apply prose dark:prose-invert prose-headings:font-semibold prose-hr:my-4 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
}
@@ -83,10 +91,6 @@ math {
-webkit-app-region: no-drag;
}
iframe {
@apply rounded-lg;
}
li p {
display: inline;
}
@@ -214,6 +218,28 @@ input[type='number'] {
-moz-appearance: textfield; /* Firefox */
}
.katex-display {
@apply overflow-y-hidden overflow-x-auto max-w-full;
}
.katex-display::-webkit-scrollbar {
height: 0.4rem;
width: 0.4rem;
}
.katex-display:active::-webkit-scrollbar-thumb,
.katex-display:focus::-webkit-scrollbar-thumb,
.katex-display:hover::-webkit-scrollbar-thumb {
visibility: visible;
}
.katex-display::-webkit-scrollbar-thumb {
visibility: hidden;
}
.katex-display::-webkit-scrollbar-corner {
display: none;
}
.cm-editor {
height: 100%;
width: 100%;
+1 -1
View File
@@ -9,7 +9,7 @@
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-title" content="Open WebUI" />
<link rel="manifest" href="/manifest.json" />
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover"
+87
View File
@@ -115,6 +115,93 @@ export const setDirectConnectionsConfig = async (token: string, config: object)
return res;
};
export const getToolServerConnections = async (token: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/tool_servers`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res;
};
export const setToolServerConnections = async (token: string, connections: object) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/tool_servers`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
...connections
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res;
};
export const verifyToolServerConnection = async (token: string, connection: object) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/tool_servers/verify`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
...connection
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res;
};
export const getCodeExecutionConfig = async (token: string) => {
let error = null;
+47 -4
View File
@@ -2,6 +2,7 @@ import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
import { convertOpenApiToToolPayload } from '$lib/utils';
import { getOpenAIModelsDirect } from './openai';
import { parse } from 'yaml';
import { toast } from 'svelte-sonner';
export const getModels = async (
@@ -259,10 +260,10 @@ export const stopTask = async (token: string, id: string) => {
return res;
};
export const getToolServerData = async (token: string, url: string) => {
export const getTaskIdsByChatId = async (token: string, chat_id: string) => {
let error = null;
const res = await fetch(`${url}/openapi.json`, {
const res = await fetch(`${WEBUI_BASE_URL}/api/tasks/chat/${chat_id}`, {
method: 'GET',
headers: {
Accept: 'application/json',
@@ -288,6 +289,45 @@ export const getToolServerData = async (token: string, url: string) => {
throw error;
}
return res;
};
export const getToolServerData = async (token: string, url: string) => {
let error = null;
const res = await fetch(`${url}`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
}
})
.then(async (res) => {
// Check if URL ends with .yaml or .yml to determine format
if (url.toLowerCase().endsWith('.yaml') || url.toLowerCase().endsWith('.yml')) {
if (!res.ok) throw await res.text();
const text = await res.text();
return parse(text);
} else {
if (!res.ok) throw await res.json();
return res.json();
}
})
.catch((err) => {
console.log(err);
if ('detail' in err) {
error = err.detail;
} else {
error = err;
}
return null;
});
if (error) {
throw error;
}
const data = {
openapi: res,
info: res.info,
@@ -304,10 +344,13 @@ export const getToolServersData = async (i18n, servers: object[]) => {
servers
.filter((server) => server?.config?.enable)
.map(async (server) => {
const data = await getToolServerData(server?.key, server?.url).catch((err) => {
const data = await getToolServerData(
(server?.auth_type ?? 'bearer') === 'bearer' ? server?.key : localStorage.token,
server?.url + '/' + (server?.path ?? 'openapi.json')
).catch((err) => {
toast.error(
i18n.t(`Failed to connect to {{URL}} OpenAPI tool server`, {
URL: server?.url
URL: server?.url + '/' + (server?.path ?? 'openapi.json')
})
);
return null;
+28
View File
@@ -345,3 +345,31 @@ export const deleteKnowledgeById = async (token: string, id: string) => {
return res;
};
export const reindexKnowledgeFiles = async (token: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/reindex`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
+3 -30
View File
@@ -50,9 +50,9 @@ type YoutubeConfigForm = {
};
type RAGConfigForm = {
pdf_extract_images?: boolean;
enable_google_drive_integration?: boolean;
enable_onedrive_integration?: boolean;
PDF_EXTRACT_IMAGES?: boolean;
ENABLE_GOOGLE_DRIVE_INTEGRATION?: boolean;
ENABLE_ONEDRIVE_INTEGRATION?: boolean;
chunk?: ChunkConfigForm;
content_extraction?: ContentExtractConfigForm;
web_loader_ssl_verification?: boolean;
@@ -89,33 +89,6 @@ export const updateRAGConfig = async (token: string, payload: RAGConfigForm) =>
return res;
};
export const getRAGTemplate = async (token: string) => {
let error = null;
const res = await fetch(`${RETRIEVAL_API_BASE_URL}/template`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res?.template ?? '';
};
export const getQuerySettings = async (token: string) => {
let error = null;
+153 -21
View File
@@ -15,6 +15,9 @@
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Switch from '$lib/components/common/Switch.svelte';
import Tags from './common/Tags.svelte';
import { getToolServerData } from '$lib/apis';
import { verifyToolServerConnection } from '$lib/apis/configs';
import AccessControl from './workspace/common/AccessControl.svelte';
export let onSubmit: Function = () => {};
export let onDelete: Function = () => {};
@@ -22,14 +25,66 @@
export let show = false;
export let edit = false;
export let direct = false;
export let connection = null;
let url = '';
let path = 'openapi.json';
let auth_type = 'bearer';
let key = '';
let accessControl = {};
let enable = true;
let loading = false;
const verifyHandler = async () => {
if (url === '') {
toast.error($i18n.t('Please enter a valid URL'));
return;
}
if (path === '') {
toast.error($i18n.t('Please enter a valid path'));
return;
}
if (direct) {
const res = await getToolServerData(
auth_type === 'bearer' ? key : localStorage.token,
`${url}/${path}`
).catch((err) => {
toast.error($i18n.t('Connection failed'));
});
if (res) {
toast.success($i18n.t('Connection successful'));
console.debug('Connection successful', res);
}
} else {
const res = await verifyToolServerConnection(localStorage.token, {
url,
path,
auth_type,
key,
config: {
enable: enable,
access_control: accessControl
}
}).catch((err) => {
toast.error($i18n.t('Connection failed'));
});
if (res) {
toast.success($i18n.t('Connection successful'));
console.debug('Connection successful', res);
}
}
};
const submitHandler = async () => {
loading = true;
@@ -38,9 +93,12 @@
const connection = {
url,
path,
auth_type,
key,
config: {
enable: enable
enable: enable,
access_control: accessControl
}
};
@@ -50,16 +108,24 @@
show = false;
url = '';
path = 'openapi.json';
key = '';
auth_type = 'bearer';
enable = true;
accessControl = null;
};
const init = () => {
if (connection) {
url = connection.url;
key = connection.key;
path = connection?.path ?? 'openapi.json';
auth_type = connection?.auth_type ?? 'bearer';
key = connection?.key ?? '';
enable = connection.config?.enable ?? true;
accessControl = connection.config?.access_control ?? null;
}
};
@@ -113,47 +179,113 @@
<div class="px-1">
<div class="flex gap-2">
<div class="flex flex-col w-full">
<div class=" mb-0.5 text-xs text-gray-500">{$i18n.t('URL')}</div>
<div class="flex justify-between mb-0.5">
<div class=" text-xs text-gray-500">{$i18n.t('URL')}</div>
</div>
<div class="flex-1">
<div class="flex flex-1 items-center">
<input
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
class="w-full flex-1 text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
type="text"
bind:value={url}
placeholder={$i18n.t('API Base URL')}
autocomplete="off"
required
/>
</div>
</div>
<div class="flex flex-col shrink-0 self-end">
<Tooltip content={enable ? $i18n.t('Enabled') : $i18n.t('Disabled')}>
<Switch bind:state={enable} />
</Tooltip>
<Tooltip
content={$i18n.t('Verify Connection')}
className="shrink-0 flex items-center mr-1"
>
<button
class="self-center p-1 bg-transparent hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
on:click={() => {
verifyHandler();
}}
type="button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
clip-rule="evenodd"
/>
</svg>
</button>
</Tooltip>
<Tooltip content={enable ? $i18n.t('Enabled') : $i18n.t('Disabled')}>
<Switch bind:state={enable} />
</Tooltip>
</div>
<div class="flex-1 flex items-center">
<div class="text-sm">/</div>
<input
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
type="text"
bind:value={path}
placeholder={$i18n.t('openapi.json Path')}
autocomplete="off"
required
/>
</div>
</div>
</div>
<div class="text-xs text-gray-500 mt-1">
{$i18n.t(`WebUI will make requests to "{{url}}/openapi.json"`, {
url: url
{$i18n.t(`WebUI will make requests to "{{url}}"`, {
url: `${url}/${path}`
})}
</div>
<div class="flex gap-2 mt-2">
<div class="flex flex-col w-full">
<div class=" mb-0.5 text-xs text-gray-500">{$i18n.t('Key')}</div>
<div class=" text-xs text-gray-500">{$i18n.t('Auth')}</div>
<div class="flex-1">
<SensitiveInput
className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
bind:value={key}
placeholder={$i18n.t('API Key')}
required={false}
/>
<div class="flex gap-2">
<div class="flex-shrink-0 self-start">
<select
class="w-full text-sm bg-transparent dark:bg-gray-900 placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden pr-5"
bind:value={auth_type}
>
<option value="bearer">Bearer</option>
<option value="session">Session</option>
</select>
</div>
<div class="flex flex-1 items-center">
{#if auth_type === 'bearer'}
<SensitiveInput
className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
bind:value={key}
placeholder={$i18n.t('API Key')}
required={false}
/>
{:else if auth_type === 'session'}
<div class="text-xs text-gray-500 self-center translate-y-[1px]">
{$i18n.t('Forwards system user session credentials to authenticate')}
</div>
{/if}
</div>
</div>
</div>
</div>
{#if !direct}
<hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
<div class="my-2 -mx-2">
<div class="px-3 py-2 bg-gray-50 dark:bg-gray-950 rounded-lg">
<AccessControl bind:accessControl />
</div>
</div>
{/if}
</div>
<div class="flex justify-end pt-3 text-sm font-medium gap-1.5">
@@ -115,7 +115,7 @@
<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{feedbacks.length}</span>
</div>
<div>
{#if feedbacks.length > 0}
<div>
<Tooltip content={$i18n.t('Export')}>
<button
@@ -128,7 +128,7 @@
</button>
</Tooltip>
</div>
</div>
{/if}
</div>
<div
+33 -31
View File
@@ -430,39 +430,41 @@
</div>
</button>
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={async () => {
const _functions = await exportFunctions(localStorage.token).catch((error) => {
toast.error(`${error}`);
return null;
});
if (_functions) {
let blob = new Blob([JSON.stringify(_functions)], {
type: 'application/json'
{#if $functions.length}
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={async () => {
const _functions = await exportFunctions(localStorage.token).catch((error) => {
toast.error(`${error}`);
return null;
});
saveAs(blob, `functions-export-${Date.now()}.json`);
}
}}
>
<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Export Functions')}</div>
<div class=" self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
clip-rule="evenodd"
/>
</svg>
</div>
</button>
if (_functions) {
let blob = new Blob([JSON.stringify(_functions)], {
type: 'application/json'
});
saveAs(blob, `functions-export-${Date.now()}.json`);
}
}}
>
<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Export Functions')}</div>
<div class=" self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
clip-rule="evenodd"
/>
</svg>
</div>
</button>
{/if}
</div>
</div>
+29
View File
@@ -20,6 +20,7 @@
import DocumentChartBar from '../icons/DocumentChartBar.svelte';
import Evaluations from './Settings/Evaluations.svelte';
import CodeExecution from './Settings/CodeExecution.svelte';
import Tools from './Settings/Tools.svelte';
const i18n = getContext('i18n');
@@ -135,6 +136,32 @@
<div class=" self-center">{$i18n.t('Evaluations')}</div>
</button>
<button
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
'tools'
? ''
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
on:click={() => {
selectedTab = 'tools';
}}
>
<div class=" self-center mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-4"
>
<path
fill-rule="evenodd"
d="M12 6.75a5.25 5.25 0 0 1 6.775-5.025.75.75 0 0 1 .313 1.248l-3.32 3.319c.063.475.276.934.641 1.299.365.365.824.578 1.3.64l3.318-3.319a.75.75 0 0 1 1.248.313 5.25 5.25 0 0 1-5.472 6.756c-1.018-.086-1.87.1-2.309.634L7.344 21.3A3.298 3.298 0 1 1 2.7 16.657l8.684-7.151c.533-.44.72-1.291.634-2.309A5.342 5.342 0 0 1 12 6.75ZM4.117 19.125a.75.75 0 0 1 .75-.75h.008a.75.75 0 0 1 .75.75v.008a.75.75 0 0 1-.75.75h-.008a.75.75 0 0 1-.75-.75v-.008Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center">{$i18n.t('Tools')}</div>
</button>
<button
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
'documents'
@@ -373,6 +400,8 @@
<Models />
{:else if selectedTab === 'evaluations'}
<Evaluations />
{:else if selectedTab === 'tools'}
<Tools />
{:else if selectedTab === 'documents'}
<Documents
on:save={async () => {
+42 -1
View File
@@ -39,6 +39,9 @@
let STT_ENGINE = '';
let STT_MODEL = '';
let STT_WHISPER_MODEL = '';
let STT_AZURE_API_KEY = '';
let STT_AZURE_REGION = '';
let STT_AZURE_LOCALES = '';
let STT_DEEPGRAM_API_KEY = '';
let STT_WHISPER_MODEL_LOADING = false;
@@ -108,7 +111,10 @@
ENGINE: STT_ENGINE,
MODEL: STT_MODEL,
WHISPER_MODEL: STT_WHISPER_MODEL,
DEEPGRAM_API_KEY: STT_DEEPGRAM_API_KEY
DEEPGRAM_API_KEY: STT_DEEPGRAM_API_KEY,
AZURE_API_KEY: STT_AZURE_API_KEY,
AZURE_REGION: STT_AZURE_REGION,
AZURE_LOCALES: STT_AZURE_LOCALES
}
});
@@ -148,6 +154,9 @@
STT_ENGINE = res.stt.ENGINE;
STT_MODEL = res.stt.MODEL;
STT_WHISPER_MODEL = res.stt.WHISPER_MODEL;
STT_AZURE_API_KEY = res.stt.AZURE_API_KEY;
STT_AZURE_REGION = res.stt.AZURE_REGION;
STT_AZURE_LOCALES = res.stt.AZURE_LOCALES;
STT_DEEPGRAM_API_KEY = res.stt.DEEPGRAM_API_KEY;
}
@@ -180,6 +189,7 @@
<option value="openai">OpenAI</option>
<option value="web">{$i18n.t('Web API')}</option>
<option value="deepgram">Deepgram</option>
<option value="azure">Azure AI Speech</option>
</select>
</div>
</div>
@@ -248,6 +258,37 @@
</a>
</div>
</div>
{:else if STT_ENGINE === 'azure'}
<div>
<div class="mt-1 flex gap-2 mb-1">
<SensitiveInput
placeholder={$i18n.t('API Key')}
bind:value={STT_AZURE_API_KEY}
required
/>
<input
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
placeholder={$i18n.t('Azure Region')}
bind:value={STT_AZURE_REGION}
required
/>
</div>
<hr class="border-gray-100 dark:border-gray-850 my-2" />
<div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('Language Locales')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={STT_AZURE_LOCALES}
placeholder={$i18n.t('e.g., en-US,ja-JP (leave blank for auto-detect)')}
/>
</div>
</div>
</div>
</div>
{:else if STT_ENGINE === ''}
<div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('STT Model')}</div>
@@ -136,7 +136,7 @@
};
onMount(async () => {
if ($user.role === 'admin') {
if ($user?.role === 'admin') {
let ollamaConfig = {};
let openaiConfig = {};
File diff suppressed because it is too large Load Diff
@@ -77,7 +77,7 @@
};
onMount(async () => {
if ($user.role === 'admin') {
if ($user?.role === 'admin') {
evaluationConfig = await getConfig(localStorage.token).catch((err) => {
toast.error(err);
return null;
@@ -176,7 +176,7 @@
};
onMount(async () => {
if ($user.role === 'admin') {
if ($user?.role === 'admin') {
const res = await getConfig(localStorage.token).catch((error) => {
toast.error(`${error}`);
return null;
@@ -380,7 +380,7 @@
</div>
</div>
{#if $user.role === 'admin'}
{#if $user?.role === 'admin'}
<div class=" space-y-3">
<div class="flex w-full justify-between mb-2">
<div class=" self-center text-sm font-semibold">
@@ -108,7 +108,15 @@
toast.success($i18n.t('Model updated successfully'));
}
} else {
const res = await createNewModel(localStorage.token, model).catch((error) => {
const res = await createNewModel(localStorage.token, {
meta: {},
id: model.id,
name: model.name,
base_model_id: null,
params: {},
access_control: {},
...model
}).catch((error) => {
return null;
});
@@ -19,7 +19,7 @@
let ollamaConfig = null;
onMount(async () => {
if ($user.role === 'admin') {
if ($user?.role === 'admin') {
await Promise.all([
(async () => {
ollamaConfig = await getOllamaConfig(localStorage.token);
@@ -0,0 +1,134 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
import { getModels as _getModels } from '$lib/apis';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
import { models, settings, user } from '$lib/stores';
import Switch from '$lib/components/common/Switch.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Plus from '$lib/components/icons/Plus.svelte';
import Connection from '$lib/components/chat/Settings/Tools/Connection.svelte';
import AddServerModal from '$lib/components/AddServerModal.svelte';
import { getToolServerConnections, setToolServerConnections } from '$lib/apis/configs';
export let saveSettings: Function;
let servers = null;
let showConnectionModal = false;
const addConnectionHandler = async (server) => {
servers = [...servers, server];
await updateHandler();
};
const updateHandler = async () => {
const res = await setToolServerConnections(localStorage.token, {
TOOL_SERVER_CONNECTIONS: servers
}).catch((err) => {
toast.error($i18n.t('Failed to save connections'));
return null;
});
if (res) {
toast.success($i18n.t('Connections saved successfully'));
}
};
onMount(async () => {
const res = await getToolServerConnections(localStorage.token);
servers = res.TOOL_SERVER_CONNECTIONS;
});
</script>
<AddServerModal bind:show={showConnectionModal} onSubmit={addConnectionHandler} />
<form
class="flex flex-col h-full justify-between text-sm"
on:submit|preventDefault={() => {
updateHandler();
}}
>
<div class=" overflow-y-scroll scrollbar-hidden h-full">
{#if servers !== null}
<div class="">
<div class="mb-3">
<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div class="mb-2.5 flex flex-col w-full justify-between">
<!-- {$i18n.t(`Failed to connect to {{URL}} OpenAPI tool server`, {
URL: 'server?.url'
})} -->
<div class="flex justify-between items-center mb-0.5">
<div class="font-medium">{$i18n.t('Manage Tool Servers')}</div>
<Tooltip content={$i18n.t(`Add Connection`)}>
<button
class="px-1"
on:click={() => {
showConnectionModal = true;
}}
type="button"
>
<Plus />
</button>
</Tooltip>
</div>
<div class="flex flex-col gap-1.5">
{#each servers as server, idx}
<Connection
bind:connection={server}
onSubmit={() => {
updateHandler();
}}
onDelete={() => {
servers = servers.filter((_, i) => i !== idx);
updateHandler();
}}
/>
{/each}
</div>
<div class="my-1.5">
<div class="text-xs text-gray-500">
{$i18n.t('Connect to your own OpenAPI compatible external tool servers.')}
</div>
</div>
</div>
<!-- <div class="mb-2.5 flex w-full justify-between">
<div class=" text-xs font-medium">{$i18n.t('Arena Models')}</div>
<Tooltip content={$i18n.t(`Message rating should be enabled to use this feature`)}>
<Switch bind:state={evaluationConfig.ENABLE_EVALUATION_ARENA_MODELS} />
</Tooltip>
</div> -->
</div>
</div>
{:else}
<div class="flex h-full justify-center">
<div class="my-auto">
<Spinner className="size-6" />
</div>
</div>
{/if}
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
<button
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
type="submit"
>
{$i18n.t('Save')}
</button>
</div>
</form>
+253 -103
View File
@@ -12,7 +12,6 @@
export let saveHandler: Function;
let webConfig = null;
let webSearchEngines = [
'searxng',
'google_pse',
@@ -30,34 +29,38 @@
'jina',
'bing',
'exa',
'perplexity'
'perplexity',
'sougou'
];
let webLoaderEngines = ['playwright', 'firecrawl', 'tavily'];
let youtubeLanguage = 'en';
let youtubeTranslation = null;
let youtubeProxyUrl = '';
let webConfig = null;
const submitHandler = async () => {
// Convert domain filter string to array before sending
if (webConfig.search.domain_filter_list) {
webConfig.search.domain_filter_list = webConfig.search.domain_filter_list
.split(',')
if (webConfig.WEB_SEARCH_DOMAIN_FILTER_LIST) {
webConfig.WEB_SEARCH_DOMAIN_FILTER_LIST = webConfig.WEB_SEARCH_DOMAIN_FILTER_LIST.split(',')
.map((domain) => domain.trim())
.filter((domain) => domain.length > 0);
} else {
webConfig.search.domain_filter_list = [];
webConfig.WEB_SEARCH_DOMAIN_FILTER_LIST = [];
}
// Convert Youtube loader language string to array before sending
if (webConfig.YOUTUBE_LOADER_LANGUAGE) {
webConfig.YOUTUBE_LOADER_LANGUAGE = webConfig.YOUTUBE_LOADER_LANGUAGE.split(',')
.map((lang) => lang.trim())
.filter((lang) => lang.length > 0);
} else {
webConfig.YOUTUBE_LOADER_LANGUAGE = [];
}
const res = await updateRAGConfig(localStorage.token, {
web: webConfig,
youtube: {
language: youtubeLanguage.split(',').map((lang) => lang.trim()),
translation: youtubeTranslation,
proxy_url: youtubeProxyUrl
}
web: webConfig
});
webConfig.search.domain_filter_list = webConfig.search.domain_filter_list.join(', ');
webConfig.WEB_SEARCH_DOMAIN_FILTER_LIST = webConfig.WEB_SEARCH_DOMAIN_FILTER_LIST.join(',');
webConfig.YOUTUBE_LOADER_LANGUAGE = webConfig.YOUTUBE_LOADER_LANGUAGE.join(',');
};
onMount(async () => {
@@ -65,14 +68,13 @@
if (res) {
webConfig = res.web;
// Convert array back to comma-separated string for display
if (webConfig?.search?.domain_filter_list) {
webConfig.search.domain_filter_list = webConfig.search.domain_filter_list.join(', ');
if (webConfig?.WEB_SEARCH_DOMAIN_FILTER_LIST) {
webConfig.WEB_SEARCH_DOMAIN_FILTER_LIST = webConfig.WEB_SEARCH_DOMAIN_FILTER_LIST.join(',');
}
youtubeLanguage = res.youtube.language.join(',');
youtubeTranslation = res.youtube.translation;
youtubeProxyUrl = res.youtube.proxy_url;
webConfig.YOUTUBE_LOADER_LANGUAGE = webConfig.YOUTUBE_LOADER_LANGUAGE.join(',');
}
});
</script>
@@ -97,7 +99,7 @@
{$i18n.t('Web Search')}
</div>
<div class="flex items-center relative">
<Switch bind:state={webConfig.search.enabled} />
<Switch bind:state={webConfig.ENABLE_WEB_SEARCH} />
</div>
</div>
@@ -108,7 +110,7 @@
<div class="flex items-center relative">
<select
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
bind:value={webConfig.search.engine}
bind:value={webConfig.WEB_SEARCH_ENGINE}
placeholder={$i18n.t('Select a engine')}
required
>
@@ -120,8 +122,8 @@
</div>
</div>
{#if webConfig.search.engine !== ''}
{#if webConfig.search.engine === 'searxng'}
{#if webConfig.WEB_SEARCH_ENGINE !== ''}
{#if webConfig.WEB_SEARCH_ENGINE === 'searxng'}
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
@@ -134,14 +136,14 @@
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="text"
placeholder={$i18n.t('Enter Searxng Query URL')}
bind:value={webConfig.search.searxng_query_url}
bind:value={webConfig.SEARXNG_QUERY_URL}
autocomplete="off"
/>
</div>
</div>
</div>
</div>
{:else if webConfig.search.engine === 'google_pse'}
{:else if webConfig.WEB_SEARCH_ENGINE === 'google_pse'}
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
@@ -150,7 +152,7 @@
<SensitiveInput
placeholder={$i18n.t('Enter Google PSE API Key')}
bind:value={webConfig.search.google_pse_api_key}
bind:value={webConfig.GOOGLE_PSE_API_KEY}
/>
</div>
<div class="mt-1.5">
@@ -164,14 +166,14 @@
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="text"
placeholder={$i18n.t('Enter Google PSE Engine Id')}
bind:value={webConfig.search.google_pse_engine_id}
bind:value={webConfig.GOOGLE_PSE_ENGINE_ID}
autocomplete="off"
/>
</div>
</div>
</div>
</div>
{:else if webConfig.search.engine === 'brave'}
{:else if webConfig.WEB_SEARCH_ENGINE === 'brave'}
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
@@ -180,11 +182,11 @@
<SensitiveInput
placeholder={$i18n.t('Enter Brave Search API Key')}
bind:value={webConfig.search.brave_search_api_key}
bind:value={webConfig.BRAVE_SEARCH_API_KEY}
/>
</div>
</div>
{:else if webConfig.search.engine === 'kagi'}
{:else if webConfig.WEB_SEARCH_ENGINE === 'kagi'}
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
@@ -193,12 +195,12 @@
<SensitiveInput
placeholder={$i18n.t('Enter Kagi Search API Key')}
bind:value={webConfig.search.kagi_search_api_key}
bind:value={webConfig.KAGI_SEARCH_API_KEY}
/>
</div>
.
</div>
{:else if webConfig.search.engine === 'mojeek'}
{:else if webConfig.WEB_SEARCH_ENGINE === 'mojeek'}
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
@@ -207,11 +209,11 @@
<SensitiveInput
placeholder={$i18n.t('Enter Mojeek Search API Key')}
bind:value={webConfig.search.mojeek_search_api_key}
bind:value={webConfig.MOJEEK_SEARCH_API_KEY}
/>
</div>
</div>
{:else if webConfig.search.engine === 'bocha'}
{:else if webConfig.WEB_SEARCH_ENGINE === 'bocha'}
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
@@ -220,11 +222,11 @@
<SensitiveInput
placeholder={$i18n.t('Enter Bocha Search API Key')}
bind:value={webConfig.search.bocha_search_api_key}
bind:value={webConfig.BOCHA_SEARCH_API_KEY}
/>
</div>
</div>
{:else if webConfig.search.engine === 'serpstack'}
{:else if webConfig.WEB_SEARCH_ENGINE === 'serpstack'}
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
@@ -233,11 +235,11 @@
<SensitiveInput
placeholder={$i18n.t('Enter Serpstack API Key')}
bind:value={webConfig.search.serpstack_api_key}
bind:value={webConfig.SERPSTACK_API_KEY}
/>
</div>
</div>
{:else if webConfig.search.engine === 'serper'}
{:else if webConfig.WEB_SEARCH_ENGINE === 'serper'}
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
@@ -246,11 +248,11 @@
<SensitiveInput
placeholder={$i18n.t('Enter Serper API Key')}
bind:value={webConfig.search.serper_api_key}
bind:value={webConfig.SERPER_API_KEY}
/>
</div>
</div>
{:else if webConfig.search.engine === 'serply'}
{:else if webConfig.WEB_SEARCH_ENGINE === 'serply'}
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
@@ -259,11 +261,24 @@
<SensitiveInput
placeholder={$i18n.t('Enter Serply API Key')}
bind:value={webConfig.search.serply_api_key}
bind:value={webConfig.SERPLY_API_KEY}
/>
</div>
</div>
{:else if webConfig.search.engine === 'searchapi'}
{:else if webConfig.WEB_SEARCH_ENGINE === 'tavily'}
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Tavily API Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Tavily API Key')}
bind:value={webConfig.TAVILY_API_KEY}
/>
</div>
</div>
{:else if webConfig.WEB_SEARCH_ENGINE === 'searchapi'}
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
@@ -272,7 +287,7 @@
<SensitiveInput
placeholder={$i18n.t('Enter SearchApi API Key')}
bind:value={webConfig.search.searchapi_api_key}
bind:value={webConfig.SEARCHAPI_API_KEY}
/>
</div>
<div class="mt-1.5">
@@ -286,14 +301,14 @@
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="text"
placeholder={$i18n.t('Enter SearchApi Engine')}
bind:value={webConfig.search.searchapi_engine}
bind:value={webConfig.SEARCHAPI_ENGINE}
autocomplete="off"
/>
</div>
</div>
</div>
</div>
{:else if webConfig.search.engine === 'serpapi'}
{:else if webConfig.WEB_SEARCH_ENGINE === 'serpapi'}
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
@@ -302,7 +317,7 @@
<SensitiveInput
placeholder={$i18n.t('Enter SerpApi API Key')}
bind:value={webConfig.search.serpapi_api_key}
bind:value={webConfig.SERPAPI_API_KEY}
/>
</div>
<div class="mt-1.5">
@@ -316,27 +331,14 @@
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="text"
placeholder={$i18n.t('Enter SerpApi Engine')}
bind:value={webConfig.search.serpapi_engine}
bind:value={webConfig.SERPAPI_ENGINE}
autocomplete="off"
/>
</div>
</div>
</div>
</div>
{:else if webConfig.search.engine === 'tavily'}
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Tavily API Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Tavily API Key')}
bind:value={webConfig.search.tavily_api_key}
/>
</div>
</div>
{:else if webConfig.search.engine === 'jina'}
{:else if webConfig.WEB_SEARCH_ENGINE === 'jina'}
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
@@ -345,35 +347,11 @@
<SensitiveInput
placeholder={$i18n.t('Enter Jina API Key')}
bind:value={webConfig.search.jina_api_key}
bind:value={webConfig.JINA_API_KEY}
/>
</div>
</div>
{:else if webConfig.search.engine === 'exa'}
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Exa API Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Exa API Key')}
bind:value={webConfig.search.exa_api_key}
/>
</div>
</div>
{:else if webConfig.search.engine === 'perplexity'}
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Perplexity API Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Perplexity API Key')}
bind:value={webConfig.search.perplexity_api_key}
/>
</div>
{:else if webConfig.search.engine === 'bing'}
{:else if webConfig.WEB_SEARCH_ENGINE === 'bing'}
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
@@ -386,7 +364,7 @@
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="text"
placeholder={$i18n.t('Enter Bing Search V7 Endpoint')}
bind:value={webConfig.search.bing_search_v7_endpoint}
bind:value={webConfig.BING_SEARCH_V7_ENDPOINT}
autocomplete="off"
/>
</div>
@@ -400,14 +378,63 @@
<SensitiveInput
placeholder={$i18n.t('Enter Bing Search V7 Subscription Key')}
bind:value={webConfig.search.bing_search_v7_subscription_key}
bind:value={webConfig.BING_SEARCH_V7_SUBSCRIPTION_KEY}
/>
</div>
</div>
{:else if webConfig.WEB_SEARCH_ENGINE === 'exa'}
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Exa API Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Exa API Key')}
bind:value={webConfig.EXA_API_KEY}
/>
</div>
</div>
{:else if webConfig.WEB_SEARCH_ENGINE === 'perplexity'}
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Perplexity API Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Perplexity API Key')}
bind:value={webConfig.PERPLEXITY_API_KEY}
/>
</div>
{:else if webConfig.WEB_SEARCH_ENGINE === 'sougou'}
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Sougou Search API sID')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Sougou Search API sID')}
bind:value={webConfig.SOUGOU_API_SID}
/>
</div>
</div>
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Sougou Search API SK')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Sougou Search API SK')}
bind:value={webConfig.SOUGOU_API_SK}
/>
</div>
</div>
{/if}
{/if}
{#if webConfig.search.enabled}
{#if webConfig.ENABLE_WEB_SEARCH}
<div class="mb-2.5 flex w-full flex-col">
<div class="flex gap-2">
<div class="w-full">
@@ -418,7 +445,7 @@
<input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
placeholder={$i18n.t('Search Result Count')}
bind:value={webConfig.search.result_count}
bind:value={webConfig.WEB_SEARCH_RESULT_COUNT}
required
/>
</div>
@@ -431,7 +458,7 @@
<input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
placeholder={$i18n.t('Concurrent Requests')}
bind:value={webConfig.search.concurrent_requests}
bind:value={webConfig.WEB_SEARCH_CONCURRENT_REQUESTS}
required
/>
</div>
@@ -448,7 +475,7 @@
placeholder={$i18n.t(
'Enter domains separated by commas (e.g., example.com,site.org)'
)}
bind:value={webConfig.search.domain_filter_list}
bind:value={webConfig.WEB_SEARCH_DOMAIN_FILTER_LIST}
/>
</div>
{/if}
@@ -480,11 +507,13 @@
</div>
<div class="flex items-center relative">
<Tooltip
content={webConfig.search.trust_env
? 'Use proxy designated by http_proxy and https_proxy environment variables to fetch page contents'
: 'Use no proxy to fetch page contents.'}
content={webConfig.WEB_SEARCH_TRUST_ENV
? $i18n.t(
'Use proxy designated by http_proxy and https_proxy environment variables to fetch page contents.'
)
: $i18n.t('Use no proxy to fetch page contents.')}
>
<Switch bind:state={webConfig.search.trust_env} />
<Switch bind:state={webConfig.WEB_SEARCH_TRUST_ENV} />
</Tooltip>
</div>
</div>
@@ -497,13 +526,134 @@
<div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Bypass SSL verification for Websites')}
{$i18n.t('Web Loader Engine')}
</div>
<div class="flex items-center relative">
<Switch bind:state={webConfig.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION} />
<select
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
bind:value={webConfig.WEB_LOADER_ENGINE}
placeholder={$i18n.t('Select a engine')}
>
<option value="">{$i18n.t('Default')}</option>
{#each webLoaderEngines as engine}
<option value={engine}>{engine}</option>
{/each}
</select>
</div>
</div>
{#if webConfig.WEB_LOADER_ENGINE === '' || webConfig.WEB_LOADER_ENGINE === 'safe_web'}
<div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Verify SSL Certificate')}
</div>
<div class="flex items-center relative">
<Switch bind:state={webConfig.ENABLE_WEB_LOADER_SSL_VERIFICATION} />
</div>
</div>
{:else if webConfig.WEB_LOADER_ENGINE === 'playwright'}
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Playwright WebSocket URL')}
</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="text"
placeholder={$i18n.t('Enter Playwright WebSocket URL')}
bind:value={webConfig.PLAYWRIGHT_WS_URL}
autocomplete="off"
/>
</div>
</div>
</div>
<div class="mt-2">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Playwright Timeout (ms)')}
</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
placeholder={$i18n.t('Enter Playwright Timeout')}
bind:value={webConfig.PLAYWRIGHT_TIMEOUT}
autocomplete="off"
/>
</div>
</div>
</div>
</div>
{:else if webConfig.WEB_LOADER_ENGINE === 'firecrawl'}
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Firecrawl API Base URL')}
</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="text"
placeholder={$i18n.t('Enter Firecrawl API Base URL')}
bind:value={webConfig.FIRECRAWL_API_BASE_URL}
autocomplete="off"
/>
</div>
</div>
</div>
<div class="mt-2">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Firecrawl API Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Firecrawl API Key')}
bind:value={webConfig.FIRECRAWL_API_KEY}
/>
</div>
</div>
{:else if webConfig.WEB_LOADER_ENGINE === 'tavily'}
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Tavily Extract Depth')}
</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="text"
placeholder={$i18n.t('Enter Tavily Extract Depth')}
bind:value={webConfig.TAVILY_EXTRACT_DEPTH}
autocomplete="off"
/>
</div>
</div>
</div>
{#if webConfig.WEB_SEARCH_ENGINE !== 'tavily'}
<div class="mt-2">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Tavily API Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Tavily API Key')}
bind:value={webConfig.TAVILY_API_KEY}
/>
</div>
{/if}
</div>
{/if}
<div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Youtube Language')}
@@ -513,7 +663,7 @@
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden"
type="text"
placeholder={$i18n.t('Enter language codes')}
bind:value={youtubeLanguage}
bind:value={webConfig.YOUTUBE_LOADER_LANGUAGE}
autocomplete="off"
/>
</div>
@@ -528,7 +678,7 @@
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden"
type="text"
placeholder={$i18n.t('Enter proxy URL (e.g. https://user:password@host:port)')}
bind:value={youtubeProxyUrl}
bind:value={webConfig.YOUTUBE_LOADER_PROXY_URL}
autocomplete="off"
/>
</div>
+3 -1
View File
@@ -63,10 +63,12 @@
file_upload: true,
delete: true,
edit: true,
multiple_models: true,
temporary: true,
temporary_enforced: true
temporary_enforced: false
},
features: {
direct_tool_servers: false,
web_search: true,
image_generation: true,
code_interpreter: true
@@ -38,6 +38,12 @@
prompts: false,
tools: false
},
sharing: {
public_models: false,
public_knowledge: false,
public_prompts: false,
public_tools: false
},
chat: {
controls: true,
file_upload: true,
@@ -46,6 +52,7 @@
temporary: true
},
features: {
direct_tool_servers: false,
web_search: true,
image_generation: true,
code_interpreter: true
@@ -24,10 +24,12 @@
delete: true,
edit: true,
file_upload: true,
multiple_models: true,
temporary: true,
temporary_enforced: true
temporary_enforced: false
},
features: {
direct_tool_servers: false,
web_search: true,
image_generation: true,
code_interpreter: true
@@ -271,6 +273,14 @@
<Switch bind:state={permissions.chat.edit} />
</div>
<div class=" flex w-full justify-between my-2 pr-2">
<div class=" self-center text-xs font-medium">
{$i18n.t('Allow Multiple Models in Chat')}
</div>
<Switch bind:state={permissions.chat.multiple_models} />
</div>
<div class=" flex w-full justify-between my-2 pr-2">
<div class=" self-center text-xs font-medium">
{$i18n.t('Allow Temporary Chat')}
@@ -295,6 +305,14 @@
<div>
<div class=" mb-2 text-sm font-medium">{$i18n.t('Features Permissions')}</div>
<div class=" flex w-full justify-between my-2 pr-2">
<div class=" self-center text-xs font-medium">
{$i18n.t('Direct Tool Servers')}
</div>
<Switch bind:state={permissions.features.direct_tool_servers} />
</div>
<div class=" flex w-full justify-between my-2 pr-2">
<div class=" self-center text-xs font-medium">
{$i18n.t('Web Search')}
@@ -29,6 +29,7 @@
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
import About from '$lib/components/chat/Settings/About.svelte';
import Banner from '$lib/components/common/Banner.svelte';
import Markdown from '$lib/components/chat/Messages/Markdown.svelte';
const i18n = getContext('i18n');
@@ -486,3 +487,27 @@
</div>
<Pagination bind:page count={users.length} />
{#if !$config?.license_metadata}
{#if users.length > 50}
<div class="text-sm">
<Markdown
content={`
> [!NOTE]
> # **Hey there! 👋**
>
> It looks like you have over 50 users — that usually falls under organizational usage.
>
> Open WebUI is proudly open source and completely free, with no hidden limits — and we'd love to keep it that way. 🌱
>
> By supporting the project through sponsorship or an enterprise license, you’re not only helping us stay independent, you’re also helping us ship new features faster, improve stability, and grow the project for the long haul. With an *enterprise license*, you also get additional perks like dedicated support, customization options, and more — all at a fraction of what it would cost to build and maintain internally.
>
> Your support helps us stay independent and continue building great tools for everyone. 💛
>
> - 👉 **[Click here to learn more about enterprise licensing](https://docs.openwebui.com/enterprise)**
> - 👉 *[Click here to sponsor the project on GitHub](https://github.com/sponsors/tjbck)*
`}
/>
</div>
{/if}
{/if}
+1 -1
View File
@@ -106,7 +106,7 @@
messages[idx] = data;
}
} else if (type === 'typing' && event.message_id === null) {
if (event.user.id === $user.id) {
if (event.user.id === $user?.id) {
return;
}
@@ -381,7 +381,7 @@
>
<div
class="flex-1 flex flex-col relative w-full rounded-3xl px-1 bg-gray-600/5 dark:bg-gray-400/5 dark:text-gray-100"
dir={$settings?.chatDirection ?? 'LTR'}
dir={$settings?.chatDirection ?? 'auto'}
>
{#if files.length > 0}
<div class="mx-2 mt-2.5 -mb-1 flex flex-wrap gap-2">
+4 -4
View File
@@ -132,7 +132,7 @@
if (
(message?.reactions ?? [])
.find((reaction) => reaction.name === name)
?.user_ids?.includes($user.id) ??
?.user_ids?.includes($user?.id) ??
false
) {
messages = messages.map((m) => {
@@ -140,7 +140,7 @@
const reaction = m.reactions.find((reaction) => reaction.name === name);
if (reaction) {
reaction.user_ids = reaction.user_ids.filter((id) => id !== $user.id);
reaction.user_ids = reaction.user_ids.filter((id) => id !== $user?.id);
reaction.count = reaction.user_ids.length;
if (reaction.count === 0) {
@@ -167,12 +167,12 @@
const reaction = m.reactions.find((reaction) => reaction.name === name);
if (reaction) {
reaction.user_ids.push($user.id);
reaction.user_ids.push($user?.id);
reaction.count = reaction.user_ids.length;
} else {
m.reactions.push({
name: name,
user_ids: [$user.id],
user_ids: [$user?.id],
count: 1
});
}
@@ -106,7 +106,7 @@
</Tooltip>
{/if}
{#if message.user_id === $user.id || $user.role === 'admin'}
{#if message.user_id === $user?.id || $user?.role === 'admin'}
<Tooltip content={$i18n.t('Edit')}>
<button
class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
@@ -265,7 +265,7 @@
<Tooltip content={`:${reaction.name}:`}>
<button
class="flex items-center gap-1.5 transition rounded-xl px-2 py-1 cursor-pointer {reaction.user_ids.includes(
$user.id
$user?.id
)
? ' bg-blue-300/10 outline outline-blue-500/50 outline-1'
: 'bg-gray-300/10 dark:bg-gray-500/10 hover:outline hover:outline-gray-700/30 dark:hover:outline-gray-300/30 hover:outline-1'}"
+2 -2
View File
@@ -58,7 +58,7 @@
{#if $user !== undefined}
<UserMenu
className="max-w-[200px]"
role={$user.role}
role={$user?.role}
on:show={(e) => {
if (e.detail === 'archived-chat') {
showArchivedChats.set(true);
@@ -71,7 +71,7 @@
>
<div class=" self-center">
<img
src={$user.profile_image_url}
src={$user?.profile_image_url}
class="size-6 object-cover rounded-full"
alt="User profile"
draggable="false"
+1 -1
View File
@@ -89,7 +89,7 @@
}
}
} else if (type === 'typing' && event.message_id === threadId) {
if (event.user.id === $user.id) {
if (event.user.id === $user?.id) {
return;
}
+113 -105
View File
@@ -4,7 +4,7 @@
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
import { chatId, showArtifacts, showControls } from '$lib/stores';
import { chatId, settings, showArtifacts, showControls } from '$lib/stores';
import XMark from '../icons/XMark.svelte';
import { copyToClipboard, createMessagesList } from '$lib/utils';
import ArrowsPointingOut from '../icons/ArrowsPointingOut.svelte';
@@ -184,35 +184,117 @@
</script>
<div class=" w-full h-full relative flex flex-col bg-gray-50 dark:bg-gray-850">
<div class="w-full h-full flex-1 relative">
<div class="w-full h-full flex flex-col flex-1 relative">
{#if contents.length > 0}
<div
class="pointer-events-auto z-20 flex justify-between items-center p-2.5 font-primar text-gray-900 dark:text-white"
>
<button
class="self-center pointer-events-auto p-1 rounded-full bg-white dark:bg-gray-850"
on:click={() => {
showArtifacts.set(false);
}}
>
<ArrowLeft className="size-3.5 text-gray-900 dark:text-white" />
</button>
<div class="flex-1 flex items-center justify-between">
<div class="flex items-center space-x-2">
<div class="flex items-center gap-0.5 self-center min-w-fit" dir="ltr">
<button
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition disabled:cursor-not-allowed"
on:click={() => navigateContent('prev')}
disabled={contents.length <= 1}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2.5"
class="size-3.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 19.5 8.25 12l7.5-7.5"
/>
</svg>
</button>
<div class="text-xs self-center dark:text-gray-100 min-w-fit">
{$i18n.t('Version {{selectedVersion}} of {{totalVersions}}', {
selectedVersion: selectedContentIdx + 1,
totalVersions: contents.length
})}
</div>
<button
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition disabled:cursor-not-allowed"
on:click={() => navigateContent('next')}
disabled={contents.length <= 1}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2.5"
class="size-3.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m8.25 4.5 7.5 7.5-7.5 7.5"
/>
</svg>
</button>
</div>
</div>
<div class="flex items-center gap-1">
<button
class="copy-code-button bg-none border-none text-xs bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5"
on:click={() => {
copyToClipboard(contents[selectedContentIdx].content);
copied = true;
setTimeout(() => {
copied = false;
}, 2000);
}}>{copied ? $i18n.t('Copied') : $i18n.t('Copy')}</button
>
{#if contents[selectedContentIdx].type === 'iframe'}
<Tooltip content={$i18n.t('Open in full screen')}>
<button
class=" bg-none border-none text-xs bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md p-0.5"
on:click={showFullScreen}
>
<ArrowsPointingOut className="size-3.5" />
</button>
</Tooltip>
{/if}
</div>
</div>
<button
class="self-center pointer-events-auto p-1 rounded-full bg-white dark:bg-gray-850"
on:click={() => {
dispatch('close');
showControls.set(false);
showArtifacts.set(false);
}}
>
<XMark className="size-3.5 text-gray-900 dark:text-white" />
</button>
</div>
{/if}
{#if overlay}
<div class=" absolute top-0 left-0 right-0 bottom-0 z-10"></div>
{/if}
<div class="absolute pointer-events-none z-50 w-full flex items-center justify-start p-4">
<button
class="self-center pointer-events-auto p-1 rounded-full bg-white dark:bg-gray-850"
on:click={() => {
showArtifacts.set(false);
}}
>
<ArrowLeft className="size-3.5 text-gray-900 dark:text-white" />
</button>
</div>
<div class=" absolute pointer-events-none z-50 w-full flex items-center justify-end p-4">
<button
class="self-center pointer-events-auto p-1 rounded-full bg-white dark:bg-gray-850"
on:click={() => {
dispatch('close');
showControls.set(false);
showArtifacts.set(false);
}}
>
<XMark className="size-3.5 text-gray-900 dark:text-white" />
</button>
</div>
<div class="flex-1 w-full h-full">
<div class=" h-full flex flex-col">
{#if contents.length > 0}
@@ -223,7 +305,11 @@
title="Content"
srcdoc={contents[selectedContentIdx].content}
class="w-full border-0 h-full rounded-none"
sandbox="allow-scripts allow-forms allow-same-origin"
sandbox="allow-scripts{($settings?.iframeSandboxAllowForms ?? false)
? ' allow-forms'
: ''}{($settings?.iframeSandboxAllowSameOrigin ?? false)
? ' allow-same-origin'
: ''}"
on:load={iframeLoadHandler}
></iframe>
{:else if contents[selectedContentIdx].type === 'svg'}
@@ -241,82 +327,4 @@
</div>
</div>
</div>
{#if contents.length > 0}
<div class="flex justify-between items-center p-2.5 font-primar text-gray-900 dark:text-white">
<div class="flex items-center space-x-2">
<div class="flex items-center gap-0.5 self-center min-w-fit" dir="ltr">
<button
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition disabled:cursor-not-allowed"
on:click={() => navigateContent('prev')}
disabled={contents.length <= 1}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2.5"
class="size-3.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 19.5 8.25 12l7.5-7.5"
/>
</svg>
</button>
<div class="text-xs self-center dark:text-gray-100 min-w-fit">
{$i18n.t('Version {{selectedVersion}} of {{totalVersions}}', {
selectedVersion: selectedContentIdx + 1,
totalVersions: contents.length
})}
</div>
<button
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition disabled:cursor-not-allowed"
on:click={() => navigateContent('next')}
disabled={contents.length <= 1}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2.5"
class="size-3.5"
>
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
</svg>
</button>
</div>
</div>
<div class="flex items-center gap-1">
<button
class="copy-code-button bg-none border-none text-xs bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5"
on:click={() => {
copyToClipboard(contents[selectedContentIdx].content);
copied = true;
setTimeout(() => {
copied = false;
}, 2000);
}}>{copied ? $i18n.t('Copied') : $i18n.t('Copy')}</button
>
{#if contents[selectedContentIdx].type === 'iframe'}
<Tooltip content={$i18n.t('Open in full screen')}>
<button
class=" bg-none border-none text-xs bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md p-0.5"
on:click={showFullScreen}
>
<ArrowsPointingOut className="size-3.5" />
</button>
</Tooltip>
{/if}
</div>
</div>
{/if}
</div>
+83 -94
View File
@@ -48,7 +48,8 @@
splitStream,
sleep,
removeDetails,
getPromptVariables
getPromptVariables,
processDetails
} from '$lib/utils';
import { generateChatCompletion } from '$lib/apis/ollama';
@@ -73,7 +74,8 @@
generateQueries,
chatAction,
generateMoACompletion,
stopTask
stopTask,
getTaskIdsByChatId
} from '$lib/apis';
import { getTools } from '$lib/apis/tools';
@@ -129,7 +131,7 @@
currentId: null
};
let taskId = null;
let taskIds = null;
// Chat Input
let prompt = '';
@@ -400,6 +402,7 @@
if (!$chatId) {
chatIdUnsubscriber = chatId.subscribe(async (value) => {
if (!value) {
await tick(); // Wait for DOM updates
await initNewChat();
}
});
@@ -816,8 +819,21 @@
await tick();
if (history.currentId) {
history.messages[history.currentId].done = true;
for (const message of Object.values(history.messages)) {
if (message.role === 'assistant') {
message.done = true;
}
}
}
const taskRes = await getTaskIdsByChatId(localStorage.token, $chatId).catch((error) => {
return null;
});
if (taskRes) {
taskIds = taskRes.task_ids;
}
await tick();
return true;
@@ -889,7 +905,7 @@
}
}
taskId = null;
taskIds = null;
};
const chatActionHandler = async (chatId, actionId, modelId, responseMessageId, event = null) => {
@@ -1336,6 +1352,10 @@
parentId: string,
{ modelId = null, modelIdx = null, newChat = false } = {}
) => {
if (autoScroll) {
scrollToBottom();
}
let _chatId = JSON.parse(JSON.stringify($chatId));
_history = JSON.parse(JSON.stringify(_history));
@@ -1498,7 +1518,7 @@
role: 'system',
content: `${promptTemplate(
params?.system ?? $settings?.system ?? '',
$user.name,
$user?.name,
$settings?.userLocation
? await getAndUpdateUserLocation(localStorage.token).catch((err) => {
console.error(err);
@@ -1514,7 +1534,7 @@
: undefined,
...createMessagesList(_history, responseMessageId).map((message) => ({
...message,
content: removeDetails(message.content, ['reasoning', 'code_interpreter'])
content: processDetails(message.content)
}))
].filter((message) => message);
@@ -1572,23 +1592,23 @@
features: {
image_generation:
$config?.features?.enable_image_generation &&
($user.role === 'admin' || $user?.permissions?.features?.image_generation)
($user?.role === 'admin' || $user?.permissions?.features?.image_generation)
? imageGenerationEnabled
: false,
code_interpreter:
$config?.features?.enable_code_interpreter &&
($user.role === 'admin' || $user?.permissions?.features?.code_interpreter)
($user?.role === 'admin' || $user?.permissions?.features?.code_interpreter)
? codeInterpreterEnabled
: false,
web_search:
$config?.features?.enable_web_search &&
($user.role === 'admin' || $user?.permissions?.features?.web_search)
($user?.role === 'admin' || $user?.permissions?.features?.web_search)
? webSearchEnabled || ($settings?.webSearch ?? false) === 'always'
: false
},
variables: {
...getPromptVariables(
$user.name,
$user?.name,
$settings?.userLocation
? await getAndUpdateUserLocation(localStorage.token).catch((err) => {
console.error(err);
@@ -1643,7 +1663,11 @@
if (res.error) {
await handleOpenAIError(res.error, responseMessage);
} else {
taskId = res.task_id;
if (taskIds) {
taskIds.push(res.task_id);
} else {
taskIds = [res.task_id];
}
}
}
@@ -1694,23 +1718,26 @@
};
const stopResponse = async () => {
if (taskId) {
const res = await stopTask(localStorage.token, taskId).catch((error) => {
toast.error(`${error}`);
return null;
});
if (taskIds) {
for (const taskId of taskIds) {
const res = await stopTask(localStorage.token, taskId).catch((error) => {
toast.error(`${error}`);
return null;
});
}
if (res) {
taskId = null;
taskIds = null;
const responseMessage = history.messages[history.currentId];
responseMessage.done = true;
const responseMessage = history.messages[history.currentId];
// Set all response messages to done
for (const messageId of history.messages[responseMessage.parentId].childrenIds) {
history.messages[messageId].done = true;
}
history.messages[history.currentId] = responseMessage;
history.messages[history.currentId] = responseMessage;
if (autoScroll) {
scrollToBottom();
}
if (autoScroll) {
scrollToBottom();
}
}
};
@@ -1739,6 +1766,11 @@
history.currentId = userMessageId;
await tick();
if (autoScroll) {
scrollToBottom();
}
await sendPrompt(history, userPrompt, userMessageId);
};
@@ -1749,6 +1781,10 @@
let userMessage = history.messages[message.parentId];
let userPrompt = userMessage.content;
if (autoScroll) {
scrollToBottom();
}
if ((userMessage?.models ?? [...selectedModels]).length == 1) {
// If user message has only one model selected, sendPrompt automatically selects it for regeneration
await sendPrompt(history, userPrompt, userMessage.id);
@@ -1913,7 +1949,7 @@
: ' '} w-full max-w-full flex flex-col"
id="chat-container"
>
{#if chatIdProp === '' || (!loading && chatIdProp)}
{#if !loading}
{#if $settings?.backgroundImageUrl ?? null}
<div
class="absolute {$showSidebar
@@ -1927,75 +1963,27 @@
/>
{/if}
<Navbar
bind:this={navbarElement}
chat={{
id: $chatId,
chat: {
title: $chatTitle,
models: selectedModels,
system: $settings.system ?? undefined,
params: params,
history: history,
timestamp: Date.now()
}
}}
title={$chatTitle}
bind:selectedModels
shareEnabled={!!history.currentId}
{initNewChat}
/>
<PaneGroup direction="horizontal" class="w-full h-full">
<Pane defaultSize={50} class="h-full flex w-full relative">
{#if !history.currentId && !$chatId && selectedModels.length <= 1 && ($banners.length > 0 || ($config?.license_metadata?.type ?? null) === 'trial' || (($config?.license_metadata?.seats ?? null) !== null && $config?.user_count > $config?.license_metadata?.seats))}
<div class="absolute top-12 left-0 right-0 w-full z-30">
<div class=" flex flex-col gap-1 w-full">
{#if ($config?.license_metadata?.type ?? null) === 'trial'}
<Banner
banner={{
type: 'info',
title: 'Trial License',
content: $i18n.t(
'You are currently using a trial license. Please contact support to upgrade your license.'
)
}}
/>
{/if}
{#if ($config?.license_metadata?.seats ?? null) !== null && $config?.user_count > $config?.license_metadata?.seats}
<Banner
banner={{
type: 'error',
title: 'License Error',
content: $i18n.t(
'Exceeded the number of seats in your license. Please contact support to increase the number of seats.'
)
}}
/>
{/if}
{#each $banners.filter( (b) => (b.dismissible ? !JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]').includes(b.id) : true) ) as banner}
<Banner
{banner}
on:dismiss={(e) => {
const bannerId = e.detail;
localStorage.setItem(
'dismissedBannerIds',
JSON.stringify(
[
bannerId,
...JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]')
].filter((id) => $banners.find((b) => b.id === id))
)
);
}}
/>
{/each}
</div>
</div>
{/if}
<Pane defaultSize={50} class="h-full flex relative max-w-full flex-col">
<Navbar
bind:this={navbarElement}
chat={{
id: $chatId,
chat: {
title: $chatTitle,
models: selectedModels,
system: $settings.system ?? undefined,
params: params,
history: history,
timestamp: Date.now()
}
}}
{history}
title={$chatTitle}
bind:selectedModels
shareEnabled={!!history.currentId}
{initNewChat}
/>
<div class="flex flex-col flex-auto z-10 w-full @container">
{#if $settings?.landingPageMode === 'chat' || createMessagesList(history, history.currentId).length > 0}
@@ -2033,6 +2021,7 @@
<div class=" pb-[1rem]">
<MessageInput
{history}
{taskIds}
{selectedModels}
bind:files
bind:prompt
+3 -3
View File
@@ -223,14 +223,14 @@
showControls.set(false);
}}
collapsible={true}
class="pt-8"
class=" z-10 "
>
{#if $showControls}
<div class="pr-4 pb-8 flex max-h-full min-h-full">
<div class="flex max-h-full min-h-full">
<div
class="w-full {($showOverview || $showArtifacts) && !$showCallOverlay
? ' '
: 'px-4 py-4 bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-100 dark:border-gray-850'} rounded-xl z-40 pointer-events-auto overflow-y-auto scrollbar-hidden"
: 'px-4 py-4 bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-100 dark:border-gray-850'} z-40 pointer-events-auto overflow-y-auto scrollbar-hidden"
>
{#if $showCallOverlay}
<div class="w-full h-full flex justify-center">
@@ -56,7 +56,7 @@
($i18n.language === 'dg-DG'
? `/doge.png`
: `${WEBUI_BASE_URL}/static/favicon.png`)}
class=" size-[2.7rem] rounded-full border-[1px] border-gray-200 dark:border-none"
class=" size-[2.7rem] rounded-full border-[1px] border-gray-100 dark:border-none"
alt="logo"
draggable="false"
/>
@@ -68,12 +68,12 @@
{#if $temporaryChatEnabled}
<Tooltip
content="This chat won't appear in history and your messages will not be saved."
className="w-fit"
placement="top-start"
content={$i18n.t('This chat won’t appear in history and your messages will not be saved.')}
className="w-full flex justify-center mb-0.5"
placement="top"
>
<div class="flex items-center gap-2 text-gray-500 font-medium text-lg my-2 w-fit">
<EyeSlash strokeWidth="2.5" className="size-5" /> Temporary Chat
<EyeSlash strokeWidth="2.5" className="size-5" />{$i18n.t('Temporary Chat')}
</div>
</Tooltip>
{/if}
@@ -86,7 +86,7 @@
{#if models[selectedModelIdx]?.name}
{models[selectedModelIdx]?.name}
{:else}
{$i18n.t('Hello, {{name}}', { name: $user.name })}
{$i18n.t('Hello, {{name}}', { name: $user?.name })}
{/if}
</div>
@@ -67,7 +67,7 @@
</div>
</Collapsible>
{#if $user.role === 'admin' || $user?.permissions.chat?.controls}
{#if $user?.role === 'admin' || $user?.permissions.chat?.controls}
<hr class="my-2 border-gray-50 dark:border-gray-700/10" />
<Collapsible title={$i18n.t('System Prompt')} open={true} buttonClassName="w-full">
+142 -192
View File
@@ -21,7 +21,12 @@
TTSWorker
} from '$lib/stores';
import { blobToFile, compressImage, createMessagesList, findWordIndices } from '$lib/utils';
import {
blobToFile,
compressImage,
createMessagesList,
extractCurlyBraceWords
} from '$lib/utils';
import { transcribeAudio } from '$lib/apis/audio';
import { uploadFile } from '$lib/apis/files';
import { generateAutoCompletion } from '$lib/apis';
@@ -47,6 +52,7 @@
import CommandLine from '../icons/CommandLine.svelte';
import { KokoroWorker } from '$lib/workers/KokoroWorker';
import ToolServersModal from './ToolServersModal.svelte';
import Wrench from '../icons/Wrench.svelte';
const i18n = getContext('i18n');
@@ -65,6 +71,7 @@
$: selectedModelIds = atSelectedModel !== undefined ? [atSelectedModel.id] : selectedModels;
export let history;
export let taskIds = null;
export let prompt = '';
export let files = [];
@@ -85,7 +92,7 @@
webSearchEnabled
});
let showToolServers = false;
let showTools = false;
let loaded = false;
let recording = false;
@@ -348,7 +355,7 @@
<FilesOverlay show={dragged} />
<ToolServersModal bind:show={showToolServers} />
<ToolServersModal bind:show={showTools} {selectedToolIds} />
{#if loaded}
<div class="w-full font-primary">
@@ -392,38 +399,6 @@
<div
class="px-3 pb-0.5 pt-1.5 text-left w-full flex flex-col absolute bottom-0 left-0 right-0 bg-linear-to-t from-white dark:from-gray-900 z-10"
>
{#if selectedToolIds.length > 0}
<div class="flex items-center justify-between w-full">
<div class="flex items-center gap-2.5 text-sm dark:text-gray-500">
<div class="pl-1">
<span class="relative flex size-2">
<span
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-yellow-400 opacity-75"
/>
<span class="relative inline-flex rounded-full size-2 bg-yellow-500" />
</span>
</div>
<div class=" text-ellipsis line-clamp-1 flex">
{#each selectedToolIds.map((id) => {
return $tools ? $tools.find((t) => t.id === id) : { id: id, name: id };
}) as tool, toolIdx (toolIdx)}
<Tooltip
content={tool?.meta?.description ?? ''}
className=" {toolIdx !== 0 ? 'pl-0.5' : ''} shrink-0"
placement="top"
>
{tool.name}
</Tooltip>
{#if toolIdx !== selectedToolIds.length - 1}
<span>, </span>
{/if}
{/each}
</div>
</div>
</div>
{/if}
{#if atSelectedModel !== undefined}
<div class="flex items-center justify-between w-full">
<div class="pl-[1px] flex items-center gap-2 text-sm dark:text-gray-500">
@@ -535,8 +510,8 @@
}}
>
<div
class="flex-1 flex flex-col relative w-full shadow-lg rounded-3xl border border-gray-100 dark:border-gray-850 hover:border-gray-200 focus-within:border-gray-200 hover:dark:border-gray-800 focus-within:dark:border-gray-800 transition px-1 bg-white/90 dark:bg-gray-400/5 dark:text-gray-100"
dir={$settings?.chatDirection ?? 'LTR'}
class="flex-1 flex flex-col relative w-full shadow-lg rounded-3xl border border-gray-50 dark:border-gray-850 hover:border-gray-100 focus-within:border-gray-100 hover:dark:border-gray-800 focus-within:dark:border-gray-800 transition px-1 bg-white/90 dark:bg-gray-400/5 dark:text-gray-100"
dir={$settings?.chatDirection ?? 'auto'}
>
{#if files.length > 0}
<div class="mx-2 mt-2.5 -mb-1 flex items-center flex-wrap gap-2">
@@ -631,6 +606,7 @@
{#if $settings?.richTextInput ?? true}
<div
class="scrollbar-hidden text-left bg-transparent dark:text-gray-100 outline-hidden w-full pt-3 px-1 resize-none h-fit max-h-80 overflow-auto"
id="chat-input-container"
>
<RichTextInput
bind:this={chatInputElement}
@@ -846,6 +822,7 @@
{:else}
<textarea
id="chat-input"
dir="auto"
bind:this={chatInputElement}
class="scrollbar-hidden bg-transparent dark:text-gray-100 outline-hidden w-full pt-3 px-1 resize-none"
placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
@@ -977,7 +954,7 @@
}
if (e.key === 'Tab') {
const words = findWordIndices(prompt);
const words = extractCurlyBraceWords(prompt);
if (words.length > 0) {
const word = words.at(0);
@@ -1057,8 +1034,8 @@
{/if}
</div>
<div class=" flex justify-between mt-1.5 mb-2.5 mx-0.5 max-w-full">
<div class="ml-1 self-end gap-0.5 flex items-center flex-1 max-w-[80%]">
<div class=" flex justify-between mt-1 mb-2.5 mx-0.5 max-w-full" dir="ltr">
<div class="ml-1 self-end flex items-center flex-1 max-w-[80%] gap-0.5">
<InputMenu
bind:selectedToolIds
{screenCaptureHandler}
@@ -1126,21 +1103,44 @@
</button>
</InputMenu>
<div class="flex gap-0.5 items-center overflow-x-auto scrollbar-none flex-1">
<div class="flex gap-1 items-center overflow-x-auto scrollbar-none flex-1">
{#if toolServers.length + selectedToolIds.length > 0}
<Tooltip
content={$i18n.t('{{COUNT}} Available Tools', {
COUNT: toolServers.length + selectedToolIds.length
})}
>
<button
class="translate-y-[0.5px] flex gap-1 items-center text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200 rounded-lg p-1 self-center transition"
aria-label="Available Tools"
type="button"
on:click={() => {
showTools = !showTools;
}}
>
<Wrench className="size-4" strokeWidth="1.75" />
<span class="text-sm font-medium text-gray-600 dark:text-gray-300">
{toolServers.length + selectedToolIds.length}
</span>
</button>
</Tooltip>
{/if}
{#if $_user}
{#if $config?.features?.enable_web_search && ($_user.role === 'admin' || $_user?.permissions?.features?.web_search)}
<Tooltip content={$i18n.t('Search the internet')} placement="top">
<button
on:click|preventDefault={() => (webSearchEnabled = !webSearchEnabled)}
type="button"
class="px-1.5 @xl:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden {webSearchEnabled ||
class="px-1.5 @xl:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden border {webSearchEnabled ||
($settings?.webSearch ?? false) === 'always'
? 'bg-blue-100 dark:bg-blue-500/20 text-blue-500 dark:text-blue-400'
: 'bg-transparent text-gray-600 dark:text-gray-300 border-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800'}"
? 'bg-blue-100 dark:bg-blue-500/20 border-blue-400/20 text-blue-500 dark:text-blue-400'
: 'bg-transparent border-transparent text-gray-600 dark:text-gray-300 border-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800'}"
>
<GlobeAlt className="size-5" strokeWidth="1.75" />
<span
class="hidden @xl:block whitespace-nowrap overflow-hidden text-ellipsis translate-y-[0.5px] mr-0.5"
class="hidden @xl:block whitespace-nowrap overflow-hidden text-ellipsis translate-y-[0.5px]"
>{$i18n.t('Web Search')}</span
>
</button>
@@ -1153,13 +1153,13 @@
on:click|preventDefault={() =>
(imageGenerationEnabled = !imageGenerationEnabled)}
type="button"
class="px-1.5 @xl:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden {imageGenerationEnabled
? 'bg-gray-100 dark:bg-gray-500/20 text-gray-600 dark:text-gray-400'
: 'bg-transparent text-gray-600 dark:text-gray-300 border-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 '}"
class="px-1.5 @xl:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden border {imageGenerationEnabled
? 'bg-gray-50 dark:bg-gray-400/10 border-gray-100 dark:border-gray-700 text-gray-600 dark:text-gray-400'
: 'bg-transparent border-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 '}"
>
<Photo className="size-5" strokeWidth="1.75" />
<span
class="hidden @xl:block whitespace-nowrap overflow-hidden text-ellipsis translate-y-[0.5px] mr-0.5"
class="hidden @xl:block whitespace-nowrap overflow-hidden text-ellipsis translate-y-[0.5px]"
>{$i18n.t('Image')}</span
>
</button>
@@ -1172,13 +1172,13 @@
on:click|preventDefault={() =>
(codeInterpreterEnabled = !codeInterpreterEnabled)}
type="button"
class="px-1.5 @xl:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden {codeInterpreterEnabled
? 'bg-gray-100 dark:bg-gray-500/20 text-gray-600 dark:text-gray-400'
: 'bg-transparent text-gray-600 dark:text-gray-300 border-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 '}"
class="px-1.5 @xl:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden border {codeInterpreterEnabled
? 'bg-gray-50 dark:bg-gray-400/10 border-gray-100 dark:border-gray-700 text-gray-600 dark:text-gray-400 '
: 'bg-transparent border-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 '}"
>
<CommandLine className="size-5" strokeWidth="1.75" />
<span
class="hidden @xl:block whitespace-nowrap overflow-hidden text-ellipsis translate-y-[0.5px] mr-0.5"
class="hidden @xl:block whitespace-nowrap overflow-hidden text-ellipsis translate-y-[0.5px]"
>{$i18n.t('Code Interpreter')}</span
>
</button>
@@ -1189,47 +1189,6 @@
</div>
<div class="self-end flex space-x-1 mr-1 shrink-0">
{#if toolServers.length > 0}
<Tooltip
content={$i18n.t('{{COUNT}} Available Tool Servers', {
COUNT: toolServers.length
})}
>
<button
class="translate-y-[1.5px] flex gap-1 items-center text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200 rounded-lg px-1.5 py-0.5 mr-0.5 self-center border border-gray-100 dark:border-gray-800 transition"
aria-label="Available Tool Servers"
type="button"
on:click={() => {
showToolServers = !showToolServers;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-3"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21.75 6.75a4.5 4.5 0 0 1-4.884 4.484c-1.076-.091-2.264.071-2.95.904l-7.152 8.684a2.548 2.548 0 1 1-3.586-3.586l8.684-7.152c.833-.686.995-1.874.904-2.95a4.5 4.5 0 0 1 6.336-4.486l-3.276 3.276a3.004 3.004 0 0 0 2.25 2.25l3.276-3.276c.256.565.398 1.192.398 1.852Z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M4.867 19.125h.008v.008h-.008v-.008Z"
/>
</svg>
<span class="text-xs">
{toolServers.length}
</span>
</button>
</Tooltip>
{/if}
{#if !history?.currentId || history.messages[history.currentId]?.done == true}
<Tooltip content={$i18n.t('Record voice')}>
<button
@@ -1279,104 +1238,7 @@
</Tooltip>
{/if}
{#if !history.currentId || history.messages[history.currentId]?.done == true}
{#if prompt === '' && files.length === 0}
<div class=" flex items-center">
<Tooltip content={$i18n.t('Call')}>
<button
class=" {webSearchEnabled ||
($settings?.webSearch ?? false) === 'always'
? 'bg-blue-500 text-white hover:bg-blue-400 '
: 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100'} transition rounded-full p-1.5 self-center"
type="button"
on:click={async () => {
if (selectedModels.length > 1) {
toast.error($i18n.t('Select only one model to call'));
return;
}
if ($config.audio.stt.engine === 'web') {
toast.error(
$i18n.t(
'Call feature is not supported when using Web STT engine'
)
);
return;
}
// check if user has access to getUserMedia
try {
let stream = await navigator.mediaDevices.getUserMedia({
audio: true
});
// If the user grants the permission, proceed to show the call overlay
if (stream) {
const tracks = stream.getTracks();
tracks.forEach((track) => track.stop());
}
stream = null;
if ($settings.audio?.tts?.engine === 'browser-kokoro') {
// If the user has not initialized the TTS worker, initialize it
if (!$TTSWorker) {
await TTSWorker.set(
new KokoroWorker({
dtype: $settings.audio?.tts?.engineConfig?.dtype ?? 'fp32'
})
);
await $TTSWorker.init();
}
}
showCallOverlay.set(true);
showControls.set(true);
} catch (err) {
// If the user denies the permission or an error occurs, show an error message
toast.error(
$i18n.t('Permission denied when accessing media devices')
);
}
}}
aria-label="Call"
>
<Headphone className="size-5" />
</button>
</Tooltip>
</div>
{:else}
<div class=" flex items-center">
<Tooltip content={$i18n.t('Send message')}>
<button
id="send-message-button"
class="{!(prompt === '' && files.length === 0)
? webSearchEnabled || ($settings?.webSearch ?? false) === 'always'
? 'bg-blue-500 text-white hover:bg-blue-400 '
: 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
: 'text-white bg-gray-200 dark:text-gray-900 dark:bg-gray-700 disabled'} transition rounded-full p-1.5 self-center"
type="submit"
disabled={prompt === '' && files.length === 0}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="size-5"
>
<path
fill-rule="evenodd"
d="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z"
clip-rule="evenodd"
/>
</svg>
</button>
</Tooltip>
</div>
{/if}
{:else}
{#if (taskIds && taskIds.length > 0) || (history.currentId && history.messages[history.currentId]?.done != true)}
<div class=" flex items-center">
<Tooltip content={$i18n.t('Stop')}>
<button
@@ -1400,6 +1262,94 @@
</button>
</Tooltip>
</div>
{:else if prompt === '' && files.length === 0}
<div class=" flex items-center">
<Tooltip content={$i18n.t('Call')}>
<button
class=" bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full p-1.5 self-center"
type="button"
on:click={async () => {
if (selectedModels.length > 1) {
toast.error($i18n.t('Select only one model to call'));
return;
}
if ($config.audio.stt.engine === 'web') {
toast.error(
$i18n.t('Call feature is not supported when using Web STT engine')
);
return;
}
// check if user has access to getUserMedia
try {
let stream = await navigator.mediaDevices.getUserMedia({
audio: true
});
// If the user grants the permission, proceed to show the call overlay
if (stream) {
const tracks = stream.getTracks();
tracks.forEach((track) => track.stop());
}
stream = null;
if ($settings.audio?.tts?.engine === 'browser-kokoro') {
// If the user has not initialized the TTS worker, initialize it
if (!$TTSWorker) {
await TTSWorker.set(
new KokoroWorker({
dtype: $settings.audio?.tts?.engineConfig?.dtype ?? 'fp32'
})
);
await $TTSWorker.init();
}
}
showCallOverlay.set(true);
showControls.set(true);
} catch (err) {
// If the user denies the permission or an error occurs, show an error message
toast.error(
$i18n.t('Permission denied when accessing media devices')
);
}
}}
aria-label="Call"
>
<Headphone className="size-5" />
</button>
</Tooltip>
</div>
{:else}
<div class=" flex items-center">
<Tooltip content={$i18n.t('Send message')}>
<button
id="send-message-button"
class="{!(prompt === '' && files.length === 0)
? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
: 'text-white bg-gray-200 dark:text-gray-900 dark:bg-gray-700 disabled'} transition rounded-full p-1.5 self-center"
type="submit"
disabled={prompt === '' && files.length === 0}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="size-5"
>
<path
fill-rule="evenodd"
d="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z"
clip-rule="evenodd"
/>
</svg>
</button>
</Tooltip>
</div>
{/if}
</div>
</div>
@@ -154,6 +154,14 @@
keys: ['name', 'description']
});
});
const decodeString = (str: string) => {
try {
return decodeURIComponent(str);
} catch (e) {
return str;
}
};
</script>
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
@@ -210,7 +218,7 @@
{/if}
<div class="line-clamp-1">
{decodeURIComponent(item?.name)}
{decodeString(item?.name)}
</div>
</div>
@@ -1,7 +1,7 @@
<script lang="ts">
import { prompts, settings, user } from '$lib/stores';
import {
findWordIndices,
extractCurlyBraceWords,
getUserPosition,
getFormattedDate,
getFormattedTime,
@@ -86,7 +86,7 @@
if (command.content.includes('{{USER_NAME}}')) {
console.log($user);
const name = $user.name || 'User';
const name = $user?.name || 'User';
text = text.replaceAll('{{USER_NAME}}', name);
}
@@ -127,29 +127,49 @@
const lastWord = lastLineWords.pop();
if ($settings?.richTextInput ?? true) {
lastLineWords.push(`${text.replace(/</g, '&lt;').replace(/>/g, '&gt;')}`);
lastLineWords.push(
`${text.replace(/</g, '&lt;').replace(/>/g, '&gt;').replaceAll('\n', '<br/>')}`
);
lines.push(lastLineWords.join(' '));
prompt = lines.join('<br/>');
} else {
lastLineWords.push(text);
lines.push(lastLineWords.join(' '));
prompt = lines.join('\n');
}
prompt = lines.join('\n');
const chatInputContainerElement = document.getElementById('chat-input-container');
const chatInputElement = document.getElementById('chat-input');
await tick();
if (chatInputContainerElement) {
chatInputContainerElement.style.height = '';
chatInputContainerElement.style.height =
Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
chatInputContainerElement.scrollTop = chatInputContainerElement.scrollHeight;
}
await tick();
if (chatInputElement) {
chatInputElement.focus();
chatInputElement.dispatchEvent(new Event('input'));
const words = extractCurlyBraceWords(prompt);
if (words.length > 0) {
const word = words.at(0);
const fullPrompt = prompt;
prompt = prompt.substring(0, word?.endIndex + 1);
await tick();
chatInputElement.scrollTop = chatInputElement.scrollHeight;
prompt = fullPrompt;
await tick();
chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1);
} else {
chatInputElement.scrollTop = chatInputElement.scrollHeight;
}
}
};
</script>
@@ -39,7 +39,7 @@
}
let fileUploadEnabled = true;
$: fileUploadEnabled = $user.role === 'admin' || $user?.permissions?.chat?.file_upload;
$: fileUploadEnabled = $user?.role === 'admin' || $user?.permissions?.chat?.file_upload;
const init = async () => {
if ($_tools === null) {
@@ -2,7 +2,7 @@
import { toast } from 'svelte-sonner';
import { createEventDispatcher, tick, getContext, onMount, onDestroy } from 'svelte';
import { config, settings } from '$lib/stores';
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
import { blobToFile, calculateSHA256, extractCurlyBraceWords } from '$lib/utils';
import { transcribeAudio } from '$lib/apis/audio';
@@ -159,7 +159,7 @@
};
const startRecording = async () => {
startDurationCounter();
loading = true;
stream = await navigator.mediaDevices.getUserMedia({
audio: {
@@ -171,6 +171,9 @@
mediaRecorder = new MediaRecorder(stream);
mediaRecorder.onstart = () => {
console.log('Recording started');
loading = false;
startDurationCounter();
audioChunks = [];
analyseAudio(stream);
};
+1 -14
View File
@@ -14,7 +14,7 @@
import { toast } from 'svelte-sonner';
import { getChatList, updateChatById } from '$lib/apis/chats';
import { copyToClipboard, findWordIndices } from '$lib/utils';
import { copyToClipboard, extractCurlyBraceWords } from '$lib/utils';
import Message from './Messages/Message.svelte';
import Loader from '../common/Loader.svelte';
@@ -406,19 +406,6 @@
}
prompt = text;
await tick();
const chatInputContainerElement = document.getElementById('chat-input-container');
if (chatInputContainerElement) {
prompt = p;
chatInputContainerElement.style.height = '';
chatInputContainerElement.style.height =
Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
chatInputContainerElement.focus();
}
await tick();
}}
/>
@@ -87,6 +87,14 @@
showRelevance = calculateShowRelevance(citations);
showPercentage = shouldShowPercentage(citations);
}
const decodeString = (str: string) => {
try {
return decodeURIComponent(str);
} catch (e) {
return str;
}
};
</script>
<CitationsModal
@@ -117,7 +125,7 @@
<div
class="flex-1 mx-1 truncate text-black/60 hover:text-black dark:text-white/60 dark:hover:text-white transition"
>
{decodeURIComponent(citation.source.name)}
{decodeString(citation.source.name)}
</div>
</button>
{/each}
@@ -157,7 +165,7 @@
</div>
{/if}
<div class="flex-1 mx-1 truncate">
{decodeURIComponent(citation.source.name)}
{decodeString(citation.source.name)}
</div>
</button>
{/each}
@@ -194,7 +202,7 @@
</div>
{/if}
<div class="flex-1 mx-1 truncate">
{decodeURIComponent(citation.source.name)}
{decodeString(citation.source.name)}
</div>
</button>
{/each}
@@ -14,6 +14,7 @@
let mergedDocuments = [];
function calculatePercentage(distance: number) {
if (typeof distance !== 'number') return null;
if (distance < 0) return 0;
if (distance > 1) return 100;
return Math.round(distance * 10000) / 100;
@@ -44,6 +45,14 @@
);
}
}
const decodeString = (str: string) => {
try {
return decodeURIComponent(str);
} catch (e) {
return str;
}
};
</script>
<Modal size="lg" bind:show>
@@ -98,7 +107,7 @@
: `#`}
target="_blank"
>
{decodeURIComponent(document?.metadata?.name ?? document.source.name)}
{decodeString(document?.metadata?.name ?? document.source.name)}
</a>
{#if document?.metadata?.page}
<span class="text-xs text-gray-500 dark:text-gray-400">
@@ -122,11 +131,14 @@
<div class="text-sm my-1 dark:text-gray-400 flex items-center gap-2 w-fit">
{#if showPercentage}
{@const percentage = calculatePercentage(document.distance)}
<span
class={`px-1 rounded-sm font-medium ${getRelevanceColor(percentage)}`}
>
{percentage.toFixed(2)}%
</span>
{#if typeof percentage === 'number'}
<span
class={`px-1 rounded-sm font-medium ${getRelevanceColor(percentage)}`}
>
{percentage.toFixed(2)}%
</span>
{/if}
<span class="text-gray-500 dark:text-gray-500">
({(document?.distance ?? 0).toFixed(4)})
</span>
@@ -4,7 +4,7 @@
const dispatch = createEventDispatcher();
import Markdown from './Markdown.svelte';
import { chatId, mobile, showArtifacts, showControls, showOverview } from '$lib/stores';
import { chatId, mobile, settings, showArtifacts, showControls, showOverview } from '$lib/stores';
import FloatingButtons from '../ContentRenderer/FloatingButtons.svelte';
import { createMessagesList } from '$lib/utils';
@@ -161,6 +161,7 @@
const { lang, code } = e.detail;
if (
($settings?.detectArtifacts ?? true) &&
(['html', 'svg'].includes(lang) || (lang === 'xml' && code.includes('svg'))) &&
!$mobile &&
$chatId
@@ -99,10 +99,12 @@ Renders the following Markdown as alerts:
> Example warning
-->
<div class={`border-l-2 pl-2 ${alertStyles[alert.type].border}`}>
<p class={alertStyles[alert.type].text}>
<div class={`border-l-4 pl-2.5 ${alertStyles[alert.type].border} my-0.5`}>
<div class="{alertStyles[alert.type].text} items-center flex gap-1 py-1.5">
<svelte:component this={alertStyles[alert.type].icon} className="inline-block size-4" />
<b>{alert.type}</b>
</p>
<MarkdownTokens id={`${id}-${tokenIdx}`} tokens={alert.tokens} {onTaskClick} {onSourceClick} />
<span class=" font-medium">{alert.type}</span>
</div>
<div class="pb-2">
<MarkdownTokens id={`${id}-${tokenIdx}`} tokens={alert.tokens} {onTaskClick} {onSourceClick} />
</div>
</div>
@@ -184,9 +184,9 @@
{/if}
{:else if token.type === 'list'}
{#if token.ordered}
<ol start={token.start || 1}>
<ol start={token.start || 1} dir="auto">
{#each token.items as item, itemIdx}
<li dir="auto" class="text-start">
<li class="text-start">
{#if item?.task}
<input
class=" translate-y-[1px] -translate-x-1"
@@ -216,9 +216,9 @@
{/each}
</ol>
{:else}
<ul>
<ul dir="auto">
{#each token.items as item, itemIdx}
<li dir="auto" class="text-start">
<li class="text-start">
{#if item?.task}
<input
class=" translate-y-[1px] -translate-x-1"
@@ -295,7 +295,7 @@
</p>
{:else if token.type === 'text'}
{#if top}
<p dir="auto">
<p>
{#if token.tokens}
<MarkdownInlineTokens id={`${id}-${tokenIdx}-t`} tokens={token.tokens} {onSourceClick} />
{:else}
@@ -24,7 +24,9 @@
getMessageContentParts,
sanitizeResponseContent,
createMessagesList,
formatDate
formatDate,
removeDetails,
removeAllDetails
} from '$lib/utils';
import { WEBUI_BASE_URL } from '$lib/constants';
@@ -152,6 +154,7 @@
let showRateComment = false;
const copyToClipboard = async (text) => {
text = removeAllDetails(text);
const res = await _copyToClipboard(text);
if (res) {
toast.success($i18n.t('Copying to clipboard was successful!'));
@@ -332,9 +335,37 @@
}
};
let preprocessedDetailsCache = [];
function preprocessForEditing(content: string): string {
// Replace <details>...</details> with unique ID placeholder
const detailsBlocks = [];
let i = 0;
content = content.replace(/<details[\s\S]*?<\/details>/gi, (match) => {
detailsBlocks.push(match);
return `<details id="__DETAIL_${i++}__"/>`;
});
// Store original blocks in the editedContent or globally (see merging later)
preprocessedDetailsCache = detailsBlocks;
return content;
}
function postprocessAfterEditing(content: string): string {
const restoredContent = content.replace(
/<details id="__DETAIL_(\d+)__"\/>/g,
(_, index) => preprocessedDetailsCache[parseInt(index)] || ''
);
return restoredContent;
}
const editMessageHandler = async () => {
edit = true;
editedContent = message.content;
editedContent = preprocessForEditing(message.content);
await tick();
@@ -343,7 +374,8 @@
};
const editMessageConfirmHandler = async () => {
editMessage(message.id, editedContent ? editedContent : '', false);
const messageContent = postprocessAfterEditing(editedContent ? editedContent : '');
editMessage(message.id, messageContent, false);
edit = false;
editedContent = '';
@@ -352,7 +384,9 @@
};
const saveAsCopyHandler = async () => {
editMessage(message.id, editedContent ? editedContent : '');
const messageContent = postprocessAfterEditing(editedContent ? editedContent : '');
editMessage(message.id, messageContent);
edit = false;
editedContent = '';
@@ -551,7 +585,7 @@
id="message-{message.id}"
dir={$settings.chatDirection}
>
<div class={`shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'}`}>
<div class={`shrink-0 ltr:mr-3 rtl:ml-3`}>
<ProfileImage
src={model?.info?.meta?.profile_image_url ??
($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
@@ -698,7 +732,7 @@
<div>
<button
id="save-new-message-button"
class=" px-4 py-2 bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 border dark:border-gray-700 text-gray-700 dark:text-gray-200 transition rounded-3xl"
class=" px-4 py-2 bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 border border-gray-100 dark:border-gray-700 text-gray-700 dark:text-gray-200 transition rounded-3xl"
on:click={() => {
saveAsCopyHandler();
}}
@@ -920,7 +954,7 @@
{#if message.done}
{#if !readOnly}
{#if $user.role === 'user' ? ($user?.permissions?.chat?.edit ?? true) : true}
{#if $user?.role === 'user' ? ($user?.permissions?.chat?.edit ?? true) : true}
<Tooltip content={$i18n.t('Edit')} placement="bottom">
<button
class="{isLastMessage
@@ -1053,7 +1087,7 @@
</button>
</Tooltip>
{#if $config?.features.enable_image_generation && ($user.role === 'admin' || $user?.permissions?.features?.image_generation) && !readOnly}
{#if $config?.features.enable_image_generation && ($user?.role === 'admin' || $user?.permissions?.features?.image_generation) && !readOnly}
<Tooltip content={$i18n.t('Generate Image')} placement="bottom">
<button
class="{isLastMessage
@@ -102,7 +102,7 @@
<div class=" flex w-full user-message" dir={$settings.chatDirection} id="message-{message.id}">
{#if !($settings?.chatBubble ?? true)}
<div class={`shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'}`}>
<div class={`shrink-0 ltr:mr-3 rtl:ml-3`}>
<ProfileImage
src={message.user
? ($models.find((m) => m.id === message.user)?.info?.meta?.profile_image_url ??
@@ -192,7 +192,7 @@
<div>
<button
id="save-edit-message-button"
class=" px-4 py-2 bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 border dark:border-gray-700 text-gray-700 dark:text-gray-200 transition rounded-3xl"
class=" px-4 py-2 bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 border border-gray-100 dark:border-gray-700 text-gray-700 dark:text-gray-200 transition rounded-3xl"
on:click={() => {
editMessageConfirmHandler(false);
}}
+53 -51
View File
@@ -45,7 +45,7 @@
label: model.name,
model: model
}))}
showTemporaryChatControl={$user.role === 'user'
showTemporaryChatControl={$user?.role === 'user'
? ($user?.permissions?.chat?.temporary ?? true) &&
!($user?.permissions?.chat?.temporary_enforced ?? false)
: true}
@@ -54,58 +54,60 @@
</div>
</div>
{#if selectedModelIdx === 0}
<div
class=" self-center mx-1 disabled:text-gray-600 disabled:hover:text-gray-600 -translate-y-[0.5px]"
>
<Tooltip content={$i18n.t('Add Model')}>
<button
class=" "
{disabled}
on:click={() => {
selectedModels = [...selectedModels, ''];
}}
aria-label="Add Model"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="size-3.5"
{#if $user?.role === 'admin' || ($user?.permissions?.chat?.multiple_models ?? true)}
{#if selectedModelIdx === 0}
<div
class=" self-center mx-1 disabled:text-gray-600 disabled:hover:text-gray-600 -translate-y-[0.5px]"
>
<Tooltip content={$i18n.t('Add Model')}>
<button
class=" "
{disabled}
on:click={() => {
selectedModels = [...selectedModels, ''];
}}
aria-label="Add Model"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m6-6H6" />
</svg>
</button>
</Tooltip>
</div>
{:else}
<div
class=" self-center mx-1 disabled:text-gray-600 disabled:hover:text-gray-600 -translate-y-[0.5px]"
>
<Tooltip content={$i18n.t('Remove Model')}>
<button
{disabled}
on:click={() => {
selectedModels.splice(selectedModelIdx, 1);
selectedModels = selectedModels;
}}
aria-label="Remove Model"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="size-3"
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="size-3.5"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m6-6H6" />
</svg>
</button>
</Tooltip>
</div>
{:else}
<div
class=" self-center mx-1 disabled:text-gray-600 disabled:hover:text-gray-600 -translate-y-[0.5px]"
>
<Tooltip content={$i18n.t('Remove Model')}>
<button
{disabled}
on:click={() => {
selectedModels.splice(selectedModelIdx, 1);
selectedModels = selectedModels;
}}
aria-label="Remove Model"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
</svg>
</button>
</Tooltip>
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="size-3"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
</svg>
</button>
</Tooltip>
</div>
{/if}
{/if}
</div>
{/each}
@@ -84,47 +84,49 @@
}
);
$: filteredItems = searchValue
? fuse
.search(searchValue)
.map((e) => {
return e.item;
})
.filter((item) => {
if (selectedTag === '') {
return true;
}
return (item.model?.tags ?? []).map((tag) => tag.name).includes(selectedTag);
})
.filter((item) => {
if (selectedConnectionType === '') {
return true;
} else if (selectedConnectionType === 'ollama') {
return item.model?.owned_by === 'ollama';
} else if (selectedConnectionType === 'openai') {
return item.model?.owned_by === 'openai';
} else if (selectedConnectionType === 'direct') {
return item.model?.direct;
}
})
: items
.filter((item) => {
if (selectedTag === '') {
return true;
}
return (item.model?.tags ?? []).map((tag) => tag.name).includes(selectedTag);
})
.filter((item) => {
if (selectedConnectionType === '') {
return true;
} else if (selectedConnectionType === 'ollama') {
return item.model?.owned_by === 'ollama';
} else if (selectedConnectionType === 'openai') {
return item.model?.owned_by === 'openai';
} else if (selectedConnectionType === 'direct') {
return item.model?.direct;
}
});
$: filteredItems = (
searchValue
? fuse
.search(searchValue)
.map((e) => {
return e.item;
})
.filter((item) => {
if (selectedTag === '') {
return true;
}
return (item.model?.tags ?? []).map((tag) => tag.name).includes(selectedTag);
})
.filter((item) => {
if (selectedConnectionType === '') {
return true;
} else if (selectedConnectionType === 'ollama') {
return item.model?.owned_by === 'ollama';
} else if (selectedConnectionType === 'openai') {
return item.model?.owned_by === 'openai';
} else if (selectedConnectionType === 'direct') {
return item.model?.direct;
}
})
: items
.filter((item) => {
if (selectedTag === '') {
return true;
}
return (item.model?.tags ?? []).map((tag) => tag.name).includes(selectedTag);
})
.filter((item) => {
if (selectedConnectionType === '') {
return true;
} else if (selectedConnectionType === 'ollama') {
return item.model?.owned_by === 'ollama';
} else if (selectedConnectionType === 'openai') {
return item.model?.owned_by === 'openai';
} else if (selectedConnectionType === 'direct') {
return item.model?.direct;
}
})
).filter((item) => !(item.model?.info?.meta?.hidden ?? false));
$: if (selectedTag || selectedConnectionType) {
resetView();
@@ -282,7 +284,10 @@
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
if (items) {
tags = items.flatMap((item) => item.model?.tags ?? []).map((tag) => tag.name);
tags = items
.filter((item) => !(item.model?.info?.meta?.hidden ?? false))
.flatMap((item) => item.model?.tags ?? [])
.map((tag) => tag.name);
// Remove duplicates and sort
tags = Array.from(new Set(tags)).sort((a, b) => a.localeCompare(b));
@@ -373,7 +378,7 @@
</div>
{/if}
<div class="px-3 mb-2 max-h-64 overflow-y-auto scrollbar-hidden group relative">
<div class="px-3 max-h-64 overflow-y-auto scrollbar-hidden group relative">
{#if tags && items.filter((item) => !(item.model?.info?.meta?.hidden ?? false)).length > 0}
<div
class=" flex w-full sticky top-0 z-10 bg-white dark:bg-gray-850 overflow-x-auto scrollbar-none"
@@ -388,18 +393,20 @@
class="flex gap-1 w-fit text-center text-sm font-medium rounded-full bg-transparent px-1.5 pb-0.5"
bind:this={tagsContainerElement}
>
<button
class="min-w-fit outline-none p-1.5 {selectedTag === '' &&
selectedConnectionType === ''
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
on:click={() => {
selectedConnectionType = '';
selectedTag = '';
}}
>
{$i18n.t('All')}
</button>
{#if (items.find((item) => item.model?.owned_by === 'ollama') && items.find((item) => item.model?.owned_by === 'openai')) || items.find((item) => item.model?.direct) || tags.length > 0}
<button
class="min-w-fit outline-none p-1.5 {selectedTag === '' &&
selectedConnectionType === ''
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
on:click={() => {
selectedConnectionType = '';
selectedTag = '';
}}
>
{$i18n.t('All')}
</button>
{/if}
{#if items.find((item) => item.model?.owned_by === 'ollama') && items.find((item) => item.model?.owned_by === 'openai')}
<button
@@ -457,7 +464,7 @@
</div>
{/if}
{#each filteredItems.filter((item) => !(item.model?.info?.meta?.hidden ?? false)) as item, index}
{#each filteredItems as item, index}
<button
aria-label="model-item"
class="flex w-full text-left font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-hidden transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-highlighted:bg-muted {index ===
@@ -634,7 +641,7 @@
</div>
{/each}
{#if !(searchValue.trim() in $MODEL_DOWNLOAD_POOL) && searchValue && ollamaVersion && $user.role === 'admin'}
{#if !(searchValue.trim() in $MODEL_DOWNLOAD_POOL) && searchValue && ollamaVersion && $user?.role === 'admin'}
<Tooltip
content={$i18n.t(`Pull "{{searchValue}}" from Ollama.com`, {
searchValue: searchValue
@@ -739,9 +746,7 @@
</div>
{#if showTemporaryChatControl}
<hr class="border-gray-100 dark:border-gray-800" />
<div class="flex items-center mx-2 my-2">
<div class="flex items-center mx-2 mb-2">
<button
class="flex justify-between w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 px-3 text-sm text-gray-700 dark:text-gray-100 outline-hidden transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-highlighted:bg-muted"
on:click={async () => {
+168 -113
View File
@@ -4,7 +4,9 @@
import {
WEBUI_NAME,
banners,
chatId,
config,
mobile,
settings,
showArchivedChats,
@@ -26,6 +28,7 @@
import AdjustmentsHorizontal from '../icons/AdjustmentsHorizontal.svelte';
import PencilSquare from '../icons/PencilSquare.svelte';
import Banner from '../common/Banner.svelte';
const i18n = getContext('i18n');
@@ -34,6 +37,7 @@
export let shareEnabled: boolean = false;
export let chat;
export let history;
export let selectedModels;
export let showModelSelector = true;
@@ -43,136 +47,187 @@
<ShareChatModal bind:show={showShareChatModal} chatId={$chatId} />
<nav class="sticky top-0 z-30 w-full px-1.5 py-1.5 -mb-8 flex items-center drag-region">
<div
class=" bg-linear-to-b via-50% from-white via-white to-transparent dark:from-gray-900 dark:via-gray-900 dark:to-transparent pointer-events-none absolute inset-0 -bottom-7 z-[-1]"
></div>
<nav class="sticky top-0 z-30 w-full py-1.5 -mb-8 flex flex-col items-center drag-region">
<div class="flex items-center w-full px-1.5">
<div
class=" bg-linear-to-b via-50% from-white via-white to-transparent dark:from-gray-900 dark:via-gray-900 dark:to-transparent pointer-events-none absolute inset-0 -bottom-7 z-[-1]"
></div>
<div class=" flex max-w-full w-full mx-auto px-1 pt-0.5 bg-transparent">
<div class="flex items-center w-full max-w-full">
<div
class="{$showSidebar
? 'md:hidden'
: ''} mr-1 self-start flex flex-none items-center text-gray-600 dark:text-gray-400"
>
<button
id="sidebar-toggle-button"
class="cursor-pointer px-2 py-2 flex rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
on:click={() => {
showSidebar.set(!$showSidebar);
}}
aria-label="Toggle Sidebar"
<div class=" flex max-w-full w-full mx-auto px-1 pt-0.5 bg-transparent">
<div class="flex items-center w-full max-w-full">
<div
class="{$showSidebar
? 'md:hidden'
: ''} mr-1 self-start flex flex-none items-center text-gray-600 dark:text-gray-400"
>
<div class=" m-auto self-center">
<MenuLines />
</div>
</button>
</div>
<button
id="sidebar-toggle-button"
class="cursor-pointer px-2 py-2 flex rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
on:click={() => {
showSidebar.set(!$showSidebar);
}}
aria-label="Toggle Sidebar"
>
<div class=" m-auto self-center">
<MenuLines />
</div>
</button>
</div>
<div
class="flex-1 overflow-hidden max-w-full py-0.5
<div
class="flex-1 overflow-hidden max-w-full py-0.5
{$showSidebar ? 'ml-1' : ''}
"
>
{#if showModelSelector}
<ModelSelector bind:selectedModels showSetDefault={!shareEnabled} />
{/if}
</div>
>
{#if showModelSelector}
<ModelSelector bind:selectedModels showSetDefault={!shareEnabled} />
{/if}
</div>
<div class="self-start flex flex-none items-center text-gray-600 dark:text-gray-400">
<!-- <div class="md:hidden flex self-center w-[1px] h-5 mx-2 bg-gray-300 dark:bg-stone-700" /> -->
{#if shareEnabled && chat && (chat.id || $temporaryChatEnabled)}
<Menu
{chat}
{shareEnabled}
shareHandler={() => {
showShareChatModal = !showShareChatModal;
}}
downloadHandler={() => {
showDownloadChatModal = !showDownloadChatModal;
}}
>
<div class="self-start flex flex-none items-center text-gray-600 dark:text-gray-400">
<!-- <div class="md:hidden flex self-center w-[1px] h-5 mx-2 bg-gray-300 dark:bg-stone-700" /> -->
{#if shareEnabled && chat && (chat.id || $temporaryChatEnabled)}
<Menu
{chat}
{shareEnabled}
shareHandler={() => {
showShareChatModal = !showShareChatModal;
}}
downloadHandler={() => {
showDownloadChatModal = !showDownloadChatModal;
}}
>
<button
class="flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
id="chat-context-menu-button"
>
<div class=" m-auto self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM12.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM18.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z"
/>
</svg>
</div>
</button>
</Menu>
{/if}
<Tooltip content={$i18n.t('Controls')}>
<button
class="flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
id="chat-context-menu-button"
class=" flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
on:click={async () => {
await showControls.set(!$showControls);
}}
aria-label="Controls"
>
<div class=" m-auto self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM12.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM18.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z"
/>
</svg>
<AdjustmentsHorizontal className=" size-5" strokeWidth="0.5" />
</div>
</button>
</Menu>
{/if}
</Tooltip>
<Tooltip content={$i18n.t('Controls')}>
<button
class=" flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
on:click={async () => {
await showControls.set(!$showControls);
}}
aria-label="Controls"
>
<div class=" m-auto self-center">
<AdjustmentsHorizontal className=" size-5" strokeWidth="0.5" />
</div>
</button>
</Tooltip>
<Tooltip content={$i18n.t('New Chat')}>
<button
id="new-chat-button"
class=" flex {$showSidebar
? 'md:hidden'
: ''} cursor-pointer px-2 py-2 rounded-xl text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-850 transition"
on:click={() => {
initNewChat();
}}
aria-label="New Chat"
>
<div class=" m-auto self-center">
<PencilSquare className=" size-5" strokeWidth="2" />
</div>
</button>
</Tooltip>
{#if $user !== undefined}
<UserMenu
className="max-w-[200px]"
role={$user.role}
on:show={(e) => {
if (e.detail === 'archived-chat') {
showArchivedChats.set(true);
}
}}
>
<Tooltip content={$i18n.t('New Chat')}>
<button
class="select-none flex rounded-xl p-1.5 w-full hover:bg-gray-50 dark:hover:bg-gray-850 transition"
aria-label="User Menu"
id="new-chat-button"
class=" flex {$showSidebar
? 'md:hidden'
: ''} cursor-pointer px-2 py-2 rounded-xl text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-850 transition"
on:click={() => {
initNewChat();
}}
aria-label="New Chat"
>
<div class=" self-center">
<img
src={$user.profile_image_url}
class="size-6 object-cover rounded-full"
alt="User profile"
draggable="false"
/>
<div class=" m-auto self-center">
<PencilSquare className=" size-5" strokeWidth="2" />
</div>
</button>
</UserMenu>
{/if}
</Tooltip>
{#if $user !== undefined && $user !== null}
<UserMenu
className="max-w-[200px]"
role={$user?.role}
on:show={(e) => {
if (e.detail === 'archived-chat') {
showArchivedChats.set(true);
}
}}
>
<button
class="select-none flex rounded-xl p-1.5 w-full hover:bg-gray-50 dark:hover:bg-gray-850 transition"
aria-label="User Menu"
>
<div class=" self-center">
<img
src={$user?.profile_image_url}
class="size-6 object-cover rounded-full"
alt="User profile"
draggable="false"
/>
</div>
</button>
</UserMenu>
{/if}
</div>
</div>
</div>
</div>
{#if !history.currentId && !$chatId && ($banners.length > 0 || ($config?.license_metadata?.type ?? null) === 'trial' || (($config?.license_metadata?.seats ?? null) !== null && $config?.user_count > $config?.license_metadata?.seats))}
<div class=" w-full z-30 mt-5">
<div class=" flex flex-col gap-1 w-full">
{#if ($config?.license_metadata?.type ?? null) === 'trial'}
<Banner
banner={{
type: 'info',
title: 'Trial License',
content: $i18n.t(
'You are currently using a trial license. Please contact support to upgrade your license.'
)
}}
/>
{/if}
{#if ($config?.license_metadata?.seats ?? null) !== null && $config?.user_count > $config?.license_metadata?.seats}
<Banner
banner={{
type: 'error',
title: 'License Error',
content: $i18n.t(
'Exceeded the number of seats in your license. Please contact support to increase the number of seats.'
)
}}
/>
{/if}
{#each $banners.filter( (b) => (b.dismissible ? !JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]').includes(b.id) : true) ) as banner}
<Banner
{banner}
on:dismiss={(e) => {
const bannerId = e.detail;
localStorage.setItem(
'dismissedBannerIds',
JSON.stringify(
[
bannerId,
...JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]')
].filter((id) => $banners.find((b) => b.id === id))
)
);
}}
/>
{/each}
</div>
</div>
{/if}
</nav>
+5 -7
View File
@@ -8,7 +8,7 @@
const dispatch = createEventDispatcher();
import { config, user, models as _models, temporaryChatEnabled } from '$lib/stores';
import { sanitizeResponseContent, findWordIndices } from '$lib/utils';
import { sanitizeResponseContent, extractCurlyBraceWords } from '$lib/utils';
import { WEBUI_BASE_URL } from '$lib/constants';
import Suggestions from './Suggestions.svelte';
@@ -65,9 +65,7 @@
const chatInputElement = document.getElementById('chat-input');
if (chatInputContainerElement) {
chatInputContainerElement.style.height = '';
chatInputContainerElement.style.height =
Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
chatInputContainerElement.scrollTop = chatInputContainerElement.scrollHeight;
}
await tick();
@@ -93,12 +91,12 @@
<div class="m-auto w-full max-w-6xl px-2 @2xl:px-20 translate-y-6 py-24 text-center">
{#if $temporaryChatEnabled}
<Tooltip
content="This chat won't appear in history and your messages will not be saved."
content={$i18n.t('This chat won’t appear in history and your messages will not be saved.')}
className="w-full flex justify-center mb-0.5"
placement="top"
>
<div class="flex items-center gap-2 text-gray-500 font-medium text-lg my-2 w-fit">
<EyeSlash strokeWidth="2.5" className="size-5" /> Temporary Chat
<EyeSlash strokeWidth="2.5" className="size-5" />{$i18n.t('Temporary Chat')}
</div>
</Tooltip>
{/if}
@@ -142,7 +140,7 @@
{#if models[selectedModelIdx]?.name}
{models[selectedModelIdx]?.name}
{:else}
{$i18n.t('Hello, {{name}}', { name: $user.name })}
{$i18n.t('Hello, {{name}}', { name: $user?.name })}
{/if}
</div>
</div>

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