Compare commits
538 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| be5534c655 | |||
| bcc2bab623 | |||
| 0260c714d9 | |||
| 009e85d56e | |||
| 4d27ecef1e | |||
| 6e6302a4a2 | |||
| 2abbc9092e | |||
| 4922b65753 | |||
| 8163902ad1 | |||
| 0f4492114f | |||
| bb6b363875 | |||
| e36be75754 | |||
| ec93fd15c3 | |||
| 5961fc84f4 | |||
| a5fa881d4a | |||
| 4fe237da58 | |||
| 6139d775ef | |||
| 8f83f51c68 | |||
| 4754e5c062 | |||
| 5b08dc56af | |||
| 8ec3934599 | |||
| 0d07d10bd8 | |||
| bbdfcd18e1 | |||
| febab58821 | |||
| 2638ae6a93 | |||
| a8c4588fb5 | |||
| 288d8a3e32 | |||
| 1fb5ef99e2 | |||
| ae35c68956 | |||
| 9ff74038b5 | |||
| 8c64f0627c | |||
| 5376525777 | |||
| 82bd1eb155 | |||
| 120f8d7786 | |||
| b9b25f872d | |||
| 123620bcc4 | |||
| dbdf83c55f | |||
| 066789d423 | |||
| 78cbdbc63f | |||
| d2249921aa | |||
| f8815b0749 | |||
| 68104358d0 | |||
| 676a4dffd0 | |||
| 0ede1d1483 | |||
| c5f13cfdda | |||
| 99dd81d700 | |||
| 22d23afbaf | |||
| b521a06b5f | |||
| 6d2785d58a | |||
| 3cd0141957 | |||
| 4edef53169 | |||
| 9a92ae78c8 | |||
| 8dac4e91a5 | |||
| 1e6ddb2fb3 | |||
| e5207e098e | |||
| 1b6700425a | |||
| 8766d2699a | |||
| 7f90d813bd | |||
| a8b92e5e9d | |||
| 0349c86448 | |||
| 75491b2f87 | |||
| 9e60d03911 | |||
| 04925407b2 | |||
| a121ae2c5d | |||
| 86eafdf5e4 | |||
| 12d59b6184 | |||
| 6f6b18e727 | |||
| 719a8a9327 | |||
| b8b9cab529 | |||
| 8a0fa0b493 | |||
| 0176edd4c7 | |||
| f9587032de | |||
| 4e43d31604 | |||
| 27c59d985f | |||
| 781c904c91 | |||
| 400bfa5a02 | |||
| 7facfa33d0 | |||
| 3aa6b0fea9 | |||
| 3890ea1490 | |||
| af5b2ddb01 | |||
| 5b2369da5d | |||
| 014e52c072 | |||
| fa54eb687e | |||
| 8181ae3918 | |||
| cdbabdfa5a | |||
| 400deb90b4 | |||
| 2c44443f93 | |||
| e5d574307e | |||
| 92c694e8ac | |||
| 6e19e46b07 | |||
| aa9f3d0632 | |||
| 85b2a63b6d | |||
| ef300248ba | |||
| 548160c739 | |||
| a4630a9825 | |||
| c7221983a5 | |||
| c7e1d3dbc7 | |||
| 5e3c801c44 | |||
| b27b035caa | |||
| a2933bf89d | |||
| 302a17b3ed | |||
| 01a656cc80 | |||
| 51a41c8db4 | |||
| 6dedf9885f | |||
| 4efb8638ff | |||
| 8f8ce26948 | |||
| 0a9092156c | |||
| 4950feded4 | |||
| 7db4baeffa | |||
| 18a7634f5c | |||
| 325fd2b97b | |||
| 1140036f20 | |||
| 58a13aec7e | |||
| 1dc80786c1 | |||
| ba61f87a2f | |||
| 34dd32d48f | |||
| 3f4eb6caa2 | |||
| 2e77ad87cc | |||
| 97f1d1665a | |||
| 4ccb5d39c7 | |||
| 16b218e686 | |||
| a3a832a935 | |||
| 9c389df727 | |||
| e29a999dc9 | |||
| b423e442d7 | |||
| d7ce488c42 | |||
| e7a7caf911 | |||
| 4a549f0c76 | |||
| e979584298 | |||
| 19203a8ed7 | |||
| 630c4d8bc6 | |||
| b6d36ab638 | |||
| e6d20df463 | |||
| de0f31680c | |||
| 39f91e39a7 | |||
| 967277f1d5 | |||
| f7fcf95689 | |||
| f2378be6f9 | |||
| db158fc9e1 | |||
| e30166118c | |||
| 9381113f60 | |||
| 85dbdd23fc | |||
| ea812a3bb8 | |||
| 240cbda06c | |||
| d4c193060e | |||
| 7d62993007 | |||
| 8f5c671e4c | |||
| 8d4c9c77bf | |||
| b263e24e56 | |||
| 290b4e56c8 | |||
| cf9dbea370 | |||
| 0722e4b690 | |||
| 308a2e1de0 | |||
| 13019acce6 | |||
| aaaef3efd8 | |||
| 5079d5ec87 | |||
| 1d98099099 | |||
| e9decf2dda | |||
| 210456e424 | |||
| c45bb64de7 | |||
| ea6f1a0e57 | |||
| a57a01a598 | |||
| 170361c27e | |||
| c489d473b9 | |||
| de75679ffb | |||
| ad67339426 | |||
| d88cc6007c | |||
| 370f182430 | |||
| 39fdd95a0c | |||
| e6ff5af38a | |||
| 767ae3be5b | |||
| 564a3a293f | |||
| 9ad4605e57 | |||
| 33b9629164 | |||
| 2290eefcb0 | |||
| 5369023e34 | |||
| 233dcb2d95 | |||
| edbe5673c5 | |||
| 1114f9625b | |||
| f42819c6ad | |||
| 8b0144cd06 | |||
| 0c033b5b7b | |||
| 7e0d3496b5 | |||
| 1888327a79 | |||
| fabcbc300f | |||
| 4f87512bbc | |||
| b5c759a059 | |||
| df167d8785 | |||
| bd82334647 | |||
| 8e5f96e4c3 | |||
| 88a94cc50d | |||
| ba13321728 | |||
| 4756511aea | |||
| 543f2652c2 | |||
| 4bd999af81 | |||
| 807be35d9f | |||
| 4997c8259b | |||
| 227a3a0d86 | |||
| e483aae1b8 | |||
| 7b864d967d | |||
| ca985cdb07 | |||
| eb87de9a63 | |||
| 6671e5f170 | |||
| 5e5473c792 | |||
| 09357b6e81 | |||
| b63aca534b | |||
| 31cb6c9857 | |||
| 5d64822c84 | |||
| 8e94db0d1b | |||
| 4fa8d2aa5a | |||
| 3a4f3e97e7 | |||
| 55060045da | |||
| e3e037e187 | |||
| 3113825895 | |||
| d2954f8eb6 | |||
| a789b785b4 | |||
| 34a6d55505 | |||
| a0dceb06a5 | |||
| 298e6848b3 | |||
| f712c90019 | |||
| 058eb76568 | |||
| a374405911 | |||
| ef8070d35d | |||
| 93aea0a4d4 | |||
| 9a95767062 | |||
| c1fc7bc764 | |||
| 038b6af813 | |||
| 1d9c745b35 | |||
| df3675aaf7 | |||
| 685ee9b714 | |||
| 90503be2ed | |||
| 26ee547f71 | |||
| 734e62b865 | |||
| 01e020d1ad | |||
| 943c49a07a | |||
| 88e8f31c07 | |||
| 1c2aa10796 | |||
| 78c2f16c3c | |||
| bcd5853ce7 | |||
| 61ffbf5b5b | |||
| 58cfa5b9c1 | |||
| b3c5ebb323 | |||
| 9f2b846ea7 | |||
| 1b2b05d2ea | |||
| 0db71e97bb | |||
| c6053476ca | |||
| b8d7fdf16e | |||
| b44ae536b9 | |||
| c99e351688 | |||
| 37abc1513c | |||
| 73689bb26c | |||
| 06cbe337de | |||
| d3822f782c | |||
| 87daf122db | |||
| 6023f86cf7 | |||
| c40e634391 | |||
| 01472c071b | |||
| 0d633c0d17 | |||
| 84ea10ab45 | |||
| 675328c881 | |||
| 421a964a9b | |||
| ceecfb66a5 | |||
| 20c4ada998 | |||
| 9fefddd844 | |||
| 7dade0715d | |||
| 9c190b56ae | |||
| 72675ec15f | |||
| 1ea91f9060 | |||
| 34632b7aac | |||
| 2969918d19 | |||
| 32f74c9fc7 | |||
| 0ed5405571 | |||
| 6d99ef8b4b | |||
| c63176402c | |||
| ea69c24bf7 | |||
| a32a752791 | |||
| 68c182db96 | |||
| 876853f2b0 | |||
| 838c6f8217 | |||
| 1b5d88d0c0 | |||
| 39bc3b508c | |||
| 5661de3221 | |||
| 3f14d8bf0b | |||
| 242e70d181 | |||
| 7ce157a8b8 | |||
| a2d0f9ce1d | |||
| 06683f5ac0 | |||
| 9f4a71029f | |||
| cc72341861 | |||
| 61bb1f1dc8 | |||
| cbd1929f36 | |||
| 3723f42f1d | |||
| 03955d6a42 | |||
| d27f80542c | |||
| f32094dd41 | |||
| 3404b8dd13 | |||
| 11dcbb2e15 | |||
| 8d5f35a404 | |||
| 755dd85a20 | |||
| 55321a3b12 | |||
| ab43bd17f6 | |||
| bfb8b08723 | |||
| 04a9115254 | |||
| 7874bea5f4 | |||
| e3a49c8e9b | |||
| f58eb0d266 | |||
| 5d6517c537 | |||
| 66e7e577df | |||
| 9b59039388 | |||
| 9aa689ea56 | |||
| 7f617225a0 | |||
| bf604bc07c | |||
| 485469683e | |||
| e98cb45238 | |||
| c84e0aa2a3 | |||
| aef2a514d1 | |||
| 64ed0d1089 | |||
| 4c490132ba | |||
| 38590da0b1 | |||
| e70cdf118b | |||
| 4c6567c46f | |||
| c3425f3bf1 | |||
| 2571f3c43b | |||
| 0872bea790 | |||
| ba09fcd548 | |||
| 546d553803 | |||
| aa0bf79330 | |||
| cecb87b8c2 | |||
| 07b39d1cb9 | |||
| 95f579cabe | |||
| 7f1acf9c2d | |||
| 6fa88854e3 | |||
| 2dcd9e75a1 | |||
| 5f7188b480 | |||
| 761c66a8d8 | |||
| 0334ab7c5d | |||
| e31cf08188 | |||
| b8c97fa012 | |||
| e92311b4eb | |||
| 594f2f264f | |||
| c2f429425e | |||
| 2fec591b59 | |||
| 9c1f224219 | |||
| af1d8470cd | |||
| 3a2f1d56c8 | |||
| a341cfe3fd | |||
| 36d8442c92 | |||
| cb2e989408 | |||
| c0fbf1e7d0 | |||
| 1c39e5cff9 | |||
| 24c0dbec0d | |||
| 0a8dae633b | |||
| 53dae39042 | |||
| 84ec58873a | |||
| d4db3dd593 | |||
| fa3c7bc218 | |||
| facb6edfdc | |||
| e8085f80a7 | |||
| ecde76270c | |||
| 70ae053a46 | |||
| 3e6b94dbc1 | |||
| e3a3a034c1 | |||
| 46027cc390 | |||
| e4c77628be | |||
| ecfb6891f6 | |||
| 115c3a38a7 | |||
| 9d8c23b3b2 | |||
| 30b053116d | |||
| 0921317b2b | |||
| f1963c95c9 | |||
| c2c89525bb | |||
| 47565065f6 | |||
| ecc841c57d | |||
| eaaa75387f | |||
| 61a8f73291 | |||
| 9df4227db4 | |||
| 683e6ca337 | |||
| b1eb08b44a | |||
| 989a4d3165 | |||
| 443a256f72 | |||
| 482f010d22 | |||
| fc86b20350 | |||
| 1f98c091f0 | |||
| dc187d35a1 | |||
| e0908f9f26 | |||
| 75911f6e4e | |||
| a2ebbd02ad | |||
| a19d4267f7 | |||
| 444f20198a | |||
| a79618ee3e | |||
| 38ff3209ad | |||
| 2789102d27 | |||
| c1f88eb0ad | |||
| e57c0c301c | |||
| 5cf621396d | |||
| 2bece7b4c5 | |||
| 7a96893093 | |||
| 180fd44a4d | |||
| a2edd2d911 | |||
| e6cf552aad | |||
| be6cc1c53a | |||
| f50ffc07d7 | |||
| e700552f93 | |||
| d680d52b85 | |||
| bf35297e4a | |||
| c91b9af0e0 | |||
| 82a61e72e1 | |||
| 52fb09ff95 | |||
| 572cd22b4d | |||
| 9ee39aa07f | |||
| 372284b419 | |||
| 714bdca3f3 | |||
| 5613af032d | |||
| 8fb5e22e43 | |||
| b19e05669e | |||
| 35b5d5ba35 | |||
| 84cd1b8cb8 | |||
| fffd283b0c | |||
| 6d378844ae | |||
| 68cfccedee | |||
| a433315346 | |||
| 77cc3b5470 | |||
| dc412d21f7 | |||
| 96d9d3447b | |||
| aed078b580 | |||
| 33c337785c | |||
| 20a6dbbe65 | |||
| bd3b5f1edb | |||
| e5703a588f | |||
| 3431a93499 | |||
| 8745091a16 | |||
| e6bcdba5ad | |||
| 96af34f240 | |||
| b7fcf14f6e | |||
| ebc6a269d3 | |||
| 0595c04909 | |||
| e60c87d750 | |||
| d5bcae6182 | |||
| dbc3aa7234 | |||
| e626fb2dbf | |||
| 34be6de4d8 | |||
| 20be93217b | |||
| c580d29a5d | |||
| f337ddb267 | |||
| 1e05caf809 | |||
| 44884a8886 | |||
| a01fd15812 | |||
| d513629984 | |||
| f1acf68bd0 | |||
| 3c9fc7858b | |||
| bf2ff47df0 | |||
| 6318282873 | |||
| 3043e43418 | |||
| 595ebd11ac | |||
| 1157b8e12d | |||
| f653944849 | |||
| 01c077da3d | |||
| 27ff386115 | |||
| 35437fb3a3 | |||
| 18e5c2e4cb | |||
| 2674656b49 | |||
| fe56c0a7ac | |||
| ddac5284fe | |||
| b2020383dd | |||
| 563377a58a | |||
| eadb671414 | |||
| b3ccabd2fe | |||
| e8bf596959 | |||
| abc9b6b3f3 | |||
| 79bbc2be23 | |||
| b763535c92 | |||
| de62153d49 | |||
| 80413a7628 | |||
| 3f0fae1d10 | |||
| dd52ea9d3d | |||
| 30d1048099 | |||
| 142f82bae5 | |||
| c0bb32d768 | |||
| 5e168e6fef | |||
| 4bf6e64c35 | |||
| 1afc49c1e4 | |||
| eebabd84ef | |||
| 6b04380c85 | |||
| f3199c6510 | |||
| c523126bae | |||
| 45f34924b0 | |||
| 25af07a0c7 | |||
| bac28222fb | |||
| 1c4e63f71e | |||
| e8abaa8bea | |||
| 5b8fd14470 | |||
| d4339fe769 | |||
| 877ed69004 | |||
| e360d57203 | |||
| 0235b0f6f3 | |||
| a128dfa06b | |||
| ffc91ba6b6 | |||
| 24c000739c | |||
| 00367ef23e | |||
| 527a0b329b | |||
| 2e4bba2374 | |||
| 4cafea0b97 | |||
| cfc9cc8a10 | |||
| c7fa024b8f | |||
| cc50746043 | |||
| bf0ea4dc5a | |||
| 9ddc243c52 | |||
| 9832e6edba | |||
| afc78b0277 | |||
| f561e94244 | |||
| eed6c7194b | |||
| 167b5712ea | |||
| 7bc17da534 | |||
| 24497592a8 | |||
| ed9e99e946 | |||
| d42517b7ed | |||
| 93d4f987e7 | |||
| 6a1d60b1b3 | |||
| 19843e39fd | |||
| ec18489723 | |||
| 9130428a55 | |||
| 236ec28491 | |||
| f070e8b7f9 | |||
| 3321a1b922 | |||
| f935acc6bf | |||
| 6d90d130bd | |||
| bfd33ec0df | |||
| 9af6c5300b | |||
| 5c887d0709 | |||
| c9589e2118 | |||
| 75cbbbe52d | |||
| 0455b80604 | |||
| 71a75bccf5 | |||
| 97609970b2 | |||
| 48ed701960 | |||
| d401d77cbd | |||
| 5e97c9927b | |||
| 76660f3e44 |
+2
-1
@@ -10,7 +10,8 @@ OPENAI_API_KEY=''
|
||||
# DO NOT TRACK
|
||||
SCARF_NO_ANALYTICS=true
|
||||
DO_NOT_TRACK=true
|
||||
ANONYMIZED_TELEMETRY=false
|
||||
|
||||
# Use locally bundled version of the LiteLLM cost map json
|
||||
# to avoid repetitive startup connections
|
||||
LITELLM_LOCAL_MODEL_COST_MAP="True"
|
||||
LITELLM_LOCAL_MODEL_COST_MAP="True"
|
||||
@@ -0,0 +1,11 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: pip
|
||||
directory: '/backend'
|
||||
schedule:
|
||||
interval: weekly
|
||||
- package-ecosystem: 'github-actions'
|
||||
directory: '/'
|
||||
schedule:
|
||||
# Check for updates to GitHub Actions every week
|
||||
interval: 'weekly'
|
||||
@@ -1,37 +1,56 @@
|
||||
## Pull Request Checklist
|
||||
# Pull Request Checklist
|
||||
|
||||
- [ ] **Description:** Briefly describe the changes in this pull request.
|
||||
### Note to first-time contributors: Please open a discussion post in [Discussions](https://github.com/open-webui/open-webui/discussions) and describe your changes before submitting a pull request.
|
||||
|
||||
**Before submitting, make sure you've checked the following:**
|
||||
|
||||
- [ ] **Target branch:** Please verify that the pull request targets the `dev` branch.
|
||||
- [ ] **Description:** Provide a concise description of the changes made in this pull request.
|
||||
- [ ] **Changelog:** Ensure a changelog entry following the format of [Keep a Changelog](https://keepachangelog.com/) is added at the bottom of the PR description.
|
||||
- [ ] **Documentation:** Have you updated relevant documentation [Open WebUI Docs](https://github.com/open-webui/docs), or other documentation sources?
|
||||
- [ ] **Dependencies:** Are there any new dependencies? Have you updated the dependency versions in the documentation?
|
||||
- [ ] **Testing:** Have you written and run sufficient tests for the changes?
|
||||
- [ ] **Code Review:** Have you self-reviewed your code and addressed any coding standard issues?
|
||||
- [ ] **Testing:** Have you written and run sufficient tests for validating the changes?
|
||||
- [ ] **Code review:** Have you performed a self-review of your code, addressing any coding standard issues and ensuring adherence to the project's coding standards?
|
||||
- [ ] **Label:** To cleary categorize this pull request, assign a relevant label to the pull request title, using one of the following:
|
||||
- **BREAKING CHANGE**: Significant changes that may affect compatibility
|
||||
- **build**: Changes that affect the build system or external dependencies
|
||||
- **ci**: Changes to our continuous integration processes or workflows
|
||||
- **chore**: Refactor, cleanup, or other non-functional code changes
|
||||
- **docs**: Documentation update or addition
|
||||
- **feat**: Introduces a new feature or enhancement to the codebase
|
||||
- **fix**: Bug fix or error correction
|
||||
- **i18n**: Internationalization or localization changes
|
||||
- **perf**: Performance improvement
|
||||
- **refactor**: Code restructuring for better maintainability, readability, or scalability
|
||||
- **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc.)
|
||||
- **test**: Adding missing tests or correcting existing tests
|
||||
- **WIP**: Work in progress, a temporary label for incomplete or ongoing work
|
||||
|
||||
---
|
||||
# Changelog Entry
|
||||
|
||||
## Description
|
||||
### Description
|
||||
|
||||
[Insert a brief description of the changes made in this pull request, including any relevant motivation and impact.]
|
||||
|
||||
---
|
||||
|
||||
### Changelog Entry
|
||||
- [Concisely describe the changes made in this pull request, including any relevant motivation and impact (e.g., fixing a bug, adding a feature, or improving performance)]
|
||||
|
||||
### Added
|
||||
|
||||
- [List any new features, functionalities, or additions]
|
||||
|
||||
### Fixed
|
||||
|
||||
- [List any fixes, corrections, or bug fixes]
|
||||
|
||||
### Changed
|
||||
|
||||
- [List any changes, updates, refactorings, or optimizations]
|
||||
|
||||
### Deprecated
|
||||
|
||||
- [List any deprecated functionality or features that have been removed]
|
||||
|
||||
### Removed
|
||||
|
||||
- [List any removed features, files, or deprecated functionalities]
|
||||
- [List any removed features, files, or functionalities]
|
||||
|
||||
### Fixed
|
||||
|
||||
- [List any fixes, corrections, or bug fixes]
|
||||
|
||||
### Security
|
||||
|
||||
@@ -39,12 +58,15 @@
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- [List any breaking changes affecting compatibility or functionality]
|
||||
- **BREAKING CHANGE**: [List any breaking changes affecting compatibility or functionality]
|
||||
|
||||
---
|
||||
|
||||
### Additional Information
|
||||
|
||||
- [Insert any additional context, notes, or explanations for the changes]
|
||||
- [Reference any related issues, commits, or other relevant information]
|
||||
|
||||
- [Reference any related issues, commits, or other relevant information]
|
||||
### Screenshots or Videos
|
||||
|
||||
- [Attach any relevant screenshots or videos demonstrating the changes]
|
||||
|
||||
@@ -29,8 +29,29 @@ jobs:
|
||||
- name: Format Frontend
|
||||
run: npm run format
|
||||
|
||||
- name: Run i18next
|
||||
run: npm run i18n:parse
|
||||
|
||||
- name: Check for Changes After Format
|
||||
run: git diff --exit-code
|
||||
|
||||
- name: Build Frontend
|
||||
run: npm run build
|
||||
|
||||
test-frontend:
|
||||
name: 'Frontend Unit Tests'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run vitest
|
||||
run: npm run test:frontend
|
||||
|
||||
@@ -20,7 +20,16 @@ jobs:
|
||||
|
||||
- name: Build and run Compose Stack
|
||||
run: |
|
||||
docker compose up --detach --build
|
||||
docker compose --file docker-compose.yaml --file docker-compose.api.yaml up --detach --build
|
||||
|
||||
- name: Wait for Ollama to be up
|
||||
timeout-minutes: 5
|
||||
run: |
|
||||
until curl --output /dev/null --silent --fail http://localhost:11434; do
|
||||
printf '.'
|
||||
sleep 1
|
||||
done
|
||||
echo "Service is up!"
|
||||
|
||||
- name: Preload Ollama model
|
||||
run: |
|
||||
@@ -53,3 +62,134 @@ jobs:
|
||||
name: compose-logs
|
||||
path: compose-logs.txt
|
||||
if-no-files-found: ignore
|
||||
|
||||
migration_test:
|
||||
name: Run Migration Tests
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
# mysql:
|
||||
# image: mysql
|
||||
# env:
|
||||
# MYSQL_ROOT_PASSWORD: mysql
|
||||
# MYSQL_DATABASE: mysql
|
||||
# options: >-
|
||||
# --health-cmd "mysqladmin ping -h localhost"
|
||||
# --health-interval 10s
|
||||
# --health-timeout 5s
|
||||
# --health-retries 5
|
||||
# ports:
|
||||
# - 3306:3306
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Set up uv
|
||||
uses: yezz123/setup-uv@v4
|
||||
with:
|
||||
uv-venv: venv
|
||||
|
||||
- name: Activate virtualenv
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
echo PATH=$PATH >> $GITHUB_ENV
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
uv pip install -r backend/requirements.txt
|
||||
|
||||
- name: Test backend with SQLite
|
||||
id: sqlite
|
||||
env:
|
||||
WEBUI_SECRET_KEY: secret-key
|
||||
GLOBAL_LOG_LEVEL: debug
|
||||
run: |
|
||||
cd backend
|
||||
uvicorn main:app --port "8080" --forwarded-allow-ips '*' &
|
||||
UVICORN_PID=$!
|
||||
# Wait up to 20 seconds for the server to start
|
||||
for i in {1..20}; do
|
||||
curl -s http://localhost:8080/api/config > /dev/null && break
|
||||
sleep 1
|
||||
if [ $i -eq 20 ]; then
|
||||
echo "Server failed to start"
|
||||
kill -9 $UVICORN_PID
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
# Check that the server is still running after 5 seconds
|
||||
sleep 5
|
||||
if ! kill -0 $UVICORN_PID; then
|
||||
echo "Server has stopped"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
- name: Test backend with Postgres
|
||||
if: success() || steps.sqlite.conclusion == 'failure'
|
||||
env:
|
||||
WEBUI_SECRET_KEY: secret-key
|
||||
GLOBAL_LOG_LEVEL: debug
|
||||
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
|
||||
run: |
|
||||
cd backend
|
||||
uvicorn main:app --port "8081" --forwarded-allow-ips '*' &
|
||||
UVICORN_PID=$!
|
||||
# Wait up to 20 seconds for the server to start
|
||||
for i in {1..20}; do
|
||||
curl -s http://localhost:8081/api/config > /dev/null && break
|
||||
sleep 1
|
||||
if [ $i -eq 20 ]; then
|
||||
echo "Server failed to start"
|
||||
kill -9 $UVICORN_PID
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
# Check that the server is still running after 5 seconds
|
||||
sleep 5
|
||||
if ! kill -0 $UVICORN_PID; then
|
||||
echo "Server has stopped"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# - name: Test backend with MySQL
|
||||
# if: success() || steps.sqlite.conclusion == 'failure' || steps.postgres.conclusion == 'failure'
|
||||
# env:
|
||||
# WEBUI_SECRET_KEY: secret-key
|
||||
# GLOBAL_LOG_LEVEL: debug
|
||||
# DATABASE_URL: mysql://root:mysql@localhost:3306/mysql
|
||||
# run: |
|
||||
# cd backend
|
||||
# uvicorn main:app --port "8083" --forwarded-allow-ips '*' &
|
||||
# UVICORN_PID=$!
|
||||
# # Wait up to 20 seconds for the server to start
|
||||
# for i in {1..20}; do
|
||||
# curl -s http://localhost:8083/api/config > /dev/null && break
|
||||
# sleep 1
|
||||
# if [ $i -eq 20 ]; then
|
||||
# echo "Server failed to start"
|
||||
# kill -9 $UVICORN_PID
|
||||
# exit 1
|
||||
# fi
|
||||
# done
|
||||
# # Check that the server is still running after 5 seconds
|
||||
# sleep 5
|
||||
# if ! kill -0 $UVICORN_PID; then
|
||||
# echo "Server has stopped"
|
||||
# exit 1
|
||||
# fi
|
||||
|
||||
@@ -16,6 +16,10 @@ __pycache__/
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Pyodide distribution
|
||||
static/pyodide/*
|
||||
!static/pyodide/pyodide-lock.json
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
|
||||
+306
-6
@@ -1,3 +1,11 @@
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
kubernetes/
|
||||
|
||||
# Copy of .gitignore
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
@@ -6,11 +14,303 @@ node_modules
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Ignore kubernetes files
|
||||
kubernetes
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
.idea/
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
# cypress artifacts
|
||||
cypress/videos
|
||||
cypress/screenshots
|
||||
|
||||
|
||||
|
||||
/static/*
|
||||
@@ -5,6 +5,73 @@ 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.1.125] - 2024-05-19
|
||||
|
||||
### Added
|
||||
|
||||
- **🔄 Updated UI**: Chat interface revamped with chat bubbles. Easily switch back to the old style via settings > interface > chat bubble UI.
|
||||
- **📂 Enhanced Sidebar UI**: Model files, documents, prompts, and playground merged into Workspace for streamlined access.
|
||||
- **🚀 Improved Many Model Interaction**: All responses now displayed simultaneously for a smoother experience.
|
||||
- **🐍 Python Code Execution**: Execute Python code locally in the browser with libraries like 'requests', 'beautifulsoup4', 'numpy', 'pandas', 'seaborn', 'matplotlib', 'scikit-learn', 'scipy', 'regex'.
|
||||
- **🧠 Experimental Memory Feature**: Manually input personal information you want LLMs to remember via settings > personalization > memory.
|
||||
- **💾 Persistent Settings**: Settings now saved as config.json for convenience.
|
||||
- **🩺 Health Check Endpoint**: Added for Docker deployment.
|
||||
- **↕️ RTL Support**: Toggle chat direction via settings > interface > chat direction.
|
||||
- **🖥️ PowerPoint Support**: RAG pipeline now supports PowerPoint documents.
|
||||
- **🌐 Language Updates**: Ukrainian, Turkish, Arabic, Chinese, Serbian, Vietnamese updated; Punjabi added.
|
||||
|
||||
### Changed
|
||||
|
||||
- **👤 Shared Chat Update**: Shared chat now includes creator user information.
|
||||
|
||||
## [0.1.124] - 2024-05-08
|
||||
|
||||
### Added
|
||||
|
||||
- **🖼️ Improved Chat Sidebar**: Now conveniently displays time ranges and organizes chats by today, yesterday, and more.
|
||||
- **📜 Citations in RAG Feature**: Easily track the context fed to the LLM with added citations in the RAG feature.
|
||||
- **🔒 Auth Disable Option**: Introducing the ability to disable authentication. Set 'WEBUI_AUTH' to False to disable authentication. Note: Only applicable for fresh installations without existing users.
|
||||
- **📹 Enhanced YouTube RAG Pipeline**: Now supports non-English videos for an enriched experience.
|
||||
- **🔊 Specify OpenAI TTS Models**: Customize your TTS experience by specifying OpenAI TTS models.
|
||||
- **🔧 Additional Environment Variables**: Discover more environment variables in our comprehensive documentation at Open WebUI Documentation (https://docs.openwebui.com).
|
||||
- **🌐 Language Support**: Arabic, Finnish, and Hindi added; Improved support for German, Vietnamese, and Chinese.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🛠️ Model Selector Styling**: Addressed styling issues for improved user experience.
|
||||
- **⚠️ Warning Messages**: Resolved backend warning messages.
|
||||
|
||||
### Changed
|
||||
|
||||
- **📝 Title Generation**: Limited output to 50 tokens.
|
||||
- **📦 Helm Charts**: Removed Helm charts, now available in a separate repository (https://github.com/open-webui/helm-charts).
|
||||
|
||||
## [0.1.123] - 2024-05-02
|
||||
|
||||
### Added
|
||||
|
||||
- **🎨 New Landing Page Design**: Refreshed design for a more modern look and optimized use of screen space.
|
||||
- **📹 Youtube RAG Pipeline**: Introduces dedicated RAG pipeline for Youtube videos, enabling interaction with video transcriptions directly.
|
||||
- **🔧 Enhanced Admin Panel**: Streamlined user management with options to add users directly or in bulk via CSV import.
|
||||
- **👥 '@' Model Integration**: Easily switch to specific models during conversations; old collaborative chat feature phased out.
|
||||
- **🌐 Language Enhancements**: Swedish translation added, plus improvements to German, Spanish, and the addition of Doge translation.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🗑️ Delete Chat Shortcut**: Addressed issue where shortcut wasn't functioning.
|
||||
- **🖼️ Modal Closing Bug**: Resolved unexpected closure of modal when dragging from within.
|
||||
- **✏️ Edit Button Styling**: Fixed styling inconsistency with edit buttons.
|
||||
- **🌐 Image Generation Compatibility Issue**: Rectified image generation compatibility issue with third-party APIs.
|
||||
- **📱 iOS PWA Icon Fix**: Corrected iOS PWA home screen icon shape.
|
||||
- **🔍 Scroll Gesture Bug**: Adjusted gesture sensitivity to prevent accidental activation when scrolling through code on mobile; now requires scrolling from the leftmost side to open the sidebar.
|
||||
|
||||
### Changed
|
||||
|
||||
- **🔄 Unlimited Context Length**: Advanced settings now allow unlimited max context length (previously limited to 16000).
|
||||
- **👑 Super Admin Assignment**: The first signup is automatically assigned a super admin role, unchangeable by other admins.
|
||||
- **🛡️ Admin User Restrictions**: User action buttons from the admin panel are now disabled for users with admin roles.
|
||||
- **🔝 Default Model Selector**: Set as default model option now exclusively available on the landing page.
|
||||
|
||||
## [0.1.122] - 2024-04-27
|
||||
|
||||
### Added
|
||||
|
||||
+59
-34
@@ -11,6 +11,9 @@ ARG USE_CUDA_VER=cu121
|
||||
# IMPORTANT: If you change the embedding model (sentence-transformers/all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them.
|
||||
ARG USE_EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
|
||||
ARG USE_RERANKING_MODEL=""
|
||||
# Override at your own risk - non-root configurations are untested
|
||||
ARG UID=0
|
||||
ARG GID=0
|
||||
|
||||
######## WebUI frontend ########
|
||||
FROM --platform=$BUILDPLATFORM node:21-alpine3.19 as build
|
||||
@@ -32,6 +35,8 @@ ARG USE_OLLAMA
|
||||
ARG USE_CUDA_VER
|
||||
ARG USE_EMBEDDING_MODEL
|
||||
ARG USE_RERANKING_MODEL
|
||||
ARG UID
|
||||
ARG GID
|
||||
|
||||
## Basis ##
|
||||
ENV ENV=prod \
|
||||
@@ -51,7 +56,8 @@ ENV OLLAMA_BASE_URL="/ollama" \
|
||||
ENV OPENAI_API_KEY="" \
|
||||
WEBUI_SECRET_KEY="" \
|
||||
SCARF_NO_ANALYTICS=true \
|
||||
DO_NOT_TRACK=true
|
||||
DO_NOT_TRACK=true \
|
||||
ANONYMIZED_TELEMETRY=false
|
||||
|
||||
# Use locally bundled version of the LiteLLM cost map json
|
||||
# to avoid repetitive startup connections
|
||||
@@ -74,43 +80,58 @@ ENV HF_HOME="/app/backend/data/cache/embedding/models"
|
||||
|
||||
WORKDIR /app/backend
|
||||
|
||||
ENV HOME /root
|
||||
# Create user and group if not root
|
||||
RUN if [ $UID -ne 0 ]; then \
|
||||
if [ $GID -ne 0 ]; then \
|
||||
addgroup --gid $GID app; \
|
||||
fi; \
|
||||
adduser --uid $UID --gid $GID --home $HOME --disabled-password --no-create-home app; \
|
||||
fi
|
||||
|
||||
RUN mkdir -p $HOME/.cache/chroma
|
||||
RUN echo -n 00000000-0000-0000-0000-000000000000 > $HOME/.cache/chroma/telemetry_user_id
|
||||
|
||||
# Make sure the user has access to the app and root directory
|
||||
RUN chown -R $UID:$GID /app $HOME
|
||||
|
||||
RUN if [ "$USE_OLLAMA" = "true" ]; then \
|
||||
apt-get update && \
|
||||
# Install pandoc and netcat
|
||||
apt-get install -y --no-install-recommends pandoc netcat-openbsd && \
|
||||
# for RAG OCR
|
||||
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
|
||||
# install helper tools
|
||||
apt-get install -y --no-install-recommends curl && \
|
||||
# install ollama
|
||||
curl -fsSL https://ollama.com/install.sh | sh && \
|
||||
# cleanup
|
||||
rm -rf /var/lib/apt/lists/*; \
|
||||
apt-get update && \
|
||||
# Install pandoc and netcat
|
||||
apt-get install -y --no-install-recommends pandoc netcat-openbsd curl && \
|
||||
# for RAG OCR
|
||||
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
|
||||
# install helper tools
|
||||
apt-get install -y --no-install-recommends curl jq && \
|
||||
# install ollama
|
||||
curl -fsSL https://ollama.com/install.sh | sh && \
|
||||
# cleanup
|
||||
rm -rf /var/lib/apt/lists/*; \
|
||||
else \
|
||||
apt-get update && \
|
||||
# Install pandoc and netcat
|
||||
apt-get install -y --no-install-recommends pandoc netcat-openbsd && \
|
||||
# for RAG OCR
|
||||
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
|
||||
# cleanup
|
||||
rm -rf /var/lib/apt/lists/*; \
|
||||
apt-get update && \
|
||||
# Install pandoc and netcat
|
||||
apt-get install -y --no-install-recommends pandoc netcat-openbsd curl jq && \
|
||||
# for RAG OCR
|
||||
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
|
||||
# cleanup
|
||||
rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
# install python dependencies
|
||||
COPY ./backend/requirements.txt ./requirements.txt
|
||||
COPY --chown=$UID:$GID ./backend/requirements.txt ./requirements.txt
|
||||
|
||||
RUN pip3 install uv && \
|
||||
if [ "$USE_CUDA" = "true" ]; then \
|
||||
# If you use CUDA the whisper and embedding model will be downloaded on first use
|
||||
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \
|
||||
uv pip install --system -r requirements.txt --no-cache-dir && \
|
||||
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
|
||||
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
|
||||
# If you use CUDA the whisper and embedding model will be downloaded on first use
|
||||
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \
|
||||
uv pip install --system -r requirements.txt --no-cache-dir && \
|
||||
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
|
||||
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
|
||||
else \
|
||||
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \
|
||||
uv pip install --system -r requirements.txt --no-cache-dir && \
|
||||
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
|
||||
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
|
||||
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \
|
||||
uv pip install --system -r requirements.txt --no-cache-dir && \
|
||||
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
|
||||
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
|
||||
fi
|
||||
|
||||
|
||||
@@ -120,13 +141,17 @@ RUN pip3 install uv && \
|
||||
# COPY --from=build /app/onnx /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2/onnx
|
||||
|
||||
# copy built frontend files
|
||||
COPY --from=build /app/build /app/build
|
||||
COPY --from=build /app/CHANGELOG.md /app/CHANGELOG.md
|
||||
COPY --from=build /app/package.json /app/package.json
|
||||
COPY --chown=$UID:$GID --from=build /app/build /app/build
|
||||
COPY --chown=$UID:$GID --from=build /app/CHANGELOG.md /app/CHANGELOG.md
|
||||
COPY --chown=$UID:$GID --from=build /app/package.json /app/package.json
|
||||
|
||||
# copy backend files
|
||||
COPY ./backend .
|
||||
COPY --chown=$UID:$GID ./backend .
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD [ "bash", "start.sh"]
|
||||
HEALTHCHECK CMD curl --silent --fail http://localhost:8080/health | jq -e '.status == true' || exit 1
|
||||
|
||||
USER $UID:$GID
|
||||
|
||||
CMD [ "bash", "start.sh"]
|
||||
|
||||
@@ -120,22 +120,66 @@ Don't forget to explore our sibling project, [Open WebUI Community](https://open
|
||||
> [!TIP]
|
||||
> If you wish to utilize Open WebUI with Ollama included or CUDA acceleration, we recommend utilizing our official images tagged with either `:cuda` or `:ollama`. To enable CUDA, you must install the [Nvidia CUDA container toolkit](https://docs.nvidia.com/dgx/nvidia-container-runtime-upgrade/) on your Linux/WSL system.
|
||||
|
||||
**If Ollama is on your computer**, use this command:
|
||||
### Installation with Default Configuration
|
||||
|
||||
```bash
|
||||
docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
|
||||
```
|
||||
- **If Ollama is on your computer**, use this command:
|
||||
|
||||
**If Ollama is on a Different Server**, use this command:
|
||||
```bash
|
||||
docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
|
||||
```
|
||||
|
||||
To connect to Ollama on another server, change the `OLLAMA_BASE_URL` to the server's URL:
|
||||
- **If Ollama is on a Different Server**, use this command:
|
||||
|
||||
```bash
|
||||
docker run -d -p 3000:8080 -e OLLAMA_BASE_URL=https://example.com -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
|
||||
```
|
||||
To connect to Ollama on another server, change the `OLLAMA_BASE_URL` to the server's URL:
|
||||
|
||||
```bash
|
||||
docker run -d -p 3000:8080 -e OLLAMA_BASE_URL=https://example.com -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
|
||||
```
|
||||
|
||||
- **To run Open WebUI with Nvidia GPU support**, use this command:
|
||||
|
||||
```bash
|
||||
docker run -d -p 3000:8080 --gpus all --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:cuda
|
||||
```
|
||||
|
||||
### Installation for OpenAI API Usage Only
|
||||
|
||||
- **If you're only using OpenAI API**, use this command:
|
||||
|
||||
```bash
|
||||
docker run -d -p 3000:8080 -e OPENAI_API_KEY=your_secret_key -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
|
||||
```
|
||||
|
||||
### Installing Open WebUI with Bundled Ollama Support
|
||||
|
||||
This installation method uses a single container image that bundles Open WebUI with Ollama, allowing for a streamlined setup via a single command. Choose the appropriate command based on your hardware setup:
|
||||
|
||||
- **With GPU Support**:
|
||||
Utilize GPU resources by running the following command:
|
||||
|
||||
```bash
|
||||
docker run -d -p 3000:8080 --gpus=all -v ollama:/root/.ollama -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:ollama
|
||||
```
|
||||
|
||||
- **For CPU Only**:
|
||||
If you're not using a GPU, use this command instead:
|
||||
|
||||
```bash
|
||||
docker run -d -p 3000:8080 -v ollama:/root/.ollama -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:ollama
|
||||
```
|
||||
|
||||
Both commands facilitate a built-in, hassle-free installation of both Open WebUI and Ollama, ensuring that you can get everything up and running swiftly.
|
||||
|
||||
After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000). Enjoy! 😄
|
||||
|
||||
### Other Installation Methods
|
||||
|
||||
We offer various installation alternatives, including non-Docker native installation methods, Docker Compose, Kustomize, and Helm. Visit our [Open WebUI Documentation](https://docs.openwebui.com/getting-started/) or join our [Discord community](https://discord.gg/5rJgQTnV4s) for comprehensive guidance.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
Encountering connection issues? Our [Open WebUI Documentation](https://docs.openwebui.com/troubleshooting/) has got you covered. For further assistance and to join our vibrant community, visit the [Open WebUI Discord](https://discord.gg/5rJgQTnV4s).
|
||||
|
||||
#### Open WebUI: Server Connection Error
|
||||
|
||||
If you're experiencing connection issues, it’s often due to the WebUI docker container not being able to reach the Ollama server at 127.0.0.1:11434 (host.docker.internal:11434) inside the container . Use the `--network=host` flag in your docker command to resolve this. Note that the port changes from 3000 to 8080, resulting in the link: `http://localhost:8080`.
|
||||
@@ -146,14 +190,6 @@ If you're experiencing connection issues, it’s often due to the WebUI docker c
|
||||
docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://127.0.0.1:11434 --name open-webui --restart always ghcr.io/open-webui/open-webui:main
|
||||
```
|
||||
|
||||
### Other Installation Methods
|
||||
|
||||
We offer various installation alternatives, including non-Docker methods, Docker Compose, Kustomize, and Helm. Visit our [Open WebUI Documentation](https://docs.openwebui.com/getting-started/) or join our [Discord community](https://discord.gg/5rJgQTnV4s) for comprehensive guidance.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
Encountering connection issues? Our [Open WebUI Documentation](https://docs.openwebui.com/getting-started/troubleshooting/) has got you covered. For further assistance and to join our vibrant community, visit the [Open WebUI Discord](https://discord.gg/5rJgQTnV4s).
|
||||
|
||||
### Keeping Your Docker Installation Up-to-Date
|
||||
|
||||
In case you want to update your local Docker installation to the latest version, you can do it with [Watchtower](https://containrrr.dev/watchtower/):
|
||||
|
||||
+24
-11
@@ -43,6 +43,9 @@ from config import (
|
||||
DEVICE_TYPE,
|
||||
AUDIO_OPENAI_API_BASE_URL,
|
||||
AUDIO_OPENAI_API_KEY,
|
||||
AUDIO_OPENAI_API_MODEL,
|
||||
AUDIO_OPENAI_API_VOICE,
|
||||
AppConfig,
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -57,9 +60,11 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
app.state.OPENAI_API_BASE_URL = AUDIO_OPENAI_API_BASE_URL
|
||||
app.state.OPENAI_API_KEY = AUDIO_OPENAI_API_KEY
|
||||
app.state.config = AppConfig()
|
||||
app.state.config.OPENAI_API_BASE_URL = AUDIO_OPENAI_API_BASE_URL
|
||||
app.state.config.OPENAI_API_KEY = AUDIO_OPENAI_API_KEY
|
||||
app.state.config.OPENAI_API_MODEL = AUDIO_OPENAI_API_MODEL
|
||||
app.state.config.OPENAI_API_VOICE = AUDIO_OPENAI_API_VOICE
|
||||
|
||||
# setting device type for whisper model
|
||||
whisper_device_type = DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == "cuda" else "cpu"
|
||||
@@ -72,13 +77,17 @@ SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
class OpenAIConfigUpdateForm(BaseModel):
|
||||
url: str
|
||||
key: str
|
||||
model: str
|
||||
speaker: str
|
||||
|
||||
|
||||
@app.get("/config")
|
||||
async def get_openai_config(user=Depends(get_admin_user)):
|
||||
return {
|
||||
"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL,
|
||||
"OPENAI_API_KEY": app.state.OPENAI_API_KEY,
|
||||
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
|
||||
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
|
||||
"OPENAI_API_MODEL": app.state.config.OPENAI_API_MODEL,
|
||||
"OPENAI_API_VOICE": app.state.config.OPENAI_API_VOICE,
|
||||
}
|
||||
|
||||
|
||||
@@ -89,13 +98,17 @@ async def update_openai_config(
|
||||
if form_data.key == "":
|
||||
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
|
||||
|
||||
app.state.OPENAI_API_BASE_URL = form_data.url
|
||||
app.state.OPENAI_API_KEY = form_data.key
|
||||
app.state.config.OPENAI_API_BASE_URL = form_data.url
|
||||
app.state.config.OPENAI_API_KEY = form_data.key
|
||||
app.state.config.OPENAI_API_MODEL = form_data.model
|
||||
app.state.config.OPENAI_API_VOICE = form_data.speaker
|
||||
|
||||
return {
|
||||
"status": True,
|
||||
"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL,
|
||||
"OPENAI_API_KEY": app.state.OPENAI_API_KEY,
|
||||
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
|
||||
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
|
||||
"OPENAI_API_MODEL": app.state.config.OPENAI_API_MODEL,
|
||||
"OPENAI_API_VOICE": app.state.config.OPENAI_API_VOICE,
|
||||
}
|
||||
|
||||
|
||||
@@ -112,13 +125,13 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
||||
return FileResponse(file_path)
|
||||
|
||||
headers = {}
|
||||
headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEY}"
|
||||
headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEY}"
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
r = None
|
||||
try:
|
||||
r = requests.post(
|
||||
url=f"{app.state.OPENAI_API_BASE_URL}/audio/speech",
|
||||
url=f"{app.state.config.OPENAI_API_BASE_URL}/audio/speech",
|
||||
data=body,
|
||||
headers=headers,
|
||||
stream=True,
|
||||
|
||||
+140
-92
@@ -24,6 +24,7 @@ from utils.misc import calculate_sha256
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
from pathlib import Path
|
||||
import mimetypes
|
||||
import uuid
|
||||
import base64
|
||||
import json
|
||||
@@ -41,6 +42,7 @@ from config import (
|
||||
IMAGE_GENERATION_MODEL,
|
||||
IMAGE_SIZE,
|
||||
IMAGE_STEPS,
|
||||
AppConfig,
|
||||
)
|
||||
|
||||
|
||||
@@ -59,26 +61,31 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.state.ENGINE = IMAGE_GENERATION_ENGINE
|
||||
app.state.ENABLED = ENABLE_IMAGE_GENERATION
|
||||
app.state.config = AppConfig()
|
||||
|
||||
app.state.OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL
|
||||
app.state.OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
|
||||
app.state.config.ENGINE = IMAGE_GENERATION_ENGINE
|
||||
app.state.config.ENABLED = ENABLE_IMAGE_GENERATION
|
||||
|
||||
app.state.MODEL = IMAGE_GENERATION_MODEL
|
||||
app.state.config.OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL
|
||||
app.state.config.OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
|
||||
|
||||
app.state.config.MODEL = IMAGE_GENERATION_MODEL
|
||||
|
||||
|
||||
app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
|
||||
app.state.COMFYUI_BASE_URL = COMFYUI_BASE_URL
|
||||
app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
|
||||
app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
|
||||
|
||||
|
||||
app.state.IMAGE_SIZE = IMAGE_SIZE
|
||||
app.state.IMAGE_STEPS = IMAGE_STEPS
|
||||
app.state.config.IMAGE_SIZE = IMAGE_SIZE
|
||||
app.state.config.IMAGE_STEPS = IMAGE_STEPS
|
||||
|
||||
|
||||
@app.get("/config")
|
||||
async def get_config(request: Request, user=Depends(get_admin_user)):
|
||||
return {"engine": app.state.ENGINE, "enabled": app.state.ENABLED}
|
||||
return {
|
||||
"engine": app.state.config.ENGINE,
|
||||
"enabled": app.state.config.ENABLED,
|
||||
}
|
||||
|
||||
|
||||
class ConfigUpdateForm(BaseModel):
|
||||
@@ -88,9 +95,12 @@ class ConfigUpdateForm(BaseModel):
|
||||
|
||||
@app.post("/config/update")
|
||||
async def update_config(form_data: ConfigUpdateForm, user=Depends(get_admin_user)):
|
||||
app.state.ENGINE = form_data.engine
|
||||
app.state.ENABLED = form_data.enabled
|
||||
return {"engine": app.state.ENGINE, "enabled": app.state.ENABLED}
|
||||
app.state.config.ENGINE = form_data.engine
|
||||
app.state.config.ENABLED = form_data.enabled
|
||||
return {
|
||||
"engine": app.state.config.ENGINE,
|
||||
"enabled": app.state.config.ENABLED,
|
||||
}
|
||||
|
||||
|
||||
class EngineUrlUpdateForm(BaseModel):
|
||||
@@ -101,8 +111,8 @@ class EngineUrlUpdateForm(BaseModel):
|
||||
@app.get("/url")
|
||||
async def get_engine_url(user=Depends(get_admin_user)):
|
||||
return {
|
||||
"AUTOMATIC1111_BASE_URL": app.state.AUTOMATIC1111_BASE_URL,
|
||||
"COMFYUI_BASE_URL": app.state.COMFYUI_BASE_URL,
|
||||
"AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
|
||||
"COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
|
||||
}
|
||||
|
||||
|
||||
@@ -112,29 +122,29 @@ async def update_engine_url(
|
||||
):
|
||||
|
||||
if form_data.AUTOMATIC1111_BASE_URL == None:
|
||||
app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
|
||||
app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
|
||||
else:
|
||||
url = form_data.AUTOMATIC1111_BASE_URL.strip("/")
|
||||
try:
|
||||
r = requests.head(url)
|
||||
app.state.AUTOMATIC1111_BASE_URL = url
|
||||
app.state.config.AUTOMATIC1111_BASE_URL = url
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
||||
|
||||
if form_data.COMFYUI_BASE_URL == None:
|
||||
app.state.COMFYUI_BASE_URL = COMFYUI_BASE_URL
|
||||
app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
|
||||
else:
|
||||
url = form_data.COMFYUI_BASE_URL.strip("/")
|
||||
|
||||
try:
|
||||
r = requests.head(url)
|
||||
app.state.COMFYUI_BASE_URL = url
|
||||
app.state.config.COMFYUI_BASE_URL = url
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
||||
|
||||
return {
|
||||
"AUTOMATIC1111_BASE_URL": app.state.AUTOMATIC1111_BASE_URL,
|
||||
"COMFYUI_BASE_URL": app.state.COMFYUI_BASE_URL,
|
||||
"AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
|
||||
"COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
|
||||
"status": True,
|
||||
}
|
||||
|
||||
@@ -147,8 +157,8 @@ class OpenAIConfigUpdateForm(BaseModel):
|
||||
@app.get("/openai/config")
|
||||
async def get_openai_config(user=Depends(get_admin_user)):
|
||||
return {
|
||||
"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL,
|
||||
"OPENAI_API_KEY": app.state.OPENAI_API_KEY,
|
||||
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
|
||||
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
|
||||
}
|
||||
|
||||
|
||||
@@ -159,13 +169,13 @@ async def update_openai_config(
|
||||
if form_data.key == "":
|
||||
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
|
||||
|
||||
app.state.OPENAI_API_BASE_URL = form_data.url
|
||||
app.state.OPENAI_API_KEY = form_data.key
|
||||
app.state.config.OPENAI_API_BASE_URL = form_data.url
|
||||
app.state.config.OPENAI_API_KEY = form_data.key
|
||||
|
||||
return {
|
||||
"status": True,
|
||||
"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL,
|
||||
"OPENAI_API_KEY": app.state.OPENAI_API_KEY,
|
||||
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
|
||||
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
|
||||
}
|
||||
|
||||
|
||||
@@ -175,7 +185,7 @@ class ImageSizeUpdateForm(BaseModel):
|
||||
|
||||
@app.get("/size")
|
||||
async def get_image_size(user=Depends(get_admin_user)):
|
||||
return {"IMAGE_SIZE": app.state.IMAGE_SIZE}
|
||||
return {"IMAGE_SIZE": app.state.config.IMAGE_SIZE}
|
||||
|
||||
|
||||
@app.post("/size/update")
|
||||
@@ -184,9 +194,9 @@ async def update_image_size(
|
||||
):
|
||||
pattern = r"^\d+x\d+$" # Regular expression pattern
|
||||
if re.match(pattern, form_data.size):
|
||||
app.state.IMAGE_SIZE = form_data.size
|
||||
app.state.config.IMAGE_SIZE = form_data.size
|
||||
return {
|
||||
"IMAGE_SIZE": app.state.IMAGE_SIZE,
|
||||
"IMAGE_SIZE": app.state.config.IMAGE_SIZE,
|
||||
"status": True,
|
||||
}
|
||||
else:
|
||||
@@ -202,7 +212,7 @@ class ImageStepsUpdateForm(BaseModel):
|
||||
|
||||
@app.get("/steps")
|
||||
async def get_image_size(user=Depends(get_admin_user)):
|
||||
return {"IMAGE_STEPS": app.state.IMAGE_STEPS}
|
||||
return {"IMAGE_STEPS": app.state.config.IMAGE_STEPS}
|
||||
|
||||
|
||||
@app.post("/steps/update")
|
||||
@@ -210,9 +220,9 @@ async def update_image_size(
|
||||
form_data: ImageStepsUpdateForm, user=Depends(get_admin_user)
|
||||
):
|
||||
if form_data.steps >= 0:
|
||||
app.state.IMAGE_STEPS = form_data.steps
|
||||
app.state.config.IMAGE_STEPS = form_data.steps
|
||||
return {
|
||||
"IMAGE_STEPS": app.state.IMAGE_STEPS,
|
||||
"IMAGE_STEPS": app.state.config.IMAGE_STEPS,
|
||||
"status": True,
|
||||
}
|
||||
else:
|
||||
@@ -225,14 +235,14 @@ async def update_image_size(
|
||||
@app.get("/models")
|
||||
def get_models(user=Depends(get_current_user)):
|
||||
try:
|
||||
if app.state.ENGINE == "openai":
|
||||
if app.state.config.ENGINE == "openai":
|
||||
return [
|
||||
{"id": "dall-e-2", "name": "DALL·E 2"},
|
||||
{"id": "dall-e-3", "name": "DALL·E 3"},
|
||||
]
|
||||
elif app.state.ENGINE == "comfyui":
|
||||
elif app.state.config.ENGINE == "comfyui":
|
||||
|
||||
r = requests.get(url=f"{app.state.COMFYUI_BASE_URL}/object_info")
|
||||
r = requests.get(url=f"{app.state.config.COMFYUI_BASE_URL}/object_info")
|
||||
info = r.json()
|
||||
|
||||
return list(
|
||||
@@ -244,7 +254,7 @@ def get_models(user=Depends(get_current_user)):
|
||||
|
||||
else:
|
||||
r = requests.get(
|
||||
url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models"
|
||||
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models"
|
||||
)
|
||||
models = r.json()
|
||||
return list(
|
||||
@@ -254,23 +264,29 @@ def get_models(user=Depends(get_current_user)):
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
app.state.ENABLED = False
|
||||
app.state.config.ENABLED = False
|
||||
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
||||
|
||||
|
||||
@app.get("/models/default")
|
||||
async def get_default_model(user=Depends(get_admin_user)):
|
||||
try:
|
||||
if app.state.ENGINE == "openai":
|
||||
return {"model": app.state.MODEL if app.state.MODEL else "dall-e-2"}
|
||||
elif app.state.ENGINE == "comfyui":
|
||||
return {"model": app.state.MODEL if app.state.MODEL else ""}
|
||||
if app.state.config.ENGINE == "openai":
|
||||
return {
|
||||
"model": (
|
||||
app.state.config.MODEL if app.state.config.MODEL else "dall-e-2"
|
||||
)
|
||||
}
|
||||
elif app.state.config.ENGINE == "comfyui":
|
||||
return {"model": (app.state.config.MODEL if app.state.config.MODEL else "")}
|
||||
else:
|
||||
r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options")
|
||||
r = requests.get(
|
||||
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options"
|
||||
)
|
||||
options = r.json()
|
||||
return {"model": options["sd_model_checkpoint"]}
|
||||
except Exception as e:
|
||||
app.state.ENABLED = False
|
||||
app.state.config.ENABLED = False
|
||||
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
||||
|
||||
|
||||
@@ -279,20 +295,20 @@ class UpdateModelForm(BaseModel):
|
||||
|
||||
|
||||
def set_model_handler(model: str):
|
||||
if app.state.ENGINE == "openai":
|
||||
app.state.MODEL = model
|
||||
return app.state.MODEL
|
||||
if app.state.ENGINE == "comfyui":
|
||||
app.state.MODEL = model
|
||||
return app.state.MODEL
|
||||
if app.state.config.ENGINE in ["openai", "comfyui"]:
|
||||
app.state.config.MODEL = model
|
||||
return app.state.config.MODEL
|
||||
else:
|
||||
r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options")
|
||||
r = requests.get(
|
||||
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options"
|
||||
)
|
||||
options = r.json()
|
||||
|
||||
if model != options["sd_model_checkpoint"]:
|
||||
options["sd_model_checkpoint"] = model
|
||||
r = requests.post(
|
||||
url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options", json=options
|
||||
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
|
||||
json=options,
|
||||
)
|
||||
|
||||
return options
|
||||
@@ -315,35 +331,61 @@ class GenerateImageForm(BaseModel):
|
||||
|
||||
|
||||
def save_b64_image(b64_str):
|
||||
image_id = str(uuid.uuid4())
|
||||
file_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.png")
|
||||
|
||||
try:
|
||||
# Split the base64 string to get the actual image data
|
||||
img_data = base64.b64decode(b64_str)
|
||||
image_id = str(uuid.uuid4())
|
||||
|
||||
# Write the image data to a file
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(img_data)
|
||||
if "," in b64_str:
|
||||
header, encoded = b64_str.split(",", 1)
|
||||
mime_type = header.split(";")[0]
|
||||
|
||||
img_data = base64.b64decode(encoded)
|
||||
image_format = mimetypes.guess_extension(mime_type)
|
||||
|
||||
image_filename = f"{image_id}{image_format}"
|
||||
file_path = IMAGE_CACHE_DIR / f"{image_filename}"
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(img_data)
|
||||
return image_filename
|
||||
else:
|
||||
image_filename = f"{image_id}.png"
|
||||
file_path = IMAGE_CACHE_DIR.joinpath(image_filename)
|
||||
|
||||
img_data = base64.b64decode(b64_str)
|
||||
|
||||
# Write the image data to a file
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(img_data)
|
||||
return image_filename
|
||||
|
||||
return image_id
|
||||
except Exception as e:
|
||||
log.error(f"Error saving image: {e}")
|
||||
log.exception(f"Error saving image: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def save_url_image(url):
|
||||
image_id = str(uuid.uuid4())
|
||||
file_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.png")
|
||||
|
||||
try:
|
||||
r = requests.get(url)
|
||||
r.raise_for_status()
|
||||
if r.headers["content-type"].split("/")[0] == "image":
|
||||
|
||||
with open(file_path, "wb") as image_file:
|
||||
image_file.write(r.content)
|
||||
mime_type = r.headers["content-type"]
|
||||
image_format = mimetypes.guess_extension(mime_type)
|
||||
|
||||
if not image_format:
|
||||
raise ValueError("Could not determine image type from MIME type")
|
||||
|
||||
image_filename = f"{image_id}{image_format}"
|
||||
|
||||
file_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}")
|
||||
with open(file_path, "wb") as image_file:
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
image_file.write(chunk)
|
||||
return image_filename
|
||||
else:
|
||||
log.error(f"Url does not point to an image.")
|
||||
return None
|
||||
|
||||
return image_id
|
||||
except Exception as e:
|
||||
log.exception(f"Error saving image: {e}")
|
||||
return None
|
||||
@@ -355,26 +397,32 @@ def generate_image(
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
|
||||
width, height = tuple(map(int, app.state.IMAGE_SIZE.split("x")))
|
||||
width, height = tuple(map(int, app.state.config.IMAGE_SIZE.split("x")))
|
||||
|
||||
r = None
|
||||
try:
|
||||
if app.state.ENGINE == "openai":
|
||||
if app.state.config.ENGINE == "openai":
|
||||
|
||||
headers = {}
|
||||
headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEY}"
|
||||
headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEY}"
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
data = {
|
||||
"model": app.state.MODEL if app.state.MODEL != "" else "dall-e-2",
|
||||
"model": (
|
||||
app.state.config.MODEL
|
||||
if app.state.config.MODEL != ""
|
||||
else "dall-e-2"
|
||||
),
|
||||
"prompt": form_data.prompt,
|
||||
"n": form_data.n,
|
||||
"size": form_data.size if form_data.size else app.state.IMAGE_SIZE,
|
||||
"size": (
|
||||
form_data.size if form_data.size else app.state.config.IMAGE_SIZE
|
||||
),
|
||||
"response_format": "b64_json",
|
||||
}
|
||||
|
||||
r = requests.post(
|
||||
url=f"{app.state.OPENAI_API_BASE_URL}/images/generations",
|
||||
url=f"{app.state.config.OPENAI_API_BASE_URL}/images/generations",
|
||||
json=data,
|
||||
headers=headers,
|
||||
)
|
||||
@@ -385,16 +433,16 @@ def generate_image(
|
||||
images = []
|
||||
|
||||
for image in res["data"]:
|
||||
image_id = save_b64_image(image["b64_json"])
|
||||
images.append({"url": f"/cache/image/generations/{image_id}.png"})
|
||||
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.json")
|
||||
image_filename = save_b64_image(image["b64_json"])
|
||||
images.append({"url": f"/cache/image/generations/{image_filename}"})
|
||||
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
|
||||
|
||||
with open(file_body_path, "w") as f:
|
||||
json.dump(data, f)
|
||||
|
||||
return images
|
||||
|
||||
elif app.state.ENGINE == "comfyui":
|
||||
elif app.state.config.ENGINE == "comfyui":
|
||||
|
||||
data = {
|
||||
"prompt": form_data.prompt,
|
||||
@@ -403,28 +451,28 @@ def generate_image(
|
||||
"n": form_data.n,
|
||||
}
|
||||
|
||||
if app.state.IMAGE_STEPS != None:
|
||||
data["steps"] = app.state.IMAGE_STEPS
|
||||
if app.state.config.IMAGE_STEPS is not None:
|
||||
data["steps"] = app.state.config.IMAGE_STEPS
|
||||
|
||||
if form_data.negative_prompt != None:
|
||||
if form_data.negative_prompt is not None:
|
||||
data["negative_prompt"] = form_data.negative_prompt
|
||||
|
||||
data = ImageGenerationPayload(**data)
|
||||
|
||||
res = comfyui_generate_image(
|
||||
app.state.MODEL,
|
||||
app.state.config.MODEL,
|
||||
data,
|
||||
user.id,
|
||||
app.state.COMFYUI_BASE_URL,
|
||||
app.state.config.COMFYUI_BASE_URL,
|
||||
)
|
||||
log.debug(f"res: {res}")
|
||||
|
||||
images = []
|
||||
|
||||
for image in res["data"]:
|
||||
image_id = save_url_image(image["url"])
|
||||
images.append({"url": f"/cache/image/generations/{image_id}.png"})
|
||||
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.json")
|
||||
image_filename = save_url_image(image["url"])
|
||||
images.append({"url": f"/cache/image/generations/{image_filename}"})
|
||||
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
|
||||
|
||||
with open(file_body_path, "w") as f:
|
||||
json.dump(data.model_dump(exclude_none=True), f)
|
||||
@@ -442,14 +490,14 @@ def generate_image(
|
||||
"height": height,
|
||||
}
|
||||
|
||||
if app.state.IMAGE_STEPS != None:
|
||||
data["steps"] = app.state.IMAGE_STEPS
|
||||
if app.state.config.IMAGE_STEPS is not None:
|
||||
data["steps"] = app.state.config.IMAGE_STEPS
|
||||
|
||||
if form_data.negative_prompt != None:
|
||||
if form_data.negative_prompt is not None:
|
||||
data["negative_prompt"] = form_data.negative_prompt
|
||||
|
||||
r = requests.post(
|
||||
url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/txt2img",
|
||||
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/txt2img",
|
||||
json=data,
|
||||
)
|
||||
|
||||
@@ -460,9 +508,9 @@ def generate_image(
|
||||
images = []
|
||||
|
||||
for image in res["images"]:
|
||||
image_id = save_b64_image(image)
|
||||
images.append({"url": f"/cache/image/generations/{image_id}.png"})
|
||||
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.json")
|
||||
image_filename = save_b64_image(image)
|
||||
images.append({"url": f"/cache/image/generations/{image_filename}"})
|
||||
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
|
||||
|
||||
with open(file_body_path, "w") as f:
|
||||
json.dump({**data, "info": res["info"]}, f)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import sys
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI, Depends, HTTPException
|
||||
from fastapi.routing import APIRoute
|
||||
@@ -36,13 +37,26 @@ from config import (
|
||||
LITELLM_PROXY_HOST,
|
||||
)
|
||||
|
||||
import warnings
|
||||
|
||||
warnings.simplefilter("ignore")
|
||||
|
||||
from litellm.utils import get_llm_provider
|
||||
|
||||
import asyncio
|
||||
import subprocess
|
||||
import yaml
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
log.info("startup_event")
|
||||
# TODO: Check config.yaml file and create one
|
||||
asyncio.create_task(start_litellm_background())
|
||||
yield
|
||||
|
||||
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
|
||||
origins = ["*"]
|
||||
|
||||
@@ -61,6 +75,10 @@ with open(LITELLM_CONFIG_DIR, "r") as file:
|
||||
litellm_config = yaml.safe_load(file)
|
||||
|
||||
|
||||
app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER.value
|
||||
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST.value
|
||||
|
||||
|
||||
app.state.ENABLE = ENABLE_LITELLM
|
||||
app.state.CONFIG = litellm_config
|
||||
|
||||
@@ -137,17 +155,6 @@ async def shutdown_litellm_background():
|
||||
background_process = None
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
log.info("startup_event")
|
||||
# TODO: Check config.yaml file and create one
|
||||
asyncio.create_task(start_litellm_background())
|
||||
|
||||
|
||||
app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
|
||||
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def get_status():
|
||||
return {"status": True}
|
||||
|
||||
+121
-37
@@ -25,13 +25,19 @@ import uuid
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
from urllib.parse import urlparse
|
||||
from typing import Optional, List, Union
|
||||
|
||||
|
||||
from apps.web.models.users import Users
|
||||
from constants import ERROR_MESSAGES
|
||||
from utils.utils import decode_token, get_current_user, get_admin_user
|
||||
from utils.utils import (
|
||||
decode_token,
|
||||
get_current_user,
|
||||
get_verified_user,
|
||||
get_admin_user,
|
||||
)
|
||||
|
||||
|
||||
from config import (
|
||||
@@ -40,6 +46,7 @@ from config import (
|
||||
ENABLE_MODEL_FILTER,
|
||||
MODEL_FILTER_LIST,
|
||||
UPLOAD_DIR,
|
||||
AppConfig,
|
||||
)
|
||||
from utils.misc import calculate_sha256
|
||||
|
||||
@@ -55,11 +62,12 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.state.config = AppConfig()
|
||||
|
||||
app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
|
||||
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
||||
app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
|
||||
app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
||||
|
||||
app.state.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
|
||||
app.state.config.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
|
||||
app.state.MODELS = {}
|
||||
|
||||
|
||||
@@ -90,7 +98,7 @@ async def get_status():
|
||||
|
||||
@app.get("/urls")
|
||||
async def get_ollama_api_urls(user=Depends(get_admin_user)):
|
||||
return {"OLLAMA_BASE_URLS": app.state.OLLAMA_BASE_URLS}
|
||||
return {"OLLAMA_BASE_URLS": app.state.config.OLLAMA_BASE_URLS}
|
||||
|
||||
|
||||
class UrlUpdateForm(BaseModel):
|
||||
@@ -99,10 +107,10 @@ class UrlUpdateForm(BaseModel):
|
||||
|
||||
@app.post("/urls/update")
|
||||
async def update_ollama_api_url(form_data: UrlUpdateForm, user=Depends(get_admin_user)):
|
||||
app.state.OLLAMA_BASE_URLS = form_data.urls
|
||||
app.state.config.OLLAMA_BASE_URLS = form_data.urls
|
||||
|
||||
log.info(f"app.state.OLLAMA_BASE_URLS: {app.state.OLLAMA_BASE_URLS}")
|
||||
return {"OLLAMA_BASE_URLS": app.state.OLLAMA_BASE_URLS}
|
||||
log.info(f"app.state.config.OLLAMA_BASE_URLS: {app.state.config.OLLAMA_BASE_URLS}")
|
||||
return {"OLLAMA_BASE_URLS": app.state.config.OLLAMA_BASE_URLS}
|
||||
|
||||
|
||||
@app.get("/cancel/{request_id}")
|
||||
@@ -116,8 +124,9 @@ async def cancel_ollama_request(request_id: str, user=Depends(get_current_user))
|
||||
|
||||
|
||||
async def fetch_url(url):
|
||||
timeout = aiohttp.ClientTimeout(total=5)
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with session.get(url) as response:
|
||||
return await response.json()
|
||||
except Exception as e:
|
||||
@@ -147,7 +156,7 @@ def merge_models_lists(model_lists):
|
||||
|
||||
async def get_all_models():
|
||||
log.info("get_all_models()")
|
||||
tasks = [fetch_url(f"{url}/api/tags") for url in app.state.OLLAMA_BASE_URLS]
|
||||
tasks = [fetch_url(f"{url}/api/tags") for url in app.state.config.OLLAMA_BASE_URLS]
|
||||
responses = await asyncio.gather(*tasks)
|
||||
|
||||
models = {
|
||||
@@ -164,23 +173,24 @@ async def get_all_models():
|
||||
@app.get("/api/tags")
|
||||
@app.get("/api/tags/{url_idx}")
|
||||
async def get_ollama_tags(
|
||||
url_idx: Optional[int] = None, user=Depends(get_current_user)
|
||||
url_idx: Optional[int] = None, user=Depends(get_verified_user)
|
||||
):
|
||||
if url_idx == None:
|
||||
models = await get_all_models()
|
||||
|
||||
if app.state.ENABLE_MODEL_FILTER:
|
||||
if app.state.config.ENABLE_MODEL_FILTER:
|
||||
if user.role == "user":
|
||||
models["models"] = list(
|
||||
filter(
|
||||
lambda model: model["name"] in app.state.MODEL_FILTER_LIST,
|
||||
lambda model: model["name"]
|
||||
in app.state.config.MODEL_FILTER_LIST,
|
||||
models["models"],
|
||||
)
|
||||
)
|
||||
return models
|
||||
return models
|
||||
else:
|
||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
try:
|
||||
r = requests.request(method="GET", url=f"{url}/api/tags")
|
||||
r.raise_for_status()
|
||||
@@ -210,7 +220,9 @@ async def get_ollama_versions(url_idx: Optional[int] = None):
|
||||
if url_idx == None:
|
||||
|
||||
# returns lowest version
|
||||
tasks = [fetch_url(f"{url}/api/version") for url in app.state.OLLAMA_BASE_URLS]
|
||||
tasks = [
|
||||
fetch_url(f"{url}/api/version") for url in app.state.config.OLLAMA_BASE_URLS
|
||||
]
|
||||
responses = await asyncio.gather(*tasks)
|
||||
responses = list(filter(lambda x: x is not None, responses))
|
||||
|
||||
@@ -229,7 +241,7 @@ async def get_ollama_versions(url_idx: Optional[int] = None):
|
||||
detail=ERROR_MESSAGES.OLLAMA_NOT_FOUND,
|
||||
)
|
||||
else:
|
||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
try:
|
||||
r = requests.request(method="GET", url=f"{url}/api/version")
|
||||
r.raise_for_status()
|
||||
@@ -261,7 +273,7 @@ class ModelNameForm(BaseModel):
|
||||
async def pull_model(
|
||||
form_data: ModelNameForm, url_idx: int = 0, user=Depends(get_admin_user)
|
||||
):
|
||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
log.info(f"url: {url}")
|
||||
|
||||
r = None
|
||||
@@ -349,7 +361,7 @@ async def push_model(
|
||||
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name),
|
||||
)
|
||||
|
||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
log.debug(f"url: {url}")
|
||||
|
||||
r = None
|
||||
@@ -411,7 +423,7 @@ async def create_model(
|
||||
form_data: CreateModelForm, url_idx: int = 0, user=Depends(get_admin_user)
|
||||
):
|
||||
log.debug(f"form_data: {form_data}")
|
||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
log.info(f"url: {url}")
|
||||
|
||||
r = None
|
||||
@@ -484,7 +496,7 @@ async def copy_model(
|
||||
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.source),
|
||||
)
|
||||
|
||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
log.info(f"url: {url}")
|
||||
|
||||
try:
|
||||
@@ -531,7 +543,7 @@ async def delete_model(
|
||||
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name),
|
||||
)
|
||||
|
||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
log.info(f"url: {url}")
|
||||
|
||||
try:
|
||||
@@ -563,7 +575,7 @@ async def delete_model(
|
||||
|
||||
|
||||
@app.post("/api/show")
|
||||
async def show_model_info(form_data: ModelNameForm, user=Depends(get_current_user)):
|
||||
async def show_model_info(form_data: ModelNameForm, user=Depends(get_verified_user)):
|
||||
if form_data.name not in app.state.MODELS:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
@@ -571,7 +583,7 @@ async def show_model_info(form_data: ModelNameForm, user=Depends(get_current_use
|
||||
)
|
||||
|
||||
url_idx = random.choice(app.state.MODELS[form_data.name]["urls"])
|
||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
log.info(f"url: {url}")
|
||||
|
||||
try:
|
||||
@@ -612,7 +624,7 @@ class GenerateEmbeddingsForm(BaseModel):
|
||||
async def generate_embeddings(
|
||||
form_data: GenerateEmbeddingsForm,
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_current_user),
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
if url_idx == None:
|
||||
model = form_data.model
|
||||
@@ -628,7 +640,7 @@ async def generate_embeddings(
|
||||
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
|
||||
)
|
||||
|
||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
log.info(f"url: {url}")
|
||||
|
||||
try:
|
||||
@@ -678,7 +690,7 @@ def generate_ollama_embeddings(
|
||||
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
|
||||
)
|
||||
|
||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
log.info(f"url: {url}")
|
||||
|
||||
try:
|
||||
@@ -730,7 +742,7 @@ class GenerateCompletionForm(BaseModel):
|
||||
async def generate_completion(
|
||||
form_data: GenerateCompletionForm,
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_current_user),
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
|
||||
if url_idx == None:
|
||||
@@ -747,7 +759,7 @@ async def generate_completion(
|
||||
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
|
||||
)
|
||||
|
||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
log.info(f"url: {url}")
|
||||
|
||||
r = None
|
||||
@@ -833,7 +845,7 @@ class GenerateChatCompletionForm(BaseModel):
|
||||
async def generate_chat_completion(
|
||||
form_data: GenerateChatCompletionForm,
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_current_user),
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
|
||||
if url_idx == None:
|
||||
@@ -850,7 +862,7 @@ async def generate_chat_completion(
|
||||
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
|
||||
)
|
||||
|
||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
log.info(f"url: {url}")
|
||||
|
||||
r = None
|
||||
@@ -942,7 +954,7 @@ class OpenAIChatCompletionForm(BaseModel):
|
||||
async def generate_openai_chat_completion(
|
||||
form_data: OpenAIChatCompletionForm,
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_current_user),
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
|
||||
if url_idx == None:
|
||||
@@ -959,7 +971,7 @@ async def generate_openai_chat_completion(
|
||||
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
|
||||
)
|
||||
|
||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
log.info(f"url: {url}")
|
||||
|
||||
r = None
|
||||
@@ -1026,6 +1038,76 @@ async def generate_openai_chat_completion(
|
||||
)
|
||||
|
||||
|
||||
@app.get("/v1/models")
|
||||
@app.get("/v1/models/{url_idx}")
|
||||
async def get_openai_models(
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
if url_idx == None:
|
||||
models = await get_all_models()
|
||||
|
||||
if app.state.config.ENABLE_MODEL_FILTER:
|
||||
if user.role == "user":
|
||||
models["models"] = list(
|
||||
filter(
|
||||
lambda model: model["name"]
|
||||
in app.state.config.MODEL_FILTER_LIST,
|
||||
models["models"],
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
"data": [
|
||||
{
|
||||
"id": model["model"],
|
||||
"object": "model",
|
||||
"created": int(time.time()),
|
||||
"owned_by": "openai",
|
||||
}
|
||||
for model in models["models"]
|
||||
],
|
||||
"object": "list",
|
||||
}
|
||||
|
||||
else:
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
try:
|
||||
r = requests.request(method="GET", url=f"{url}/api/tags")
|
||||
r.raise_for_status()
|
||||
|
||||
models = r.json()
|
||||
|
||||
return {
|
||||
"data": [
|
||||
{
|
||||
"id": model["model"],
|
||||
"object": "model",
|
||||
"created": int(time.time()),
|
||||
"owned_by": "openai",
|
||||
}
|
||||
for model in models["models"]
|
||||
],
|
||||
"object": "list",
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
|
||||
|
||||
class UrlForm(BaseModel):
|
||||
url: str
|
||||
|
||||
@@ -1123,7 +1205,7 @@ async def download_model(
|
||||
|
||||
if url_idx == None:
|
||||
url_idx = 0
|
||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
|
||||
file_name = parse_huggingface_url(form_data.url)
|
||||
|
||||
@@ -1142,7 +1224,7 @@ async def download_model(
|
||||
def upload_model(file: UploadFile = File(...), url_idx: Optional[int] = None):
|
||||
if url_idx == None:
|
||||
url_idx = 0
|
||||
ollama_url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||
ollama_url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
|
||||
file_path = f"{UPLOAD_DIR}/{file.filename}"
|
||||
|
||||
@@ -1207,7 +1289,7 @@ def upload_model(file: UploadFile = File(...), url_idx: Optional[int] = None):
|
||||
# async def upload_model(file: UploadFile = File(), url_idx: Optional[int] = None):
|
||||
# if url_idx == None:
|
||||
# url_idx = 0
|
||||
# url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||
# url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
|
||||
# file_location = os.path.join(UPLOAD_DIR, file.filename)
|
||||
# total_size = file.size
|
||||
@@ -1241,8 +1323,10 @@ def upload_model(file: UploadFile = File(...), url_idx: Optional[int] = None):
|
||||
|
||||
|
||||
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
||||
async def deprecated_proxy(path: str, request: Request, user=Depends(get_current_user)):
|
||||
url = app.state.OLLAMA_BASE_URLS[0]
|
||||
async def deprecated_proxy(
|
||||
path: str, request: Request, user=Depends(get_verified_user)
|
||||
):
|
||||
url = app.state.config.OLLAMA_BASE_URLS[0]
|
||||
target_url = f"{url}/{path}"
|
||||
|
||||
body = await request.body()
|
||||
|
||||
+64
-29
@@ -21,11 +21,13 @@ from utils.utils import (
|
||||
)
|
||||
from config import (
|
||||
SRC_LOG_LEVELS,
|
||||
ENABLE_OPENAI_API,
|
||||
OPENAI_API_BASE_URLS,
|
||||
OPENAI_API_KEYS,
|
||||
CACHE_DIR,
|
||||
ENABLE_MODEL_FILTER,
|
||||
MODEL_FILTER_LIST,
|
||||
AppConfig,
|
||||
)
|
||||
from typing import List, Optional
|
||||
|
||||
@@ -45,11 +47,16 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
|
||||
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
||||
|
||||
app.state.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
|
||||
app.state.OPENAI_API_KEYS = OPENAI_API_KEYS
|
||||
app.state.config = AppConfig()
|
||||
|
||||
app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
|
||||
app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
||||
|
||||
|
||||
app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API
|
||||
app.state.config.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
|
||||
app.state.config.OPENAI_API_KEYS = OPENAI_API_KEYS
|
||||
|
||||
app.state.MODELS = {}
|
||||
|
||||
@@ -65,6 +72,21 @@ async def check_url(request: Request, call_next):
|
||||
return response
|
||||
|
||||
|
||||
@app.get("/config")
|
||||
async def get_config(user=Depends(get_admin_user)):
|
||||
return {"ENABLE_OPENAI_API": app.state.config.ENABLE_OPENAI_API}
|
||||
|
||||
|
||||
class OpenAIConfigForm(BaseModel):
|
||||
enable_openai_api: Optional[bool] = None
|
||||
|
||||
|
||||
@app.post("/config/update")
|
||||
async def update_config(form_data: OpenAIConfigForm, user=Depends(get_admin_user)):
|
||||
app.state.config.ENABLE_OPENAI_API = form_data.enable_openai_api
|
||||
return {"ENABLE_OPENAI_API": app.state.config.ENABLE_OPENAI_API}
|
||||
|
||||
|
||||
class UrlsUpdateForm(BaseModel):
|
||||
urls: List[str]
|
||||
|
||||
@@ -75,32 +97,32 @@ class KeysUpdateForm(BaseModel):
|
||||
|
||||
@app.get("/urls")
|
||||
async def get_openai_urls(user=Depends(get_admin_user)):
|
||||
return {"OPENAI_API_BASE_URLS": app.state.OPENAI_API_BASE_URLS}
|
||||
return {"OPENAI_API_BASE_URLS": app.state.config.OPENAI_API_BASE_URLS}
|
||||
|
||||
|
||||
@app.post("/urls/update")
|
||||
async def update_openai_urls(form_data: UrlsUpdateForm, user=Depends(get_admin_user)):
|
||||
await get_all_models()
|
||||
app.state.OPENAI_API_BASE_URLS = form_data.urls
|
||||
return {"OPENAI_API_BASE_URLS": app.state.OPENAI_API_BASE_URLS}
|
||||
app.state.config.OPENAI_API_BASE_URLS = form_data.urls
|
||||
return {"OPENAI_API_BASE_URLS": app.state.config.OPENAI_API_BASE_URLS}
|
||||
|
||||
|
||||
@app.get("/keys")
|
||||
async def get_openai_keys(user=Depends(get_admin_user)):
|
||||
return {"OPENAI_API_KEYS": app.state.OPENAI_API_KEYS}
|
||||
return {"OPENAI_API_KEYS": app.state.config.OPENAI_API_KEYS}
|
||||
|
||||
|
||||
@app.post("/keys/update")
|
||||
async def update_openai_key(form_data: KeysUpdateForm, user=Depends(get_admin_user)):
|
||||
app.state.OPENAI_API_KEYS = form_data.keys
|
||||
return {"OPENAI_API_KEYS": app.state.OPENAI_API_KEYS}
|
||||
app.state.config.OPENAI_API_KEYS = form_data.keys
|
||||
return {"OPENAI_API_KEYS": app.state.config.OPENAI_API_KEYS}
|
||||
|
||||
|
||||
@app.post("/audio/speech")
|
||||
async def speech(request: Request, user=Depends(get_verified_user)):
|
||||
idx = None
|
||||
try:
|
||||
idx = app.state.OPENAI_API_BASE_URLS.index("https://api.openai.com/v1")
|
||||
idx = app.state.config.OPENAI_API_BASE_URLS.index("https://api.openai.com/v1")
|
||||
body = await request.body()
|
||||
name = hashlib.sha256(body).hexdigest()
|
||||
|
||||
@@ -114,13 +136,15 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
||||
return FileResponse(file_path)
|
||||
|
||||
headers = {}
|
||||
headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEYS[idx]}"
|
||||
headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEYS[idx]}"
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
if "openrouter.ai" in app.state.config.OPENAI_API_BASE_URLS[idx]:
|
||||
headers["HTTP-Referer"] = "https://openwebui.com/"
|
||||
headers["X-Title"] = "Open WebUI"
|
||||
r = None
|
||||
try:
|
||||
r = requests.post(
|
||||
url=f"{app.state.OPENAI_API_BASE_URLS[idx]}/audio/speech",
|
||||
url=f"{app.state.config.OPENAI_API_BASE_URLS[idx]}/audio/speech",
|
||||
data=body,
|
||||
headers=headers,
|
||||
stream=True,
|
||||
@@ -159,11 +183,15 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
||||
|
||||
|
||||
async def fetch_url(url, key):
|
||||
timeout = aiohttp.ClientTimeout(total=5)
|
||||
try:
|
||||
headers = {"Authorization": f"Bearer {key}"}
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, headers=headers) as response:
|
||||
return await response.json()
|
||||
if key != "":
|
||||
headers = {"Authorization": f"Bearer {key}"}
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with session.get(url, headers=headers) as response:
|
||||
return await response.json()
|
||||
else:
|
||||
return None
|
||||
except Exception as e:
|
||||
# Handle connection error here
|
||||
log.error(f"Connection error: {e}")
|
||||
@@ -171,6 +199,7 @@ async def fetch_url(url, key):
|
||||
|
||||
|
||||
def merge_models_lists(model_lists):
|
||||
log.info(f"merge_models_lists {model_lists}")
|
||||
merged_list = []
|
||||
|
||||
for idx, models in enumerate(model_lists):
|
||||
@@ -179,7 +208,8 @@ def merge_models_lists(model_lists):
|
||||
[
|
||||
{**model, "urlIdx": idx}
|
||||
for model in models
|
||||
if "api.openai.com" not in app.state.OPENAI_API_BASE_URLS[idx]
|
||||
if "api.openai.com"
|
||||
not in app.state.config.OPENAI_API_BASE_URLS[idx]
|
||||
or "gpt" in model["id"]
|
||||
]
|
||||
)
|
||||
@@ -190,23 +220,28 @@ def merge_models_lists(model_lists):
|
||||
async def get_all_models():
|
||||
log.info("get_all_models()")
|
||||
|
||||
if len(app.state.OPENAI_API_KEYS) == 1 and app.state.OPENAI_API_KEYS[0] == "":
|
||||
if (
|
||||
len(app.state.config.OPENAI_API_KEYS) == 1
|
||||
and app.state.config.OPENAI_API_KEYS[0] == ""
|
||||
) or not app.state.config.ENABLE_OPENAI_API:
|
||||
models = {"data": []}
|
||||
else:
|
||||
tasks = [
|
||||
fetch_url(f"{url}/models", app.state.OPENAI_API_KEYS[idx])
|
||||
for idx, url in enumerate(app.state.OPENAI_API_BASE_URLS)
|
||||
fetch_url(f"{url}/models", app.state.config.OPENAI_API_KEYS[idx])
|
||||
for idx, url in enumerate(app.state.config.OPENAI_API_BASE_URLS)
|
||||
]
|
||||
|
||||
responses = await asyncio.gather(*tasks)
|
||||
log.info(f"get_all_models:responses() {responses}")
|
||||
|
||||
models = {
|
||||
"data": merge_models_lists(
|
||||
list(
|
||||
map(
|
||||
lambda response: (
|
||||
response["data"]
|
||||
if response and "data" in response
|
||||
else None
|
||||
if (response and "data" in response)
|
||||
else (response if isinstance(response, list) else None)
|
||||
),
|
||||
responses,
|
||||
)
|
||||
@@ -225,18 +260,18 @@ async def get_all_models():
|
||||
async def get_models(url_idx: Optional[int] = None, user=Depends(get_current_user)):
|
||||
if url_idx == None:
|
||||
models = await get_all_models()
|
||||
if app.state.ENABLE_MODEL_FILTER:
|
||||
if app.state.config.ENABLE_MODEL_FILTER:
|
||||
if user.role == "user":
|
||||
models["data"] = list(
|
||||
filter(
|
||||
lambda model: model["id"] in app.state.MODEL_FILTER_LIST,
|
||||
lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST,
|
||||
models["data"],
|
||||
)
|
||||
)
|
||||
return models
|
||||
return models
|
||||
else:
|
||||
url = app.state.OPENAI_API_BASE_URLS[url_idx]
|
||||
url = app.state.config.OPENAI_API_BASE_URLS[url_idx]
|
||||
|
||||
r = None
|
||||
|
||||
@@ -300,8 +335,8 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
||||
except json.JSONDecodeError as e:
|
||||
log.error("Error loading request body into a dictionary:", e)
|
||||
|
||||
url = app.state.OPENAI_API_BASE_URLS[idx]
|
||||
key = app.state.OPENAI_API_KEYS[idx]
|
||||
url = app.state.config.OPENAI_API_BASE_URLS[idx]
|
||||
key = app.state.config.OPENAI_API_KEYS[idx]
|
||||
|
||||
target_url = f"{url}/{path}"
|
||||
|
||||
|
||||
+257
-100
@@ -28,9 +28,15 @@ from langchain_community.document_loaders import (
|
||||
UnstructuredXMLLoader,
|
||||
UnstructuredRSTLoader,
|
||||
UnstructuredExcelLoader,
|
||||
YoutubeLoader,
|
||||
)
|
||||
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
|
||||
import validators
|
||||
import urllib.parse
|
||||
import socket
|
||||
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
import mimetypes
|
||||
@@ -63,6 +69,7 @@ from utils.misc import (
|
||||
from utils.utils import get_current_user, get_admin_user
|
||||
|
||||
from config import (
|
||||
ENV,
|
||||
SRC_LOG_LEVELS,
|
||||
UPLOAD_DIR,
|
||||
DOCS_DIR,
|
||||
@@ -73,6 +80,7 @@ from config import (
|
||||
RAG_EMBEDDING_MODEL_AUTO_UPDATE,
|
||||
RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
|
||||
ENABLE_RAG_HYBRID_SEARCH,
|
||||
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
|
||||
RAG_RERANKING_MODEL,
|
||||
PDF_EXTRACT_IMAGES,
|
||||
RAG_RERANKING_MODEL_AUTO_UPDATE,
|
||||
@@ -84,6 +92,9 @@ from config import (
|
||||
CHUNK_SIZE,
|
||||
CHUNK_OVERLAP,
|
||||
RAG_TEMPLATE,
|
||||
ENABLE_RAG_LOCAL_WEB_FETCH,
|
||||
YOUTUBE_LOADER_LANGUAGE,
|
||||
AppConfig,
|
||||
)
|
||||
|
||||
from constants import ERROR_MESSAGES
|
||||
@@ -93,30 +104,40 @@ log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
app.state.TOP_K = RAG_TOP_K
|
||||
app.state.RELEVANCE_THRESHOLD = RAG_RELEVANCE_THRESHOLD
|
||||
app.state.config = AppConfig()
|
||||
|
||||
app.state.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH
|
||||
app.state.config.TOP_K = RAG_TOP_K
|
||||
app.state.config.RELEVANCE_THRESHOLD = RAG_RELEVANCE_THRESHOLD
|
||||
|
||||
app.state.CHUNK_SIZE = CHUNK_SIZE
|
||||
app.state.CHUNK_OVERLAP = CHUNK_OVERLAP
|
||||
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.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE
|
||||
app.state.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
|
||||
app.state.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL
|
||||
app.state.RAG_TEMPLATE = RAG_TEMPLATE
|
||||
app.state.config.CHUNK_SIZE = CHUNK_SIZE
|
||||
app.state.config.CHUNK_OVERLAP = CHUNK_OVERLAP
|
||||
|
||||
app.state.OPENAI_API_BASE_URL = RAG_OPENAI_API_BASE_URL
|
||||
app.state.OPENAI_API_KEY = RAG_OPENAI_API_KEY
|
||||
app.state.config.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE
|
||||
app.state.config.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
|
||||
app.state.config.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL
|
||||
app.state.config.RAG_TEMPLATE = RAG_TEMPLATE
|
||||
|
||||
app.state.PDF_EXTRACT_IMAGES = PDF_EXTRACT_IMAGES
|
||||
|
||||
app.state.config.OPENAI_API_BASE_URL = RAG_OPENAI_API_BASE_URL
|
||||
app.state.config.OPENAI_API_KEY = RAG_OPENAI_API_KEY
|
||||
|
||||
app.state.config.PDF_EXTRACT_IMAGES = PDF_EXTRACT_IMAGES
|
||||
|
||||
|
||||
app.state.config.YOUTUBE_LOADER_LANGUAGE = YOUTUBE_LOADER_LANGUAGE
|
||||
app.state.YOUTUBE_LOADER_TRANSLATION = None
|
||||
|
||||
|
||||
def update_embedding_model(
|
||||
embedding_model: str,
|
||||
update_model: bool = False,
|
||||
):
|
||||
if embedding_model and app.state.RAG_EMBEDDING_ENGINE == "":
|
||||
if embedding_model and app.state.config.RAG_EMBEDDING_ENGINE == "":
|
||||
app.state.sentence_transformer_ef = sentence_transformers.SentenceTransformer(
|
||||
get_model_path(embedding_model, update_model),
|
||||
device=DEVICE_TYPE,
|
||||
@@ -141,22 +162,22 @@ def update_reranking_model(
|
||||
|
||||
|
||||
update_embedding_model(
|
||||
app.state.RAG_EMBEDDING_MODEL,
|
||||
app.state.config.RAG_EMBEDDING_MODEL,
|
||||
RAG_EMBEDDING_MODEL_AUTO_UPDATE,
|
||||
)
|
||||
|
||||
update_reranking_model(
|
||||
app.state.RAG_RERANKING_MODEL,
|
||||
app.state.config.RAG_RERANKING_MODEL,
|
||||
RAG_RERANKING_MODEL_AUTO_UPDATE,
|
||||
)
|
||||
|
||||
|
||||
app.state.EMBEDDING_FUNCTION = get_embedding_function(
|
||||
app.state.RAG_EMBEDDING_ENGINE,
|
||||
app.state.RAG_EMBEDDING_MODEL,
|
||||
app.state.config.RAG_EMBEDDING_ENGINE,
|
||||
app.state.config.RAG_EMBEDDING_MODEL,
|
||||
app.state.sentence_transformer_ef,
|
||||
app.state.OPENAI_API_KEY,
|
||||
app.state.OPENAI_API_BASE_URL,
|
||||
app.state.config.OPENAI_API_KEY,
|
||||
app.state.config.OPENAI_API_BASE_URL,
|
||||
)
|
||||
|
||||
origins = ["*"]
|
||||
@@ -175,7 +196,7 @@ class CollectionNameForm(BaseModel):
|
||||
collection_name: Optional[str] = "test"
|
||||
|
||||
|
||||
class StoreWebForm(CollectionNameForm):
|
||||
class UrlForm(CollectionNameForm):
|
||||
url: str
|
||||
|
||||
|
||||
@@ -183,12 +204,12 @@ class StoreWebForm(CollectionNameForm):
|
||||
async def get_status():
|
||||
return {
|
||||
"status": True,
|
||||
"chunk_size": app.state.CHUNK_SIZE,
|
||||
"chunk_overlap": app.state.CHUNK_OVERLAP,
|
||||
"template": app.state.RAG_TEMPLATE,
|
||||
"embedding_engine": app.state.RAG_EMBEDDING_ENGINE,
|
||||
"embedding_model": app.state.RAG_EMBEDDING_MODEL,
|
||||
"reranking_model": app.state.RAG_RERANKING_MODEL,
|
||||
"chunk_size": app.state.config.CHUNK_SIZE,
|
||||
"chunk_overlap": app.state.config.CHUNK_OVERLAP,
|
||||
"template": app.state.config.RAG_TEMPLATE,
|
||||
"embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
|
||||
"embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
|
||||
"reranking_model": app.state.config.RAG_RERANKING_MODEL,
|
||||
}
|
||||
|
||||
|
||||
@@ -196,18 +217,21 @@ async def get_status():
|
||||
async def get_embedding_config(user=Depends(get_admin_user)):
|
||||
return {
|
||||
"status": True,
|
||||
"embedding_engine": app.state.RAG_EMBEDDING_ENGINE,
|
||||
"embedding_model": app.state.RAG_EMBEDDING_MODEL,
|
||||
"embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
|
||||
"embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
|
||||
"openai_config": {
|
||||
"url": app.state.OPENAI_API_BASE_URL,
|
||||
"key": app.state.OPENAI_API_KEY,
|
||||
"url": app.state.config.OPENAI_API_BASE_URL,
|
||||
"key": app.state.config.OPENAI_API_KEY,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@app.get("/reranking")
|
||||
async def get_reraanking_config(user=Depends(get_admin_user)):
|
||||
return {"status": True, "reranking_model": app.state.RAG_RERANKING_MODEL}
|
||||
return {
|
||||
"status": True,
|
||||
"reranking_model": app.state.config.RAG_RERANKING_MODEL,
|
||||
}
|
||||
|
||||
|
||||
class OpenAIConfigForm(BaseModel):
|
||||
@@ -226,34 +250,34 @@ async def update_embedding_config(
|
||||
form_data: EmbeddingModelUpdateForm, user=Depends(get_admin_user)
|
||||
):
|
||||
log.info(
|
||||
f"Updating embedding model: {app.state.RAG_EMBEDDING_MODEL} to {form_data.embedding_model}"
|
||||
f"Updating embedding model: {app.state.config.RAG_EMBEDDING_MODEL} to {form_data.embedding_model}"
|
||||
)
|
||||
try:
|
||||
app.state.RAG_EMBEDDING_ENGINE = form_data.embedding_engine
|
||||
app.state.RAG_EMBEDDING_MODEL = form_data.embedding_model
|
||||
app.state.config.RAG_EMBEDDING_ENGINE = form_data.embedding_engine
|
||||
app.state.config.RAG_EMBEDDING_MODEL = form_data.embedding_model
|
||||
|
||||
if app.state.RAG_EMBEDDING_ENGINE in ["ollama", "openai"]:
|
||||
if app.state.config.RAG_EMBEDDING_ENGINE in ["ollama", "openai"]:
|
||||
if form_data.openai_config != None:
|
||||
app.state.OPENAI_API_BASE_URL = form_data.openai_config.url
|
||||
app.state.OPENAI_API_KEY = form_data.openai_config.key
|
||||
app.state.config.OPENAI_API_BASE_URL = form_data.openai_config.url
|
||||
app.state.config.OPENAI_API_KEY = form_data.openai_config.key
|
||||
|
||||
update_embedding_model(app.state.RAG_EMBEDDING_MODEL, True)
|
||||
update_embedding_model(app.state.config.RAG_EMBEDDING_MODEL)
|
||||
|
||||
app.state.EMBEDDING_FUNCTION = get_embedding_function(
|
||||
app.state.RAG_EMBEDDING_ENGINE,
|
||||
app.state.RAG_EMBEDDING_MODEL,
|
||||
app.state.config.RAG_EMBEDDING_ENGINE,
|
||||
app.state.config.RAG_EMBEDDING_MODEL,
|
||||
app.state.sentence_transformer_ef,
|
||||
app.state.OPENAI_API_KEY,
|
||||
app.state.OPENAI_API_BASE_URL,
|
||||
app.state.config.OPENAI_API_KEY,
|
||||
app.state.config.OPENAI_API_BASE_URL,
|
||||
)
|
||||
|
||||
return {
|
||||
"status": True,
|
||||
"embedding_engine": app.state.RAG_EMBEDDING_ENGINE,
|
||||
"embedding_model": app.state.RAG_EMBEDDING_MODEL,
|
||||
"embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
|
||||
"embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
|
||||
"openai_config": {
|
||||
"url": app.state.OPENAI_API_BASE_URL,
|
||||
"key": app.state.OPENAI_API_KEY,
|
||||
"url": app.state.config.OPENAI_API_BASE_URL,
|
||||
"key": app.state.config.OPENAI_API_KEY,
|
||||
},
|
||||
}
|
||||
except Exception as e:
|
||||
@@ -273,16 +297,16 @@ async def update_reranking_config(
|
||||
form_data: RerankingModelUpdateForm, user=Depends(get_admin_user)
|
||||
):
|
||||
log.info(
|
||||
f"Updating reranking model: {app.state.RAG_RERANKING_MODEL} to {form_data.reranking_model}"
|
||||
f"Updating reranking model: {app.state.config.RAG_RERANKING_MODEL} to {form_data.reranking_model}"
|
||||
)
|
||||
try:
|
||||
app.state.RAG_RERANKING_MODEL = form_data.reranking_model
|
||||
app.state.config.RAG_RERANKING_MODEL = form_data.reranking_model
|
||||
|
||||
update_reranking_model(app.state.RAG_RERANKING_MODEL, True)
|
||||
update_reranking_model(app.state.config.RAG_RERANKING_MODEL), True
|
||||
|
||||
return {
|
||||
"status": True,
|
||||
"reranking_model": app.state.RAG_RERANKING_MODEL,
|
||||
"reranking_model": app.state.config.RAG_RERANKING_MODEL,
|
||||
}
|
||||
except Exception as e:
|
||||
log.exception(f"Problem updating reranking model: {e}")
|
||||
@@ -296,10 +320,15 @@ async def update_reranking_config(
|
||||
async def get_rag_config(user=Depends(get_admin_user)):
|
||||
return {
|
||||
"status": True,
|
||||
"pdf_extract_images": app.state.PDF_EXTRACT_IMAGES,
|
||||
"pdf_extract_images": app.state.config.PDF_EXTRACT_IMAGES,
|
||||
"chunk": {
|
||||
"chunk_size": app.state.CHUNK_SIZE,
|
||||
"chunk_overlap": app.state.CHUNK_OVERLAP,
|
||||
"chunk_size": app.state.config.CHUNK_SIZE,
|
||||
"chunk_overlap": app.state.config.CHUNK_OVERLAP,
|
||||
},
|
||||
"web_loader_ssl_verification": app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
|
||||
"youtube": {
|
||||
"language": app.state.config.YOUTUBE_LOADER_LANGUAGE,
|
||||
"translation": app.state.YOUTUBE_LOADER_TRANSLATION,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -309,23 +338,67 @@ class ChunkParamUpdateForm(BaseModel):
|
||||
chunk_overlap: int
|
||||
|
||||
|
||||
class YoutubeLoaderConfig(BaseModel):
|
||||
language: List[str]
|
||||
translation: Optional[str] = None
|
||||
|
||||
|
||||
class ConfigUpdateForm(BaseModel):
|
||||
pdf_extract_images: bool
|
||||
chunk: ChunkParamUpdateForm
|
||||
pdf_extract_images: Optional[bool] = None
|
||||
chunk: Optional[ChunkParamUpdateForm] = None
|
||||
web_loader_ssl_verification: Optional[bool] = None
|
||||
youtube: Optional[YoutubeLoaderConfig] = None
|
||||
|
||||
|
||||
@app.post("/config/update")
|
||||
async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_user)):
|
||||
app.state.PDF_EXTRACT_IMAGES = form_data.pdf_extract_images
|
||||
app.state.CHUNK_SIZE = form_data.chunk.chunk_size
|
||||
app.state.CHUNK_OVERLAP = form_data.chunk.chunk_overlap
|
||||
app.state.config.PDF_EXTRACT_IMAGES = (
|
||||
form_data.pdf_extract_images
|
||||
if form_data.pdf_extract_images is not None
|
||||
else app.state.config.PDF_EXTRACT_IMAGES
|
||||
)
|
||||
|
||||
app.state.config.CHUNK_SIZE = (
|
||||
form_data.chunk.chunk_size
|
||||
if form_data.chunk is not None
|
||||
else app.state.config.CHUNK_SIZE
|
||||
)
|
||||
|
||||
app.state.config.CHUNK_OVERLAP = (
|
||||
form_data.chunk.chunk_overlap
|
||||
if form_data.chunk is not None
|
||||
else app.state.config.CHUNK_OVERLAP
|
||||
)
|
||||
|
||||
app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
|
||||
form_data.web_loader_ssl_verification
|
||||
if form_data.web_loader_ssl_verification != None
|
||||
else app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
|
||||
)
|
||||
|
||||
app.state.config.YOUTUBE_LOADER_LANGUAGE = (
|
||||
form_data.youtube.language
|
||||
if form_data.youtube is not None
|
||||
else app.state.config.YOUTUBE_LOADER_LANGUAGE
|
||||
)
|
||||
|
||||
app.state.YOUTUBE_LOADER_TRANSLATION = (
|
||||
form_data.youtube.translation
|
||||
if form_data.youtube is not None
|
||||
else app.state.YOUTUBE_LOADER_TRANSLATION
|
||||
)
|
||||
|
||||
return {
|
||||
"status": True,
|
||||
"pdf_extract_images": app.state.PDF_EXTRACT_IMAGES,
|
||||
"pdf_extract_images": app.state.config.PDF_EXTRACT_IMAGES,
|
||||
"chunk": {
|
||||
"chunk_size": app.state.CHUNK_SIZE,
|
||||
"chunk_overlap": app.state.CHUNK_OVERLAP,
|
||||
"chunk_size": app.state.config.CHUNK_SIZE,
|
||||
"chunk_overlap": app.state.config.CHUNK_OVERLAP,
|
||||
},
|
||||
"web_loader_ssl_verification": app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
|
||||
"youtube": {
|
||||
"language": app.state.config.YOUTUBE_LOADER_LANGUAGE,
|
||||
"translation": app.state.YOUTUBE_LOADER_TRANSLATION,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -334,7 +407,7 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_
|
||||
async def get_rag_template(user=Depends(get_current_user)):
|
||||
return {
|
||||
"status": True,
|
||||
"template": app.state.RAG_TEMPLATE,
|
||||
"template": app.state.config.RAG_TEMPLATE,
|
||||
}
|
||||
|
||||
|
||||
@@ -342,10 +415,10 @@ async def get_rag_template(user=Depends(get_current_user)):
|
||||
async def get_query_settings(user=Depends(get_admin_user)):
|
||||
return {
|
||||
"status": True,
|
||||
"template": app.state.RAG_TEMPLATE,
|
||||
"k": app.state.TOP_K,
|
||||
"r": app.state.RELEVANCE_THRESHOLD,
|
||||
"hybrid": app.state.ENABLE_RAG_HYBRID_SEARCH,
|
||||
"template": app.state.config.RAG_TEMPLATE,
|
||||
"k": app.state.config.TOP_K,
|
||||
"r": app.state.config.RELEVANCE_THRESHOLD,
|
||||
"hybrid": app.state.config.ENABLE_RAG_HYBRID_SEARCH,
|
||||
}
|
||||
|
||||
|
||||
@@ -360,16 +433,20 @@ class QuerySettingsForm(BaseModel):
|
||||
async def update_query_settings(
|
||||
form_data: QuerySettingsForm, user=Depends(get_admin_user)
|
||||
):
|
||||
app.state.RAG_TEMPLATE = form_data.template if form_data.template else RAG_TEMPLATE
|
||||
app.state.TOP_K = form_data.k if form_data.k else 4
|
||||
app.state.RELEVANCE_THRESHOLD = form_data.r if form_data.r else 0.0
|
||||
app.state.ENABLE_RAG_HYBRID_SEARCH = form_data.hybrid if form_data.hybrid else False
|
||||
app.state.config.RAG_TEMPLATE = (
|
||||
form_data.template if form_data.template else RAG_TEMPLATE
|
||||
)
|
||||
app.state.config.TOP_K = form_data.k if form_data.k else 4
|
||||
app.state.config.RELEVANCE_THRESHOLD = form_data.r if form_data.r else 0.0
|
||||
app.state.config.ENABLE_RAG_HYBRID_SEARCH = (
|
||||
form_data.hybrid if form_data.hybrid else False
|
||||
)
|
||||
return {
|
||||
"status": True,
|
||||
"template": app.state.RAG_TEMPLATE,
|
||||
"k": app.state.TOP_K,
|
||||
"r": app.state.RELEVANCE_THRESHOLD,
|
||||
"hybrid": app.state.ENABLE_RAG_HYBRID_SEARCH,
|
||||
"template": app.state.config.RAG_TEMPLATE,
|
||||
"k": app.state.config.TOP_K,
|
||||
"r": app.state.config.RELEVANCE_THRESHOLD,
|
||||
"hybrid": app.state.config.ENABLE_RAG_HYBRID_SEARCH,
|
||||
}
|
||||
|
||||
|
||||
@@ -387,21 +464,23 @@ def query_doc_handler(
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
if app.state.ENABLE_RAG_HYBRID_SEARCH:
|
||||
if app.state.config.ENABLE_RAG_HYBRID_SEARCH:
|
||||
return query_doc_with_hybrid_search(
|
||||
collection_name=form_data.collection_name,
|
||||
query=form_data.query,
|
||||
embeddings_function=app.state.EMBEDDING_FUNCTION,
|
||||
embedding_function=app.state.EMBEDDING_FUNCTION,
|
||||
k=form_data.k if form_data.k else app.state.config.TOP_K,
|
||||
reranking_function=app.state.sentence_transformer_rf,
|
||||
k=form_data.k if form_data.k else app.state.TOP_K,
|
||||
r=form_data.r if form_data.r else app.state.RELEVANCE_THRESHOLD,
|
||||
r=(
|
||||
form_data.r if form_data.r else app.state.config.RELEVANCE_THRESHOLD
|
||||
),
|
||||
)
|
||||
else:
|
||||
return query_doc(
|
||||
collection_name=form_data.collection_name,
|
||||
query=form_data.query,
|
||||
embeddings_function=app.state.EMBEDDING_FUNCTION,
|
||||
k=form_data.k if form_data.k else app.state.TOP_K,
|
||||
embedding_function=app.state.EMBEDDING_FUNCTION,
|
||||
k=form_data.k if form_data.k else app.state.config.TOP_K,
|
||||
)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
@@ -425,21 +504,23 @@ def query_collection_handler(
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
if app.state.ENABLE_RAG_HYBRID_SEARCH:
|
||||
if app.state.config.ENABLE_RAG_HYBRID_SEARCH:
|
||||
return query_collection_with_hybrid_search(
|
||||
collection_names=form_data.collection_names,
|
||||
query=form_data.query,
|
||||
embeddings_function=app.state.EMBEDDING_FUNCTION,
|
||||
embedding_function=app.state.EMBEDDING_FUNCTION,
|
||||
k=form_data.k if form_data.k else app.state.config.TOP_K,
|
||||
reranking_function=app.state.sentence_transformer_rf,
|
||||
k=form_data.k if form_data.k else app.state.TOP_K,
|
||||
r=form_data.r if form_data.r else app.state.RELEVANCE_THRESHOLD,
|
||||
r=(
|
||||
form_data.r if form_data.r else app.state.config.RELEVANCE_THRESHOLD
|
||||
),
|
||||
)
|
||||
else:
|
||||
return query_collection(
|
||||
collection_names=form_data.collection_names,
|
||||
query=form_data.query,
|
||||
embeddings_function=app.state.EMBEDDING_FUNCTION,
|
||||
k=form_data.k if form_data.k else app.state.TOP_K,
|
||||
embedding_function=app.state.EMBEDDING_FUNCTION,
|
||||
k=form_data.k if form_data.k else app.state.config.TOP_K,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -450,11 +531,15 @@ def query_collection_handler(
|
||||
)
|
||||
|
||||
|
||||
@app.post("/web")
|
||||
def store_web(form_data: StoreWebForm, user=Depends(get_current_user)):
|
||||
# "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm"
|
||||
@app.post("/youtube")
|
||||
def store_youtube_video(form_data: UrlForm, user=Depends(get_current_user)):
|
||||
try:
|
||||
loader = WebBaseLoader(form_data.url)
|
||||
loader = YoutubeLoader.from_youtube_url(
|
||||
form_data.url,
|
||||
add_video_info=True,
|
||||
language=app.state.config.YOUTUBE_LOADER_LANGUAGE,
|
||||
translation=app.state.YOUTUBE_LOADER_TRANSLATION,
|
||||
)
|
||||
data = loader.load()
|
||||
|
||||
collection_name = form_data.collection_name
|
||||
@@ -475,11 +560,70 @@ def store_web(form_data: StoreWebForm, user=Depends(get_current_user)):
|
||||
)
|
||||
|
||||
|
||||
@app.post("/web")
|
||||
def store_web(form_data: UrlForm, user=Depends(get_current_user)):
|
||||
# "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm"
|
||||
try:
|
||||
loader = get_web_loader(
|
||||
form_data.url,
|
||||
verify_ssl=app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
|
||||
)
|
||||
data = loader.load()
|
||||
|
||||
collection_name = form_data.collection_name
|
||||
if collection_name == "":
|
||||
collection_name = calculate_sha256_string(form_data.url)[:63]
|
||||
|
||||
store_data_in_vector_db(data, collection_name, overwrite=True)
|
||||
return {
|
||||
"status": True,
|
||||
"collection_name": collection_name,
|
||||
"filename": form_data.url,
|
||||
}
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||
)
|
||||
|
||||
|
||||
def get_web_loader(url: str, verify_ssl: bool = True):
|
||||
# Check if the URL is valid
|
||||
if isinstance(validators.url(url), validators.ValidationError):
|
||||
raise ValueError(ERROR_MESSAGES.INVALID_URL)
|
||||
if not ENABLE_RAG_LOCAL_WEB_FETCH:
|
||||
# Local web fetch is disabled, filter out any URLs that resolve to private IP addresses
|
||||
parsed_url = urllib.parse.urlparse(url)
|
||||
# Get IPv4 and IPv6 addresses
|
||||
ipv4_addresses, ipv6_addresses = resolve_hostname(parsed_url.hostname)
|
||||
# Check if any of the resolved addresses are private
|
||||
# This is technically still vulnerable to DNS rebinding attacks, as we don't control WebBaseLoader
|
||||
for ip in ipv4_addresses:
|
||||
if validators.ipv4(ip, private=True):
|
||||
raise ValueError(ERROR_MESSAGES.INVALID_URL)
|
||||
for ip in ipv6_addresses:
|
||||
if validators.ipv6(ip, private=True):
|
||||
raise ValueError(ERROR_MESSAGES.INVALID_URL)
|
||||
return WebBaseLoader(url, verify_ssl=verify_ssl)
|
||||
|
||||
|
||||
def resolve_hostname(hostname):
|
||||
# Get address information
|
||||
addr_info = socket.getaddrinfo(hostname, None)
|
||||
|
||||
# Extract IP addresses from address information
|
||||
ipv4_addresses = [info[4][0] for info in addr_info if info[0] == socket.AF_INET]
|
||||
ipv6_addresses = [info[4][0] for info in addr_info if info[0] == socket.AF_INET6]
|
||||
|
||||
return ipv4_addresses, ipv6_addresses
|
||||
|
||||
|
||||
def store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> bool:
|
||||
|
||||
text_splitter = RecursiveCharacterTextSplitter(
|
||||
chunk_size=app.state.CHUNK_SIZE,
|
||||
chunk_overlap=app.state.CHUNK_OVERLAP,
|
||||
chunk_size=app.state.config.CHUNK_SIZE,
|
||||
chunk_overlap=app.state.config.CHUNK_OVERLAP,
|
||||
add_start_index=True,
|
||||
)
|
||||
|
||||
@@ -496,8 +640,8 @@ def store_text_in_vector_db(
|
||||
text, metadata, collection_name, overwrite: bool = False
|
||||
) -> bool:
|
||||
text_splitter = RecursiveCharacterTextSplitter(
|
||||
chunk_size=app.state.CHUNK_SIZE,
|
||||
chunk_overlap=app.state.CHUNK_OVERLAP,
|
||||
chunk_size=app.state.config.CHUNK_SIZE,
|
||||
chunk_overlap=app.state.config.CHUNK_OVERLAP,
|
||||
add_start_index=True,
|
||||
)
|
||||
docs = text_splitter.create_documents([text], metadatas=[metadata])
|
||||
@@ -520,11 +664,11 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b
|
||||
collection = CHROMA_CLIENT.create_collection(name=collection_name)
|
||||
|
||||
embedding_func = get_embedding_function(
|
||||
app.state.RAG_EMBEDDING_ENGINE,
|
||||
app.state.RAG_EMBEDDING_MODEL,
|
||||
app.state.config.RAG_EMBEDDING_ENGINE,
|
||||
app.state.config.RAG_EMBEDDING_MODEL,
|
||||
app.state.sentence_transformer_ef,
|
||||
app.state.OPENAI_API_KEY,
|
||||
app.state.OPENAI_API_BASE_URL,
|
||||
app.state.config.OPENAI_API_KEY,
|
||||
app.state.config.OPENAI_API_BASE_URL,
|
||||
)
|
||||
|
||||
embedding_texts = list(map(lambda x: x.replace("\n", " "), texts))
|
||||
@@ -532,7 +676,7 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b
|
||||
|
||||
for batch in create_batches(
|
||||
api=CHROMA_CLIENT,
|
||||
ids=[str(uuid.uuid1()) for _ in texts],
|
||||
ids=[str(uuid.uuid4()) for _ in texts],
|
||||
metadatas=metadatas,
|
||||
embeddings=embeddings,
|
||||
documents=texts,
|
||||
@@ -598,7 +742,9 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
|
||||
]
|
||||
|
||||
if file_ext == "pdf":
|
||||
loader = PyPDFLoader(file_path, extract_images=app.state.PDF_EXTRACT_IMAGES)
|
||||
loader = PyPDFLoader(
|
||||
file_path, extract_images=app.state.config.PDF_EXTRACT_IMAGES
|
||||
)
|
||||
elif file_ext == "csv":
|
||||
loader = CSVLoader(file_path)
|
||||
elif file_ext == "rst":
|
||||
@@ -806,3 +952,14 @@ def reset(user=Depends(get_admin_user)) -> bool:
|
||||
log.exception(e)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
if ENV == "dev":
|
||||
|
||||
@app.get("/ef")
|
||||
async def get_embeddings():
|
||||
return {"result": app.state.EMBEDDING_FUNCTION("hello world")}
|
||||
|
||||
@app.get("/ef/{text}")
|
||||
async def get_embeddings_text(text: str):
|
||||
return {"result": app.state.EMBEDDING_FUNCTION(text)}
|
||||
|
||||
+48
-37
@@ -35,6 +35,7 @@ def query_doc(
|
||||
try:
|
||||
collection = CHROMA_CLIENT.get_collection(name=collection_name)
|
||||
query_embeddings = embedding_function(query)
|
||||
|
||||
result = collection.query(
|
||||
query_embeddings=[query_embeddings],
|
||||
n_results=k,
|
||||
@@ -52,7 +53,7 @@ def query_doc_with_hybrid_search(
|
||||
embedding_function,
|
||||
k: int,
|
||||
reranking_function,
|
||||
r: int,
|
||||
r: float,
|
||||
):
|
||||
try:
|
||||
collection = CHROMA_CLIENT.get_collection(name=collection_name)
|
||||
@@ -76,9 +77,9 @@ def query_doc_with_hybrid_search(
|
||||
|
||||
compressor = RerankCompressor(
|
||||
embedding_function=embedding_function,
|
||||
top_n=k,
|
||||
reranking_function=reranking_function,
|
||||
r_score=r,
|
||||
top_n=k,
|
||||
)
|
||||
|
||||
compression_retriever = ContextualCompressionRetriever(
|
||||
@@ -91,6 +92,7 @@ def query_doc_with_hybrid_search(
|
||||
"documents": [[d.page_content for d in result]],
|
||||
"metadatas": [[d.metadata for d in result]],
|
||||
}
|
||||
|
||||
log.info(f"query_doc_with_hybrid_search:result {result}")
|
||||
return result
|
||||
except Exception as e:
|
||||
@@ -167,7 +169,6 @@ def query_collection_with_hybrid_search(
|
||||
reranking_function,
|
||||
r: float,
|
||||
):
|
||||
|
||||
results = []
|
||||
for collection_name in collection_names:
|
||||
try:
|
||||
@@ -182,7 +183,6 @@ def query_collection_with_hybrid_search(
|
||||
results.append(result)
|
||||
except:
|
||||
pass
|
||||
|
||||
return merge_and_sort_query_results(results, k=k, reverse=True)
|
||||
|
||||
|
||||
@@ -271,14 +271,14 @@ def rag_messages(
|
||||
for doc in docs:
|
||||
context = None
|
||||
|
||||
collection = doc.get("collection_name")
|
||||
if collection:
|
||||
collection = [collection]
|
||||
else:
|
||||
collection = doc.get("collection_names", [])
|
||||
collection_names = (
|
||||
doc["collection_names"]
|
||||
if doc["type"] == "collection"
|
||||
else [doc["collection_name"]]
|
||||
)
|
||||
|
||||
collection = set(collection).difference(extracted_collections)
|
||||
if not collection:
|
||||
collection_names = set(collection_names).difference(extracted_collections)
|
||||
if not collection_names:
|
||||
log.debug(f"skipping {doc} as it has already been extracted")
|
||||
continue
|
||||
|
||||
@@ -288,11 +288,7 @@ def rag_messages(
|
||||
else:
|
||||
if hybrid_search:
|
||||
context = query_collection_with_hybrid_search(
|
||||
collection_names=(
|
||||
doc["collection_names"]
|
||||
if doc["type"] == "collection"
|
||||
else [doc["collection_name"]]
|
||||
),
|
||||
collection_names=collection_names,
|
||||
query=query,
|
||||
embedding_function=embedding_function,
|
||||
k=k,
|
||||
@@ -301,11 +297,7 @@ def rag_messages(
|
||||
)
|
||||
else:
|
||||
context = query_collection(
|
||||
collection_names=(
|
||||
doc["collection_names"]
|
||||
if doc["type"] == "collection"
|
||||
else [doc["collection_name"]]
|
||||
),
|
||||
collection_names=collection_names,
|
||||
query=query,
|
||||
embedding_function=embedding_function,
|
||||
k=k,
|
||||
@@ -315,14 +307,31 @@ def rag_messages(
|
||||
context = None
|
||||
|
||||
if context:
|
||||
relevant_contexts.append(context)
|
||||
relevant_contexts.append({**context, "source": doc})
|
||||
|
||||
extracted_collections.extend(collection)
|
||||
extracted_collections.extend(collection_names)
|
||||
|
||||
context_string = ""
|
||||
|
||||
citations = []
|
||||
for context in relevant_contexts:
|
||||
items = context["documents"][0]
|
||||
context_string += "\n\n".join(items)
|
||||
try:
|
||||
if "documents" in context:
|
||||
context_string += "\n\n".join(
|
||||
[text for text in context["documents"][0] if text is not None]
|
||||
)
|
||||
|
||||
if "metadatas" in context:
|
||||
citations.append(
|
||||
{
|
||||
"source": context["source"],
|
||||
"document": context["documents"][0],
|
||||
"metadata": context["metadatas"][0],
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
||||
context_string = context_string.strip()
|
||||
|
||||
ra_content = rag_template(
|
||||
@@ -351,7 +360,7 @@ def rag_messages(
|
||||
|
||||
messages[last_user_message_idx] = new_user_message
|
||||
|
||||
return messages
|
||||
return messages, citations
|
||||
|
||||
|
||||
def get_model_path(model: str, update_model: bool = False):
|
||||
@@ -443,13 +452,15 @@ class ChromaRetriever(BaseRetriever):
|
||||
metadatas = results["metadatas"][0]
|
||||
documents = results["documents"][0]
|
||||
|
||||
return [
|
||||
Document(
|
||||
metadata=metadatas[idx],
|
||||
page_content=documents[idx],
|
||||
results = []
|
||||
for idx in range(len(ids)):
|
||||
results.append(
|
||||
Document(
|
||||
metadata=metadatas[idx],
|
||||
page_content=documents[idx],
|
||||
)
|
||||
)
|
||||
for idx in range(len(ids))
|
||||
]
|
||||
return results
|
||||
|
||||
|
||||
import operator
|
||||
@@ -465,9 +476,9 @@ from sentence_transformers import util
|
||||
|
||||
class RerankCompressor(BaseDocumentCompressor):
|
||||
embedding_function: Any
|
||||
top_n: int
|
||||
reranking_function: Any
|
||||
r_score: float
|
||||
top_n: int
|
||||
|
||||
class Config:
|
||||
extra = Extra.forbid
|
||||
@@ -479,7 +490,9 @@ class RerankCompressor(BaseDocumentCompressor):
|
||||
query: str,
|
||||
callbacks: Optional[Callbacks] = None,
|
||||
) -> Sequence[Document]:
|
||||
if self.reranking_function:
|
||||
reranking = self.reranking_function is not None
|
||||
|
||||
if reranking:
|
||||
scores = self.reranking_function.predict(
|
||||
[(query, doc.page_content) for doc in documents]
|
||||
)
|
||||
@@ -496,9 +509,7 @@ class RerankCompressor(BaseDocumentCompressor):
|
||||
(d, s) for d, s in docs_with_scores if s >= self.r_score
|
||||
]
|
||||
|
||||
reverse = self.reranking_function is not None
|
||||
result = sorted(docs_with_scores, key=operator.itemgetter(1), reverse=reverse)
|
||||
|
||||
result = sorted(docs_with_scores, key=operator.itemgetter(1), reverse=True)
|
||||
final_results = []
|
||||
for doc, doc_score in result[: self.top_n]:
|
||||
metadata = doc.metadata
|
||||
|
||||
@@ -65,7 +65,7 @@ def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
user_id = pw.CharField(max_length=255)
|
||||
title = pw.CharField()
|
||||
chat = pw.TextField()
|
||||
timestamp = pw.DateField()
|
||||
timestamp = pw.BigIntegerField()
|
||||
|
||||
class Meta:
|
||||
table_name = "chat"
|
||||
@@ -76,7 +76,7 @@ def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
tag_name = pw.CharField(max_length=255)
|
||||
chat_id = pw.CharField(max_length=255)
|
||||
user_id = pw.CharField(max_length=255)
|
||||
timestamp = pw.DateField()
|
||||
timestamp = pw.BigIntegerField()
|
||||
|
||||
class Meta:
|
||||
table_name = "chatidtag"
|
||||
@@ -90,7 +90,7 @@ def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
filename = pw.CharField()
|
||||
content = pw.TextField(null=True)
|
||||
user_id = pw.CharField(max_length=255)
|
||||
timestamp = pw.DateField()
|
||||
timestamp = pw.BigIntegerField()
|
||||
|
||||
class Meta:
|
||||
table_name = "document"
|
||||
@@ -101,7 +101,7 @@ def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
tag_name = pw.CharField(max_length=255, unique=True)
|
||||
user_id = pw.CharField(max_length=255)
|
||||
modelfile = pw.TextField()
|
||||
timestamp = pw.DateField()
|
||||
timestamp = pw.BigIntegerField()
|
||||
|
||||
class Meta:
|
||||
table_name = "modelfile"
|
||||
@@ -113,7 +113,7 @@ def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
user_id = pw.CharField(max_length=255)
|
||||
title = pw.CharField()
|
||||
content = pw.TextField()
|
||||
timestamp = pw.DateField()
|
||||
timestamp = pw.BigIntegerField()
|
||||
|
||||
class Meta:
|
||||
table_name = "prompt"
|
||||
@@ -135,7 +135,7 @@ def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
email = pw.CharField(max_length=255)
|
||||
role = pw.CharField(max_length=255)
|
||||
profile_image_url = pw.CharField(max_length=255)
|
||||
timestamp = pw.DateField()
|
||||
timestamp = pw.BigIntegerField()
|
||||
|
||||
class Meta:
|
||||
table_name = "user"
|
||||
|
||||
@@ -47,7 +47,7 @@ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
|
||||
# Populate the new fields from an existing 'timestamp' field
|
||||
migrator.sql(
|
||||
"UPDATE user SET created_at = timestamp, updated_at = timestamp, last_active_at = timestamp WHERE timestamp IS NOT NULL"
|
||||
'UPDATE "user" SET created_at = timestamp, updated_at = timestamp, last_active_at = timestamp WHERE timestamp IS NOT NULL'
|
||||
)
|
||||
|
||||
# Now that the data has been copied, remove the original 'timestamp' field
|
||||
@@ -70,7 +70,7 @@ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
|
||||
# Copy the earliest created_at date back into the new timestamp field
|
||||
# This assumes created_at was originally a copy of timestamp
|
||||
migrator.sql("UPDATE user SET timestamp = created_at")
|
||||
migrator.sql('UPDATE "user" SET timestamp = created_at')
|
||||
|
||||
# Remove the created_at and updated_at fields
|
||||
migrator.remove_fields("user", "created_at", "updated_at", "last_active_at")
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Peewee migrations -- 002_add_local_sharing.py.
|
||||
|
||||
Some examples (model - class or model name)::
|
||||
|
||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||
> Model = migrator.ModelClass # Return model in current state by name
|
||||
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
> migrator.add_constraint(model, name, sql)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.drop_constraints(model, *constraints)
|
||||
|
||||
"""
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
import peewee as pw
|
||||
from peewee_migrate import Migrator
|
||||
|
||||
|
||||
with suppress(ImportError):
|
||||
import playhouse.postgres_ext as pw_pext
|
||||
|
||||
|
||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
@migrator.create_model
|
||||
class Memory(pw.Model):
|
||||
id = pw.CharField(max_length=255, unique=True)
|
||||
user_id = pw.CharField(max_length=255)
|
||||
content = pw.TextField(null=False)
|
||||
updated_at = pw.BigIntegerField(null=False)
|
||||
created_at = pw.BigIntegerField(null=False)
|
||||
|
||||
class Meta:
|
||||
table_name = "memory"
|
||||
|
||||
|
||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your rollback migrations here."""
|
||||
|
||||
migrator.remove_model("memory")
|
||||
@@ -9,6 +9,7 @@ from apps.web.routers import (
|
||||
modelfiles,
|
||||
prompts,
|
||||
configs,
|
||||
memories,
|
||||
utils,
|
||||
)
|
||||
from config import (
|
||||
@@ -21,22 +22,27 @@ from config import (
|
||||
USER_PERMISSIONS,
|
||||
WEBHOOK_URL,
|
||||
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
|
||||
JWT_EXPIRES_IN,
|
||||
AppConfig,
|
||||
)
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
origins = ["*"]
|
||||
|
||||
app.state.ENABLE_SIGNUP = ENABLE_SIGNUP
|
||||
app.state.JWT_EXPIRES_IN = "-1"
|
||||
app.state.config = AppConfig()
|
||||
|
||||
app.state.DEFAULT_MODELS = DEFAULT_MODELS
|
||||
app.state.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
|
||||
app.state.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
|
||||
app.state.USER_PERMISSIONS = USER_PERMISSIONS
|
||||
app.state.WEBHOOK_URL = WEBHOOK_URL
|
||||
app.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP
|
||||
app.state.config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
|
||||
|
||||
app.state.config.DEFAULT_MODELS = DEFAULT_MODELS
|
||||
app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
|
||||
app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
|
||||
app.state.config.USER_PERMISSIONS = USER_PERMISSIONS
|
||||
app.state.config.WEBHOOK_URL = WEBHOOK_URL
|
||||
app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
|
||||
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins,
|
||||
@@ -48,9 +54,12 @@ app.add_middleware(
|
||||
app.include_router(auths.router, prefix="/auths", tags=["auths"])
|
||||
app.include_router(users.router, prefix="/users", tags=["users"])
|
||||
app.include_router(chats.router, prefix="/chats", tags=["chats"])
|
||||
|
||||
app.include_router(documents.router, prefix="/documents", tags=["documents"])
|
||||
app.include_router(modelfiles.router, prefix="/modelfiles", tags=["modelfiles"])
|
||||
app.include_router(prompts.router, prefix="/prompts", tags=["prompts"])
|
||||
app.include_router(memories.router, prefix="/memories", tags=["memories"])
|
||||
|
||||
|
||||
app.include_router(configs.router, prefix="/configs", tags=["configs"])
|
||||
app.include_router(utils.router, prefix="/utils", tags=["utils"])
|
||||
@@ -61,6 +70,6 @@ async def get_status():
|
||||
return {
|
||||
"status": True,
|
||||
"auth": WEBUI_AUTH,
|
||||
"default_models": app.state.DEFAULT_MODELS,
|
||||
"default_prompt_suggestions": app.state.DEFAULT_PROMPT_SUGGESTIONS,
|
||||
"default_models": app.state.config.DEFAULT_MODELS,
|
||||
"default_prompt_suggestions": app.state.config.DEFAULT_PROMPT_SUGGESTIONS,
|
||||
}
|
||||
|
||||
@@ -89,6 +89,10 @@ class SignupForm(BaseModel):
|
||||
profile_image_url: Optional[str] = "/user.png"
|
||||
|
||||
|
||||
class AddUserForm(SignupForm):
|
||||
role: Optional[str] = "pending"
|
||||
|
||||
|
||||
class AuthsTable:
|
||||
def __init__(self, db):
|
||||
self.db = db
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
from pydantic import BaseModel
|
||||
from peewee import *
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
from typing import List, Union, Optional
|
||||
|
||||
from apps.web.internal.db import DB
|
||||
from apps.web.models.chats import Chats
|
||||
|
||||
import time
|
||||
import uuid
|
||||
|
||||
####################
|
||||
# Memory DB Schema
|
||||
####################
|
||||
|
||||
|
||||
class Memory(Model):
|
||||
id = CharField(unique=True)
|
||||
user_id = CharField()
|
||||
content = TextField()
|
||||
updated_at = BigIntegerField()
|
||||
created_at = BigIntegerField()
|
||||
|
||||
class Meta:
|
||||
database = DB
|
||||
|
||||
|
||||
class MemoryModel(BaseModel):
|
||||
id: str
|
||||
user_id: str
|
||||
content: str
|
||||
updated_at: int # timestamp in epoch
|
||||
created_at: int # timestamp in epoch
|
||||
|
||||
|
||||
####################
|
||||
# Forms
|
||||
####################
|
||||
|
||||
|
||||
class MemoriesTable:
|
||||
def __init__(self, db):
|
||||
self.db = db
|
||||
self.db.create_tables([Memory])
|
||||
|
||||
def insert_new_memory(
|
||||
self,
|
||||
user_id: str,
|
||||
content: str,
|
||||
) -> Optional[MemoryModel]:
|
||||
id = str(uuid.uuid4())
|
||||
|
||||
memory = MemoryModel(
|
||||
**{
|
||||
"id": id,
|
||||
"user_id": user_id,
|
||||
"content": content,
|
||||
"created_at": int(time.time()),
|
||||
"updated_at": int(time.time()),
|
||||
}
|
||||
)
|
||||
result = Memory.create(**memory.model_dump())
|
||||
if result:
|
||||
return memory
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_memories(self) -> List[MemoryModel]:
|
||||
try:
|
||||
memories = Memory.select()
|
||||
return [MemoryModel(**model_to_dict(memory)) for memory in memories]
|
||||
except:
|
||||
return None
|
||||
|
||||
def get_memories_by_user_id(self, user_id: str) -> List[MemoryModel]:
|
||||
try:
|
||||
memories = Memory.select().where(Memory.user_id == user_id)
|
||||
return [MemoryModel(**model_to_dict(memory)) for memory in memories]
|
||||
except:
|
||||
return None
|
||||
|
||||
def get_memory_by_id(self, id) -> Optional[MemoryModel]:
|
||||
try:
|
||||
memory = Memory.get(Memory.id == id)
|
||||
return MemoryModel(**model_to_dict(memory))
|
||||
except:
|
||||
return None
|
||||
|
||||
def delete_memory_by_id(self, id: str) -> bool:
|
||||
try:
|
||||
query = Memory.delete().where(Memory.id == id)
|
||||
query.execute() # Remove the rows, return number of rows removed.
|
||||
|
||||
return True
|
||||
|
||||
except:
|
||||
return False
|
||||
|
||||
def delete_memories_by_user_id(self, user_id: str) -> bool:
|
||||
try:
|
||||
query = Memory.delete().where(Memory.user_id == user_id)
|
||||
query.execute()
|
||||
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def delete_memory_by_id_and_user_id(self, id: str, user_id: str) -> bool:
|
||||
try:
|
||||
query = Memory.delete().where(Memory.id == id, Memory.user_id == user_id)
|
||||
query.execute()
|
||||
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
Memories = MemoriesTable(DB)
|
||||
@@ -123,6 +123,13 @@ class UsersTable:
|
||||
def get_num_users(self) -> Optional[int]:
|
||||
return User.select().count()
|
||||
|
||||
def get_first_user(self) -> UserModel:
|
||||
try:
|
||||
user = User.select().order_by(User.created_at).first()
|
||||
return UserModel(**model_to_dict(user))
|
||||
except:
|
||||
return None
|
||||
|
||||
def update_user_role_by_id(self, id: str, role: str) -> Optional[UserModel]:
|
||||
try:
|
||||
query = User.update(role=role).where(User.id == id)
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import logging
|
||||
|
||||
from fastapi import Request
|
||||
from fastapi import Request, UploadFile, File
|
||||
from fastapi import Depends, HTTPException, status
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
import re
|
||||
import uuid
|
||||
import csv
|
||||
|
||||
|
||||
from apps.web.models.auths import (
|
||||
SigninForm,
|
||||
SignupForm,
|
||||
AddUserForm,
|
||||
UpdateProfileForm,
|
||||
UpdatePasswordForm,
|
||||
UserResponse,
|
||||
@@ -30,7 +33,7 @@ from utils.utils import (
|
||||
from utils.misc import parse_duration, validate_email_format
|
||||
from utils.webhook import post_webhook
|
||||
from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
|
||||
from config import WEBUI_AUTH_TRUSTED_EMAIL_HEADER
|
||||
from config import WEBUI_AUTH, WEBUI_AUTH_TRUSTED_EMAIL_HEADER
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -115,13 +118,29 @@ async def signin(request: Request, form_data: SigninForm):
|
||||
),
|
||||
)
|
||||
user = Auths.authenticate_user_by_trusted_header(trusted_email)
|
||||
elif WEBUI_AUTH == False:
|
||||
admin_email = "admin@localhost"
|
||||
admin_password = "admin"
|
||||
|
||||
if Users.get_user_by_email(admin_email.lower()):
|
||||
user = Auths.authenticate_user(admin_email.lower(), admin_password)
|
||||
else:
|
||||
if Users.get_num_users() != 0:
|
||||
raise HTTPException(400, detail=ERROR_MESSAGES.EXISTING_USERS)
|
||||
|
||||
await signup(
|
||||
request,
|
||||
SignupForm(email=admin_email, password=admin_password, name="User"),
|
||||
)
|
||||
|
||||
user = Auths.authenticate_user(admin_email.lower(), admin_password)
|
||||
else:
|
||||
user = Auths.authenticate_user(form_data.email.lower(), form_data.password)
|
||||
|
||||
if user:
|
||||
token = create_token(
|
||||
data={"id": user.id},
|
||||
expires_delta=parse_duration(request.app.state.JWT_EXPIRES_IN),
|
||||
expires_delta=parse_duration(request.app.state.config.JWT_EXPIRES_IN),
|
||||
)
|
||||
|
||||
return {
|
||||
@@ -144,7 +163,7 @@ async def signin(request: Request, form_data: SigninForm):
|
||||
|
||||
@router.post("/signup", response_model=SigninResponse)
|
||||
async def signup(request: Request, form_data: SignupForm):
|
||||
if not request.app.state.ENABLE_SIGNUP:
|
||||
if not request.app.state.config.ENABLE_SIGNUP and WEBUI_AUTH:
|
||||
raise HTTPException(
|
||||
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
|
||||
)
|
||||
@@ -161,7 +180,7 @@ async def signup(request: Request, form_data: SignupForm):
|
||||
role = (
|
||||
"admin"
|
||||
if Users.get_num_users() == 0
|
||||
else request.app.state.DEFAULT_USER_ROLE
|
||||
else request.app.state.config.DEFAULT_USER_ROLE
|
||||
)
|
||||
hashed = get_password_hash(form_data.password)
|
||||
user = Auths.insert_new_auth(
|
||||
@@ -175,13 +194,13 @@ async def signup(request: Request, form_data: SignupForm):
|
||||
if user:
|
||||
token = create_token(
|
||||
data={"id": user.id},
|
||||
expires_delta=parse_duration(request.app.state.JWT_EXPIRES_IN),
|
||||
expires_delta=parse_duration(request.app.state.config.JWT_EXPIRES_IN),
|
||||
)
|
||||
# response.set_cookie(key='token', value=token, httponly=True)
|
||||
|
||||
if request.app.state.WEBHOOK_URL:
|
||||
if request.app.state.config.WEBHOOK_URL:
|
||||
post_webhook(
|
||||
request.app.state.WEBHOOK_URL,
|
||||
request.app.state.config.WEBHOOK_URL,
|
||||
WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
|
||||
{
|
||||
"action": "signup",
|
||||
@@ -205,6 +224,51 @@ async def signup(request: Request, form_data: SignupForm):
|
||||
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
|
||||
|
||||
|
||||
############################
|
||||
# AddUser
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/add", response_model=SigninResponse)
|
||||
async def add_user(form_data: AddUserForm, user=Depends(get_admin_user)):
|
||||
|
||||
if not validate_email_format(form_data.email.lower()):
|
||||
raise HTTPException(
|
||||
status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT
|
||||
)
|
||||
|
||||
if Users.get_user_by_email(form_data.email.lower()):
|
||||
raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
|
||||
|
||||
try:
|
||||
|
||||
print(form_data)
|
||||
hashed = get_password_hash(form_data.password)
|
||||
user = Auths.insert_new_auth(
|
||||
form_data.email.lower(),
|
||||
hashed,
|
||||
form_data.name,
|
||||
form_data.profile_image_url,
|
||||
form_data.role,
|
||||
)
|
||||
|
||||
if user:
|
||||
token = create_token(data={"id": user.id})
|
||||
return {
|
||||
"token": token,
|
||||
"token_type": "Bearer",
|
||||
"id": user.id,
|
||||
"email": user.email,
|
||||
"name": user.name,
|
||||
"role": user.role,
|
||||
"profile_image_url": user.profile_image_url,
|
||||
}
|
||||
else:
|
||||
raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR)
|
||||
except Exception as err:
|
||||
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
|
||||
|
||||
|
||||
############################
|
||||
# ToggleSignUp
|
||||
############################
|
||||
@@ -212,13 +276,13 @@ async def signup(request: Request, form_data: SignupForm):
|
||||
|
||||
@router.get("/signup/enabled", response_model=bool)
|
||||
async def get_sign_up_status(request: Request, user=Depends(get_admin_user)):
|
||||
return request.app.state.ENABLE_SIGNUP
|
||||
return request.app.state.config.ENABLE_SIGNUP
|
||||
|
||||
|
||||
@router.get("/signup/enabled/toggle", response_model=bool)
|
||||
async def toggle_sign_up(request: Request, user=Depends(get_admin_user)):
|
||||
request.app.state.ENABLE_SIGNUP = not request.app.state.ENABLE_SIGNUP
|
||||
return request.app.state.ENABLE_SIGNUP
|
||||
request.app.state.config.ENABLE_SIGNUP = not request.app.state.config.ENABLE_SIGNUP
|
||||
return request.app.state.config.ENABLE_SIGNUP
|
||||
|
||||
|
||||
############################
|
||||
@@ -228,7 +292,7 @@ async def toggle_sign_up(request: Request, user=Depends(get_admin_user)):
|
||||
|
||||
@router.get("/signup/user/role")
|
||||
async def get_default_user_role(request: Request, user=Depends(get_admin_user)):
|
||||
return request.app.state.DEFAULT_USER_ROLE
|
||||
return request.app.state.config.DEFAULT_USER_ROLE
|
||||
|
||||
|
||||
class UpdateRoleForm(BaseModel):
|
||||
@@ -240,8 +304,8 @@ async def update_default_user_role(
|
||||
request: Request, form_data: UpdateRoleForm, user=Depends(get_admin_user)
|
||||
):
|
||||
if form_data.role in ["pending", "user", "admin"]:
|
||||
request.app.state.DEFAULT_USER_ROLE = form_data.role
|
||||
return request.app.state.DEFAULT_USER_ROLE
|
||||
request.app.state.config.DEFAULT_USER_ROLE = form_data.role
|
||||
return request.app.state.config.DEFAULT_USER_ROLE
|
||||
|
||||
|
||||
############################
|
||||
@@ -251,7 +315,7 @@ async def update_default_user_role(
|
||||
|
||||
@router.get("/token/expires")
|
||||
async def get_token_expires_duration(request: Request, user=Depends(get_admin_user)):
|
||||
return request.app.state.JWT_EXPIRES_IN
|
||||
return request.app.state.config.JWT_EXPIRES_IN
|
||||
|
||||
|
||||
class UpdateJWTExpiresDurationForm(BaseModel):
|
||||
@@ -268,10 +332,10 @@ async def update_token_expires_duration(
|
||||
|
||||
# Check if the input string matches the pattern
|
||||
if re.match(pattern, form_data.duration):
|
||||
request.app.state.JWT_EXPIRES_IN = form_data.duration
|
||||
return request.app.state.JWT_EXPIRES_IN
|
||||
request.app.state.config.JWT_EXPIRES_IN = form_data.duration
|
||||
return request.app.state.config.JWT_EXPIRES_IN
|
||||
else:
|
||||
return request.app.state.JWT_EXPIRES_IN
|
||||
return request.app.state.config.JWT_EXPIRES_IN
|
||||
|
||||
|
||||
############################
|
||||
|
||||
@@ -58,7 +58,7 @@ async def delete_all_user_chats(request: Request, user=Depends(get_current_user)
|
||||
|
||||
if (
|
||||
user.role == "user"
|
||||
and not request.app.state.USER_PERMISSIONS["chat"]["deletion"]
|
||||
and not request.app.state.config.USER_PERMISSIONS["chat"]["deletion"]
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
@@ -93,6 +93,31 @@ async def get_archived_session_user_chat_list(
|
||||
return Chats.get_archived_chat_list_by_user_id(user.id, skip, limit)
|
||||
|
||||
|
||||
############################
|
||||
# GetSharedChatById
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/share/{share_id}", response_model=Optional[ChatResponse])
|
||||
async def get_shared_chat_by_id(share_id: str, user=Depends(get_current_user)):
|
||||
if user.role == "pending":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
|
||||
)
|
||||
|
||||
if user.role == "user":
|
||||
chat = Chats.get_chat_by_share_id(share_id)
|
||||
elif user.role == "admin":
|
||||
chat = Chats.get_chat_by_id(share_id)
|
||||
|
||||
if chat:
|
||||
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# GetChats
|
||||
############################
|
||||
@@ -141,6 +166,55 @@ async def create_new_chat(form_data: ChatForm, user=Depends(get_current_user)):
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# GetChatsByTags
|
||||
############################
|
||||
|
||||
|
||||
class TagNameForm(BaseModel):
|
||||
name: str
|
||||
skip: Optional[int] = 0
|
||||
limit: Optional[int] = 50
|
||||
|
||||
|
||||
@router.post("/tags", response_model=List[ChatTitleIdResponse])
|
||||
async def get_user_chat_list_by_tag_name(
|
||||
form_data: TagNameForm, user=Depends(get_current_user)
|
||||
):
|
||||
|
||||
print(form_data)
|
||||
chat_ids = [
|
||||
chat_id_tag.chat_id
|
||||
for chat_id_tag in Tags.get_chat_ids_by_tag_name_and_user_id(
|
||||
form_data.name, user.id
|
||||
)
|
||||
]
|
||||
|
||||
chats = Chats.get_chat_list_by_chat_ids(chat_ids, form_data.skip, form_data.limit)
|
||||
|
||||
if len(chats) == 0:
|
||||
Tags.delete_tag_by_tag_name_and_user_id(form_data.name, user.id)
|
||||
|
||||
return chats
|
||||
|
||||
|
||||
############################
|
||||
# GetAllTags
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/tags/all", response_model=List[TagModel])
|
||||
async def get_all_tags(user=Depends(get_current_user)):
|
||||
try:
|
||||
tags = Tags.get_tags_by_user_id(user.id)
|
||||
return tags
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# GetChatById
|
||||
############################
|
||||
@@ -192,7 +266,7 @@ async def delete_chat_by_id(request: Request, id: str, user=Depends(get_current_
|
||||
result = Chats.delete_chat_by_id(id)
|
||||
return result
|
||||
else:
|
||||
if not request.app.state.USER_PERMISSIONS["chat"]["deletion"]:
|
||||
if not request.app.state.config.USER_PERMISSIONS["chat"]["deletion"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
@@ -274,70 +348,6 @@ async def delete_shared_chat_by_id(id: str, user=Depends(get_current_user)):
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# GetSharedChatById
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/share/{share_id}", response_model=Optional[ChatResponse])
|
||||
async def get_shared_chat_by_id(share_id: str, user=Depends(get_current_user)):
|
||||
if user.role == "pending":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
|
||||
)
|
||||
|
||||
if user.role == "user":
|
||||
chat = Chats.get_chat_by_share_id(share_id)
|
||||
elif user.role == "admin":
|
||||
chat = Chats.get_chat_by_id(share_id)
|
||||
|
||||
if chat:
|
||||
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# GetAllTags
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/tags/all", response_model=List[TagModel])
|
||||
async def get_all_tags(user=Depends(get_current_user)):
|
||||
try:
|
||||
tags = Tags.get_tags_by_user_id(user.id)
|
||||
return tags
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# GetChatsByTags
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/tags/tag/{tag_name}", response_model=List[ChatTitleIdResponse])
|
||||
async def get_user_chat_list_by_tag_name(
|
||||
tag_name: str, user=Depends(get_current_user), skip: int = 0, limit: int = 50
|
||||
):
|
||||
chat_ids = [
|
||||
chat_id_tag.chat_id
|
||||
for chat_id_tag in Tags.get_chat_ids_by_tag_name_and_user_id(tag_name, user.id)
|
||||
]
|
||||
|
||||
chats = Chats.get_chat_list_by_chat_ids(chat_ids, skip, limit)
|
||||
|
||||
if len(chats) == 0:
|
||||
Tags.delete_tag_by_tag_name_and_user_id(tag_name, user.id)
|
||||
|
||||
return chats
|
||||
|
||||
|
||||
############################
|
||||
# GetChatTagsById
|
||||
############################
|
||||
|
||||
@@ -44,8 +44,8 @@ class SetDefaultSuggestionsForm(BaseModel):
|
||||
async def set_global_default_models(
|
||||
request: Request, form_data: SetDefaultModelsForm, user=Depends(get_admin_user)
|
||||
):
|
||||
request.app.state.DEFAULT_MODELS = form_data.models
|
||||
return request.app.state.DEFAULT_MODELS
|
||||
request.app.state.config.DEFAULT_MODELS = form_data.models
|
||||
return request.app.state.config.DEFAULT_MODELS
|
||||
|
||||
|
||||
@router.post("/default/suggestions", response_model=List[PromptSuggestion])
|
||||
@@ -55,5 +55,5 @@ async def set_global_default_suggestions(
|
||||
user=Depends(get_admin_user),
|
||||
):
|
||||
data = form_data.model_dump()
|
||||
request.app.state.DEFAULT_PROMPT_SUGGESTIONS = data["suggestions"]
|
||||
return request.app.state.DEFAULT_PROMPT_SUGGESTIONS
|
||||
request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS = data["suggestions"]
|
||||
return request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
from fastapi import Response, Request
|
||||
from fastapi import Depends, FastAPI, HTTPException, status
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Union, Optional
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
import logging
|
||||
|
||||
from apps.web.models.memories import Memories, MemoryModel
|
||||
|
||||
from utils.utils import get_verified_user
|
||||
from constants import ERROR_MESSAGES
|
||||
|
||||
from config import SRC_LOG_LEVELS, CHROMA_CLIENT
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/ef")
|
||||
async def get_embeddings(request: Request):
|
||||
return {"result": request.app.state.EMBEDDING_FUNCTION("hello world")}
|
||||
|
||||
|
||||
############################
|
||||
# GetMemories
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/", response_model=List[MemoryModel])
|
||||
async def get_memories(user=Depends(get_verified_user)):
|
||||
return Memories.get_memories_by_user_id(user.id)
|
||||
|
||||
|
||||
############################
|
||||
# AddMemory
|
||||
############################
|
||||
|
||||
|
||||
class AddMemoryForm(BaseModel):
|
||||
content: str
|
||||
|
||||
|
||||
@router.post("/add", response_model=Optional[MemoryModel])
|
||||
async def add_memory(
|
||||
request: Request, form_data: AddMemoryForm, user=Depends(get_verified_user)
|
||||
):
|
||||
memory = Memories.insert_new_memory(user.id, form_data.content)
|
||||
memory_embedding = request.app.state.EMBEDDING_FUNCTION(memory.content)
|
||||
|
||||
collection = CHROMA_CLIENT.get_or_create_collection(name=f"user-memory-{user.id}")
|
||||
collection.upsert(
|
||||
documents=[memory.content],
|
||||
ids=[memory.id],
|
||||
embeddings=[memory_embedding],
|
||||
metadatas=[{"created_at": memory.created_at}],
|
||||
)
|
||||
|
||||
return memory
|
||||
|
||||
|
||||
############################
|
||||
# QueryMemory
|
||||
############################
|
||||
|
||||
|
||||
class QueryMemoryForm(BaseModel):
|
||||
content: str
|
||||
|
||||
|
||||
@router.post("/query")
|
||||
async def query_memory(
|
||||
request: Request, form_data: QueryMemoryForm, user=Depends(get_verified_user)
|
||||
):
|
||||
query_embedding = request.app.state.EMBEDDING_FUNCTION(form_data.content)
|
||||
collection = CHROMA_CLIENT.get_or_create_collection(name=f"user-memory-{user.id}")
|
||||
|
||||
results = collection.query(
|
||||
query_embeddings=[query_embedding],
|
||||
n_results=1, # how many results to return
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
############################
|
||||
# ResetMemoryFromVectorDB
|
||||
############################
|
||||
@router.get("/reset", response_model=bool)
|
||||
async def reset_memory_from_vector_db(
|
||||
request: Request, user=Depends(get_verified_user)
|
||||
):
|
||||
CHROMA_CLIENT.delete_collection(f"user-memory-{user.id}")
|
||||
collection = CHROMA_CLIENT.get_or_create_collection(name=f"user-memory-{user.id}")
|
||||
|
||||
memories = Memories.get_memories_by_user_id(user.id)
|
||||
for memory in memories:
|
||||
memory_embedding = request.app.state.EMBEDDING_FUNCTION(memory.content)
|
||||
collection.upsert(
|
||||
documents=[memory.content],
|
||||
ids=[memory.id],
|
||||
embeddings=[memory_embedding],
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
############################
|
||||
# DeleteMemoriesByUserId
|
||||
############################
|
||||
|
||||
|
||||
@router.delete("/user", response_model=bool)
|
||||
async def delete_memory_by_user_id(user=Depends(get_verified_user)):
|
||||
result = Memories.delete_memories_by_user_id(user.id)
|
||||
|
||||
if result:
|
||||
try:
|
||||
CHROMA_CLIENT.delete_collection(f"user-memory-{user.id}")
|
||||
except Exception as e:
|
||||
log.error(e)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
############################
|
||||
# DeleteMemoryById
|
||||
############################
|
||||
|
||||
|
||||
@router.delete("/{memory_id}", response_model=bool)
|
||||
async def delete_memory_by_id(memory_id: str, user=Depends(get_verified_user)):
|
||||
result = Memories.delete_memory_by_id_and_user_id(memory_id, user.id)
|
||||
|
||||
if result:
|
||||
collection = CHROMA_CLIENT.get_or_create_collection(
|
||||
name=f"user-memory-{user.id}"
|
||||
)
|
||||
collection.delete(ids=[memory_id])
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -11,8 +11,9 @@ import logging
|
||||
|
||||
from apps.web.models.users import UserModel, UserUpdateForm, UserRoleUpdateForm, Users
|
||||
from apps.web.models.auths import Auths
|
||||
from apps.web.models.chats import Chats
|
||||
|
||||
from utils.utils import get_current_user, get_password_hash, get_admin_user
|
||||
from utils.utils import get_verified_user, get_password_hash, get_admin_user
|
||||
from constants import ERROR_MESSAGES
|
||||
|
||||
from config import SRC_LOG_LEVELS
|
||||
@@ -39,15 +40,15 @@ async def get_users(skip: int = 0, limit: int = 50, user=Depends(get_admin_user)
|
||||
|
||||
@router.get("/permissions/user")
|
||||
async def get_user_permissions(request: Request, user=Depends(get_admin_user)):
|
||||
return request.app.state.USER_PERMISSIONS
|
||||
return request.app.state.config.USER_PERMISSIONS
|
||||
|
||||
|
||||
@router.post("/permissions/user")
|
||||
async def update_user_permissions(
|
||||
request: Request, form_data: dict, user=Depends(get_admin_user)
|
||||
):
|
||||
request.app.state.USER_PERMISSIONS = form_data
|
||||
return request.app.state.USER_PERMISSIONS
|
||||
request.app.state.config.USER_PERMISSIONS = form_data
|
||||
return request.app.state.config.USER_PERMISSIONS
|
||||
|
||||
|
||||
############################
|
||||
@@ -58,7 +59,7 @@ async def update_user_permissions(
|
||||
@router.post("/update/role", response_model=Optional[UserModel])
|
||||
async def update_user_role(form_data: UserRoleUpdateForm, user=Depends(get_admin_user)):
|
||||
|
||||
if user.id != form_data.id:
|
||||
if user.id != form_data.id and form_data.id != Users.get_first_user().id:
|
||||
return Users.update_user_role_by_id(form_data.id, form_data.role)
|
||||
|
||||
raise HTTPException(
|
||||
@@ -67,6 +68,41 @@ async def update_user_role(form_data: UserRoleUpdateForm, user=Depends(get_admin
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# GetUserById
|
||||
############################
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
name: str
|
||||
profile_image_url: str
|
||||
|
||||
|
||||
@router.get("/{user_id}", response_model=UserResponse)
|
||||
async def get_user_by_id(user_id: str, user=Depends(get_verified_user)):
|
||||
|
||||
if user_id.startswith("shared-"):
|
||||
chat_id = user_id.replace("shared-", "")
|
||||
chat = Chats.get_chat_by_id(chat_id)
|
||||
if chat:
|
||||
user_id = chat.user_id
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.USER_NOT_FOUND,
|
||||
)
|
||||
|
||||
user = Users.get_user_by_id(user_id)
|
||||
|
||||
if user:
|
||||
return UserResponse(name=user.name, profile_image_url=user.profile_image_url)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.USER_NOT_FOUND,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# UpdateUserById
|
||||
############################
|
||||
|
||||
+345
-90
@@ -5,6 +5,7 @@ import chromadb
|
||||
from chromadb import Settings
|
||||
from base64 import b64encode
|
||||
from bs4 import BeautifulSoup
|
||||
from typing import TypeVar, Generic, Union
|
||||
|
||||
from pathlib import Path
|
||||
import json
|
||||
@@ -17,6 +18,17 @@ import shutil
|
||||
from secrets import token_bytes
|
||||
from constants import ERROR_MESSAGES
|
||||
|
||||
####################################
|
||||
# Load .env file
|
||||
####################################
|
||||
|
||||
try:
|
||||
from dotenv import load_dotenv, find_dotenv
|
||||
|
||||
load_dotenv(find_dotenv("../.env"))
|
||||
except ImportError:
|
||||
print("dotenv not installed, skipping...")
|
||||
|
||||
|
||||
####################################
|
||||
# LOGGING
|
||||
@@ -59,23 +71,15 @@ for source in log_sources:
|
||||
|
||||
log.setLevel(SRC_LOG_LEVELS["CONFIG"])
|
||||
|
||||
####################################
|
||||
# Load .env file
|
||||
####################################
|
||||
|
||||
try:
|
||||
from dotenv import load_dotenv, find_dotenv
|
||||
|
||||
load_dotenv(find_dotenv("../.env"))
|
||||
except ImportError:
|
||||
log.warning("dotenv not installed, skipping...")
|
||||
|
||||
WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI")
|
||||
if WEBUI_NAME != "Open WebUI":
|
||||
WEBUI_NAME += " (Open WebUI)"
|
||||
|
||||
WEBUI_URL = os.environ.get("WEBUI_URL", "http://localhost:3000")
|
||||
|
||||
WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png"
|
||||
|
||||
|
||||
####################################
|
||||
# ENV (dev,test,prod)
|
||||
####################################
|
||||
@@ -149,6 +153,13 @@ for version in soup.find_all("h2"):
|
||||
|
||||
CHANGELOG = changelog_json
|
||||
|
||||
|
||||
####################################
|
||||
# WEBUI_VERSION
|
||||
####################################
|
||||
|
||||
WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.100")
|
||||
|
||||
####################################
|
||||
# DATA/FRONTEND BUILD DIR
|
||||
####################################
|
||||
@@ -162,13 +173,119 @@ try:
|
||||
except:
|
||||
CONFIG_DATA = {}
|
||||
|
||||
|
||||
####################################
|
||||
# Config helpers
|
||||
####################################
|
||||
|
||||
|
||||
def save_config():
|
||||
try:
|
||||
with open(f"{DATA_DIR}/config.json", "w") as f:
|
||||
json.dump(CONFIG_DATA, f, indent="\t")
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
||||
|
||||
def get_config_value(config_path: str):
|
||||
path_parts = config_path.split(".")
|
||||
cur_config = CONFIG_DATA
|
||||
for key in path_parts:
|
||||
if key in cur_config:
|
||||
cur_config = cur_config[key]
|
||||
else:
|
||||
return None
|
||||
return cur_config
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class PersistentConfig(Generic[T]):
|
||||
def __init__(self, env_name: str, config_path: str, env_value: T):
|
||||
self.env_name = env_name
|
||||
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:
|
||||
log.info(f"'{env_name}' loaded from config.json")
|
||||
self.value = self.config_value
|
||||
else:
|
||||
self.value = env_value
|
||||
|
||||
def __str__(self):
|
||||
return str(self.value)
|
||||
|
||||
@property
|
||||
def __dict__(self):
|
||||
raise TypeError(
|
||||
"PersistentConfig object cannot be converted to dict, use config_get or .value instead."
|
||||
)
|
||||
|
||||
def __getattribute__(self, item):
|
||||
if item == "__dict__":
|
||||
raise TypeError(
|
||||
"PersistentConfig object cannot be converted to dict, use config_get or .value instead."
|
||||
)
|
||||
return super().__getattribute__(item)
|
||||
|
||||
def save(self):
|
||||
# Don't save if the value is the same as the env value and the config value
|
||||
if self.env_value == self.value:
|
||||
if self.config_value == self.value:
|
||||
return
|
||||
log.info(f"Saving '{self.env_name}' to config.json")
|
||||
path_parts = self.config_path.split(".")
|
||||
config = CONFIG_DATA
|
||||
for key in path_parts[:-1]:
|
||||
if key not in config:
|
||||
config[key] = {}
|
||||
config = config[key]
|
||||
config[path_parts[-1]] = self.value
|
||||
save_config()
|
||||
self.config_value = self.value
|
||||
|
||||
|
||||
class AppConfig:
|
||||
_state: dict[str, PersistentConfig]
|
||||
|
||||
def __init__(self):
|
||||
super().__setattr__("_state", {})
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
if isinstance(value, PersistentConfig):
|
||||
self._state[key] = value
|
||||
else:
|
||||
self._state[key].value = value
|
||||
self._state[key].save()
|
||||
|
||||
def __getattr__(self, key):
|
||||
return self._state[key].value
|
||||
|
||||
|
||||
####################################
|
||||
# WEBUI_AUTH (Required for security)
|
||||
####################################
|
||||
|
||||
WEBUI_AUTH = os.environ.get("WEBUI_AUTH", "True").lower() == "true"
|
||||
WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get(
|
||||
"WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None
|
||||
)
|
||||
JWT_EXPIRES_IN = PersistentConfig(
|
||||
"JWT_EXPIRES_IN", "auth.jwt_expiry", os.environ.get("JWT_EXPIRES_IN", "-1")
|
||||
)
|
||||
|
||||
####################################
|
||||
# Static DIR
|
||||
####################################
|
||||
|
||||
STATIC_DIR = str(Path(os.getenv("STATIC_DIR", "./static")).resolve())
|
||||
|
||||
shutil.copyfile(f"{FRONTEND_BUILD_DIR}/favicon.png", f"{STATIC_DIR}/favicon.png")
|
||||
frontend_favicon = f"{FRONTEND_BUILD_DIR}/favicon.png"
|
||||
if os.path.exists(frontend_favicon):
|
||||
shutil.copyfile(frontend_favicon, f"{STATIC_DIR}/favicon.png")
|
||||
else:
|
||||
logging.warning(f"Frontend favicon not found at {frontend_favicon}")
|
||||
|
||||
####################################
|
||||
# CUSTOM_NAME
|
||||
@@ -292,12 +409,22 @@ OLLAMA_BASE_URLS = os.environ.get("OLLAMA_BASE_URLS", "")
|
||||
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS if OLLAMA_BASE_URLS != "" else OLLAMA_BASE_URL
|
||||
|
||||
OLLAMA_BASE_URLS = [url.strip() for url in OLLAMA_BASE_URLS.split(";")]
|
||||
|
||||
OLLAMA_BASE_URLS = PersistentConfig(
|
||||
"OLLAMA_BASE_URLS", "ollama.base_urls", OLLAMA_BASE_URLS
|
||||
)
|
||||
|
||||
####################################
|
||||
# OPENAI_API
|
||||
####################################
|
||||
|
||||
|
||||
ENABLE_OPENAI_API = PersistentConfig(
|
||||
"ENABLE_OPENAI_API",
|
||||
"openai.enable",
|
||||
os.environ.get("ENABLE_OPENAI_API", "True").lower() == "true",
|
||||
)
|
||||
|
||||
|
||||
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
|
||||
OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "")
|
||||
|
||||
@@ -309,7 +436,9 @@ OPENAI_API_KEYS = os.environ.get("OPENAI_API_KEYS", "")
|
||||
OPENAI_API_KEYS = OPENAI_API_KEYS if OPENAI_API_KEYS != "" else OPENAI_API_KEY
|
||||
|
||||
OPENAI_API_KEYS = [url.strip() for url in OPENAI_API_KEYS.split(";")]
|
||||
|
||||
OPENAI_API_KEYS = PersistentConfig(
|
||||
"OPENAI_API_KEYS", "openai.api_keys", OPENAI_API_KEYS
|
||||
)
|
||||
|
||||
OPENAI_API_BASE_URLS = os.environ.get("OPENAI_API_BASE_URLS", "")
|
||||
OPENAI_API_BASE_URLS = (
|
||||
@@ -320,33 +449,42 @@ OPENAI_API_BASE_URLS = [
|
||||
url.strip() if url != "" else "https://api.openai.com/v1"
|
||||
for url in OPENAI_API_BASE_URLS.split(";")
|
||||
]
|
||||
OPENAI_API_BASE_URLS = PersistentConfig(
|
||||
"OPENAI_API_BASE_URLS", "openai.api_base_urls", OPENAI_API_BASE_URLS
|
||||
)
|
||||
|
||||
OPENAI_API_KEY = ""
|
||||
|
||||
try:
|
||||
OPENAI_API_KEY = OPENAI_API_KEYS[
|
||||
OPENAI_API_BASE_URLS.index("https://api.openai.com/v1")
|
||||
OPENAI_API_KEY = OPENAI_API_KEYS.value[
|
||||
OPENAI_API_BASE_URLS.value.index("https://api.openai.com/v1")
|
||||
]
|
||||
except:
|
||||
pass
|
||||
|
||||
OPENAI_API_BASE_URL = "https://api.openai.com/v1"
|
||||
|
||||
|
||||
####################################
|
||||
# WEBUI
|
||||
####################################
|
||||
|
||||
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "True").lower() == "true"
|
||||
DEFAULT_MODELS = os.environ.get("DEFAULT_MODELS", None)
|
||||
ENABLE_SIGNUP = PersistentConfig(
|
||||
"ENABLE_SIGNUP",
|
||||
"ui.enable_signup",
|
||||
(
|
||||
False
|
||||
if not WEBUI_AUTH
|
||||
else os.environ.get("ENABLE_SIGNUP", "True").lower() == "true"
|
||||
),
|
||||
)
|
||||
DEFAULT_MODELS = PersistentConfig(
|
||||
"DEFAULT_MODELS", "ui.default_models", os.environ.get("DEFAULT_MODELS", None)
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_PROMPT_SUGGESTIONS = (
|
||||
CONFIG_DATA["ui"]["prompt_suggestions"]
|
||||
if "ui" in CONFIG_DATA
|
||||
and "prompt_suggestions" in CONFIG_DATA["ui"]
|
||||
and type(CONFIG_DATA["ui"]["prompt_suggestions"]) is list
|
||||
else [
|
||||
DEFAULT_PROMPT_SUGGESTIONS = PersistentConfig(
|
||||
"DEFAULT_PROMPT_SUGGESTIONS",
|
||||
"ui.prompt_suggestions",
|
||||
[
|
||||
{
|
||||
"title": ["Help me study", "vocabulary for a college entrance exam"],
|
||||
"content": "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option.",
|
||||
@@ -363,41 +501,54 @@ DEFAULT_PROMPT_SUGGESTIONS = (
|
||||
"title": ["Show me a code snippet", "of a website's sticky header"],
|
||||
"content": "Show me a code snippet of a website's sticky header in CSS and JavaScript.",
|
||||
},
|
||||
]
|
||||
{
|
||||
"title": [
|
||||
"Explain options trading",
|
||||
"if I'm familiar with buying and selling stocks",
|
||||
],
|
||||
"content": "Explain options trading in simple terms if I'm familiar with buying and selling stocks.",
|
||||
},
|
||||
{
|
||||
"title": ["Overcome procrastination", "give me tips"],
|
||||
"content": "Could you start by asking me about instances when I procrastinate the most and then give me some suggestions to overcome it?",
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_USER_ROLE = os.getenv("DEFAULT_USER_ROLE", "pending")
|
||||
DEFAULT_USER_ROLE = PersistentConfig(
|
||||
"DEFAULT_USER_ROLE",
|
||||
"ui.default_user_role",
|
||||
os.getenv("DEFAULT_USER_ROLE", "pending"),
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_CHAT_DELETION = (
|
||||
os.environ.get("USER_PERMISSIONS_CHAT_DELETION", "True").lower() == "true"
|
||||
)
|
||||
|
||||
USER_PERMISSIONS = {"chat": {"deletion": USER_PERMISSIONS_CHAT_DELETION}}
|
||||
USER_PERMISSIONS = PersistentConfig(
|
||||
"USER_PERMISSIONS",
|
||||
"ui.user_permissions",
|
||||
{"chat": {"deletion": USER_PERMISSIONS_CHAT_DELETION}},
|
||||
)
|
||||
|
||||
ENABLE_MODEL_FILTER = os.environ.get("ENABLE_MODEL_FILTER", "False").lower() == "true"
|
||||
ENABLE_MODEL_FILTER = PersistentConfig(
|
||||
"ENABLE_MODEL_FILTER",
|
||||
"model_filter.enable",
|
||||
os.environ.get("ENABLE_MODEL_FILTER", "False").lower() == "true",
|
||||
)
|
||||
MODEL_FILTER_LIST = os.environ.get("MODEL_FILTER_LIST", "")
|
||||
MODEL_FILTER_LIST = [model.strip() for model in MODEL_FILTER_LIST.split(";")]
|
||||
MODEL_FILTER_LIST = PersistentConfig(
|
||||
"MODEL_FILTER_LIST",
|
||||
"model_filter.list",
|
||||
[model.strip() for model in MODEL_FILTER_LIST.split(";")],
|
||||
)
|
||||
|
||||
WEBHOOK_URL = os.environ.get("WEBHOOK_URL", "")
|
||||
WEBHOOK_URL = PersistentConfig(
|
||||
"WEBHOOK_URL", "webhook_url", os.environ.get("WEBHOOK_URL", "")
|
||||
)
|
||||
|
||||
ENABLE_ADMIN_EXPORT = os.environ.get("ENABLE_ADMIN_EXPORT", "True").lower() == "true"
|
||||
|
||||
####################################
|
||||
# WEBUI_VERSION
|
||||
####################################
|
||||
|
||||
WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.100")
|
||||
|
||||
####################################
|
||||
# WEBUI_AUTH (Required for security)
|
||||
####################################
|
||||
|
||||
WEBUI_AUTH = True
|
||||
WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get(
|
||||
"WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None
|
||||
)
|
||||
|
||||
####################################
|
||||
# WEBUI_SECRET_KEY
|
||||
####################################
|
||||
@@ -432,21 +583,45 @@ else:
|
||||
CHROMA_HTTP_SSL = os.environ.get("CHROMA_HTTP_SSL", "false").lower() == "true"
|
||||
# this uses the model defined in the Dockerfile ENV variable. If you dont use docker or docker based deployments such as k8s, the default embedding model will be used (sentence-transformers/all-MiniLM-L6-v2)
|
||||
|
||||
RAG_TOP_K = int(os.environ.get("RAG_TOP_K", "5"))
|
||||
RAG_RELEVANCE_THRESHOLD = float(os.environ.get("RAG_RELEVANCE_THRESHOLD", "0.0"))
|
||||
|
||||
ENABLE_RAG_HYBRID_SEARCH = (
|
||||
os.environ.get("ENABLE_RAG_HYBRID_SEARCH", "").lower() == "true"
|
||||
RAG_TOP_K = PersistentConfig(
|
||||
"RAG_TOP_K", "rag.top_k", int(os.environ.get("RAG_TOP_K", "5"))
|
||||
)
|
||||
RAG_RELEVANCE_THRESHOLD = PersistentConfig(
|
||||
"RAG_RELEVANCE_THRESHOLD",
|
||||
"rag.relevance_threshold",
|
||||
float(os.environ.get("RAG_RELEVANCE_THRESHOLD", "0.0")),
|
||||
)
|
||||
|
||||
RAG_EMBEDDING_ENGINE = os.environ.get("RAG_EMBEDDING_ENGINE", "")
|
||||
|
||||
PDF_EXTRACT_IMAGES = os.environ.get("PDF_EXTRACT_IMAGES", "False").lower() == "true"
|
||||
|
||||
RAG_EMBEDDING_MODEL = os.environ.get(
|
||||
"RAG_EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2"
|
||||
ENABLE_RAG_HYBRID_SEARCH = PersistentConfig(
|
||||
"ENABLE_RAG_HYBRID_SEARCH",
|
||||
"rag.enable_hybrid_search",
|
||||
os.environ.get("ENABLE_RAG_HYBRID_SEARCH", "").lower() == "true",
|
||||
)
|
||||
log.info(f"Embedding model set: {RAG_EMBEDDING_MODEL}"),
|
||||
|
||||
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",
|
||||
os.environ.get("RAG_EMBEDDING_ENGINE", ""),
|
||||
)
|
||||
|
||||
PDF_EXTRACT_IMAGES = PersistentConfig(
|
||||
"PDF_EXTRACT_IMAGES",
|
||||
"rag.pdf_extract_images",
|
||||
os.environ.get("PDF_EXTRACT_IMAGES", "False").lower() == "true",
|
||||
)
|
||||
|
||||
RAG_EMBEDDING_MODEL = PersistentConfig(
|
||||
"RAG_EMBEDDING_MODEL",
|
||||
"rag.embedding_model",
|
||||
os.environ.get("RAG_EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2"),
|
||||
)
|
||||
log.info(f"Embedding model set: {RAG_EMBEDDING_MODEL.value}"),
|
||||
|
||||
RAG_EMBEDDING_MODEL_AUTO_UPDATE = (
|
||||
os.environ.get("RAG_EMBEDDING_MODEL_AUTO_UPDATE", "").lower() == "true"
|
||||
@@ -456,9 +631,13 @@ RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE = (
|
||||
os.environ.get("RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE", "").lower() == "true"
|
||||
)
|
||||
|
||||
RAG_RERANKING_MODEL = os.environ.get("RAG_RERANKING_MODEL", "")
|
||||
if not RAG_RERANKING_MODEL == "":
|
||||
log.info(f"Reranking model set: {RAG_RERANKING_MODEL}"),
|
||||
RAG_RERANKING_MODEL = PersistentConfig(
|
||||
"RAG_RERANKING_MODEL",
|
||||
"rag.reranking_model",
|
||||
os.environ.get("RAG_RERANKING_MODEL", ""),
|
||||
)
|
||||
if RAG_RERANKING_MODEL.value != "":
|
||||
log.info(f"Reranking model set: {RAG_RERANKING_MODEL.value}"),
|
||||
|
||||
RAG_RERANKING_MODEL_AUTO_UPDATE = (
|
||||
os.environ.get("RAG_RERANKING_MODEL_AUTO_UPDATE", "").lower() == "true"
|
||||
@@ -468,13 +647,6 @@ RAG_RERANKING_MODEL_TRUST_REMOTE_CODE = (
|
||||
os.environ.get("RAG_RERANKING_MODEL_TRUST_REMOTE_CODE", "").lower() == "true"
|
||||
)
|
||||
|
||||
# device type embedding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance
|
||||
USE_CUDA = os.environ.get("USE_CUDA_DOCKER", "false")
|
||||
|
||||
if USE_CUDA.lower() == "true":
|
||||
DEVICE_TYPE = "cuda"
|
||||
else:
|
||||
DEVICE_TYPE = "cpu"
|
||||
|
||||
if CHROMA_HTTP_HOST != "":
|
||||
CHROMA_CLIENT = chromadb.HttpClient(
|
||||
@@ -494,8 +666,23 @@ else:
|
||||
database=CHROMA_DATABASE,
|
||||
)
|
||||
|
||||
CHUNK_SIZE = int(os.environ.get("CHUNK_SIZE", "1500"))
|
||||
CHUNK_OVERLAP = int(os.environ.get("CHUNK_OVERLAP", "100"))
|
||||
|
||||
# device type embedding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance
|
||||
USE_CUDA = os.environ.get("USE_CUDA_DOCKER", "false")
|
||||
|
||||
if USE_CUDA.lower() == "true":
|
||||
DEVICE_TYPE = "cuda"
|
||||
else:
|
||||
DEVICE_TYPE = "cpu"
|
||||
|
||||
CHUNK_SIZE = PersistentConfig(
|
||||
"CHUNK_SIZE", "rag.chunk_size", int(os.environ.get("CHUNK_SIZE", "1500"))
|
||||
)
|
||||
CHUNK_OVERLAP = PersistentConfig(
|
||||
"CHUNK_OVERLAP",
|
||||
"rag.chunk_overlap",
|
||||
int(os.environ.get("CHUNK_OVERLAP", "100")),
|
||||
)
|
||||
|
||||
DEFAULT_RAG_TEMPLATE = """Use the following context as your learned knowledge, inside <context></context> XML tags.
|
||||
<context>
|
||||
@@ -511,10 +698,32 @@ And answer according to the language of the user's question.
|
||||
Given the context information, answer the query.
|
||||
Query: [query]"""
|
||||
|
||||
RAG_TEMPLATE = os.environ.get("RAG_TEMPLATE", DEFAULT_RAG_TEMPLATE)
|
||||
RAG_TEMPLATE = PersistentConfig(
|
||||
"RAG_TEMPLATE",
|
||||
"rag.template",
|
||||
os.environ.get("RAG_TEMPLATE", DEFAULT_RAG_TEMPLATE),
|
||||
)
|
||||
|
||||
RAG_OPENAI_API_BASE_URL = os.getenv("RAG_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL)
|
||||
RAG_OPENAI_API_KEY = os.getenv("RAG_OPENAI_API_KEY", OPENAI_API_KEY)
|
||||
RAG_OPENAI_API_BASE_URL = PersistentConfig(
|
||||
"RAG_OPENAI_API_BASE_URL",
|
||||
"rag.openai_api_base_url",
|
||||
os.getenv("RAG_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
|
||||
)
|
||||
RAG_OPENAI_API_KEY = PersistentConfig(
|
||||
"RAG_OPENAI_API_KEY",
|
||||
"rag.openai_api_key",
|
||||
os.getenv("RAG_OPENAI_API_KEY", OPENAI_API_KEY),
|
||||
)
|
||||
|
||||
ENABLE_RAG_LOCAL_WEB_FETCH = (
|
||||
os.getenv("ENABLE_RAG_LOCAL_WEB_FETCH", "False").lower() == "true"
|
||||
)
|
||||
|
||||
YOUTUBE_LOADER_LANGUAGE = PersistentConfig(
|
||||
"YOUTUBE_LOADER_LANGUAGE",
|
||||
"rag.youtube_loader_language",
|
||||
os.getenv("YOUTUBE_LOADER_LANGUAGE", "en").split(","),
|
||||
)
|
||||
|
||||
####################################
|
||||
# Transcribe
|
||||
@@ -531,32 +740,78 @@ WHISPER_MODEL_AUTO_UPDATE = (
|
||||
# Images
|
||||
####################################
|
||||
|
||||
IMAGE_GENERATION_ENGINE = os.getenv("IMAGE_GENERATION_ENGINE", "")
|
||||
|
||||
ENABLE_IMAGE_GENERATION = (
|
||||
os.environ.get("ENABLE_IMAGE_GENERATION", "").lower() == "true"
|
||||
IMAGE_GENERATION_ENGINE = PersistentConfig(
|
||||
"IMAGE_GENERATION_ENGINE",
|
||||
"image_generation.engine",
|
||||
os.getenv("IMAGE_GENERATION_ENGINE", ""),
|
||||
)
|
||||
AUTOMATIC1111_BASE_URL = os.getenv("AUTOMATIC1111_BASE_URL", "")
|
||||
|
||||
COMFYUI_BASE_URL = os.getenv("COMFYUI_BASE_URL", "")
|
||||
|
||||
IMAGES_OPENAI_API_BASE_URL = os.getenv(
|
||||
"IMAGES_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL
|
||||
ENABLE_IMAGE_GENERATION = PersistentConfig(
|
||||
"ENABLE_IMAGE_GENERATION",
|
||||
"image_generation.enable",
|
||||
os.environ.get("ENABLE_IMAGE_GENERATION", "").lower() == "true",
|
||||
)
|
||||
AUTOMATIC1111_BASE_URL = PersistentConfig(
|
||||
"AUTOMATIC1111_BASE_URL",
|
||||
"image_generation.automatic1111.base_url",
|
||||
os.getenv("AUTOMATIC1111_BASE_URL", ""),
|
||||
)
|
||||
IMAGES_OPENAI_API_KEY = os.getenv("IMAGES_OPENAI_API_KEY", OPENAI_API_KEY)
|
||||
|
||||
IMAGE_SIZE = os.getenv("IMAGE_SIZE", "512x512")
|
||||
COMFYUI_BASE_URL = PersistentConfig(
|
||||
"COMFYUI_BASE_URL",
|
||||
"image_generation.comfyui.base_url",
|
||||
os.getenv("COMFYUI_BASE_URL", ""),
|
||||
)
|
||||
|
||||
IMAGE_STEPS = int(os.getenv("IMAGE_STEPS", 50))
|
||||
IMAGES_OPENAI_API_BASE_URL = PersistentConfig(
|
||||
"IMAGES_OPENAI_API_BASE_URL",
|
||||
"image_generation.openai.api_base_url",
|
||||
os.getenv("IMAGES_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
|
||||
)
|
||||
IMAGES_OPENAI_API_KEY = PersistentConfig(
|
||||
"IMAGES_OPENAI_API_KEY",
|
||||
"image_generation.openai.api_key",
|
||||
os.getenv("IMAGES_OPENAI_API_KEY", OPENAI_API_KEY),
|
||||
)
|
||||
|
||||
IMAGE_GENERATION_MODEL = os.getenv("IMAGE_GENERATION_MODEL", "")
|
||||
IMAGE_SIZE = PersistentConfig(
|
||||
"IMAGE_SIZE", "image_generation.size", os.getenv("IMAGE_SIZE", "512x512")
|
||||
)
|
||||
|
||||
IMAGE_STEPS = PersistentConfig(
|
||||
"IMAGE_STEPS", "image_generation.steps", int(os.getenv("IMAGE_STEPS", 50))
|
||||
)
|
||||
|
||||
IMAGE_GENERATION_MODEL = PersistentConfig(
|
||||
"IMAGE_GENERATION_MODEL",
|
||||
"image_generation.model",
|
||||
os.getenv("IMAGE_GENERATION_MODEL", ""),
|
||||
)
|
||||
|
||||
####################################
|
||||
# Audio
|
||||
####################################
|
||||
|
||||
AUDIO_OPENAI_API_BASE_URL = os.getenv("AUDIO_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL)
|
||||
AUDIO_OPENAI_API_KEY = os.getenv("AUDIO_OPENAI_API_KEY", OPENAI_API_KEY)
|
||||
AUDIO_OPENAI_API_BASE_URL = PersistentConfig(
|
||||
"AUDIO_OPENAI_API_BASE_URL",
|
||||
"audio.openai.api_base_url",
|
||||
os.getenv("AUDIO_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
|
||||
)
|
||||
AUDIO_OPENAI_API_KEY = PersistentConfig(
|
||||
"AUDIO_OPENAI_API_KEY",
|
||||
"audio.openai.api_key",
|
||||
os.getenv("AUDIO_OPENAI_API_KEY", OPENAI_API_KEY),
|
||||
)
|
||||
AUDIO_OPENAI_API_MODEL = PersistentConfig(
|
||||
"AUDIO_OPENAI_API_MODEL",
|
||||
"audio.openai.api_model",
|
||||
os.getenv("AUDIO_OPENAI_API_MODEL", "tts-1"),
|
||||
)
|
||||
AUDIO_OPENAI_API_VOICE = PersistentConfig(
|
||||
"AUDIO_OPENAI_API_VOICE",
|
||||
"audio.openai.api_voice",
|
||||
os.getenv("AUDIO_OPENAI_API_VOICE", "alloy"),
|
||||
)
|
||||
|
||||
####################################
|
||||
# LiteLLM
|
||||
|
||||
@@ -42,6 +42,9 @@ class ERROR_MESSAGES(str, Enum):
|
||||
"The password provided is incorrect. Please check for typos and try again."
|
||||
)
|
||||
INVALID_TRUSTED_HEADER = "Your provider has not provided a trusted header. Please contact your administrator for assistance."
|
||||
|
||||
EXISTING_USERS = "You can't turn off authentication because there are existing users. If you want to disable WEBUI_AUTH, make sure your web interface doesn't have any existing users and is a fresh installation."
|
||||
|
||||
UNAUTHORIZED = "401 Unauthorized"
|
||||
ACCESS_PROHIBITED = "You do not have permission to access this resource. Please contact your administrator for assistance."
|
||||
ACTION_PROHIBITED = (
|
||||
@@ -71,3 +74,7 @@ class ERROR_MESSAGES(str, Enum):
|
||||
EMPTY_CONTENT = "The content provided is empty. Please ensure that there is text or data present before proceeding."
|
||||
|
||||
DB_NOT_SQLITE = "This feature is only available when running with SQLite databases."
|
||||
|
||||
INVALID_URL = (
|
||||
"Oops! The URL you provided is invalid. Please double-check and try again."
|
||||
)
|
||||
|
||||
@@ -18,6 +18,18 @@
|
||||
{
|
||||
"title": ["Show me a code snippet", "of a website's sticky header"],
|
||||
"content": "Show me a code snippet of a website's sticky header in CSS and JavaScript."
|
||||
},
|
||||
{
|
||||
"title": ["Explain options trading", "if I'm familiar with buying and selling stocks"],
|
||||
"content": "Explain options trading in simple terms if I'm familiar with buying and selling stocks."
|
||||
},
|
||||
{
|
||||
"title": ["Overcome procrastination", "give me tips"],
|
||||
"content": "Could you start by asking me about instances when I procrastinate the most and then give me some suggestions to overcome it?"
|
||||
},
|
||||
{
|
||||
"title": ["Grammar check", "rewrite it for better readability "],
|
||||
"content": "Check the following sentence for grammar and clarity: \"[sentence]\". Rewrite it for better readability while maintaining its original meaning."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Regular → Executable
+137
-49
@@ -1,3 +1,4 @@
|
||||
from contextlib import asynccontextmanager
|
||||
from bs4 import BeautifulSoup
|
||||
import json
|
||||
import markdown
|
||||
@@ -15,7 +16,7 @@ from fastapi.middleware.wsgi import WSGIMiddleware
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
|
||||
from starlette.responses import StreamingResponse, Response
|
||||
|
||||
from apps.ollama.main import app as ollama_app
|
||||
from apps.openai.main import app as openai_app
|
||||
@@ -25,6 +26,8 @@ from apps.litellm.main import (
|
||||
start_litellm_background,
|
||||
shutdown_litellm_background,
|
||||
)
|
||||
|
||||
|
||||
from apps.audio.main import app as audio_app
|
||||
from apps.images.main import app as images_app
|
||||
from apps.rag.main import app as rag_app
|
||||
@@ -41,6 +44,8 @@ from apps.rag.utils import rag_messages
|
||||
from config import (
|
||||
CONFIG_DATA,
|
||||
WEBUI_NAME,
|
||||
WEBUI_URL,
|
||||
WEBUI_AUTH,
|
||||
ENV,
|
||||
VERSION,
|
||||
CHANGELOG,
|
||||
@@ -54,6 +59,7 @@ from config import (
|
||||
SRC_LOG_LEVELS,
|
||||
WEBHOOK_URL,
|
||||
ENABLE_ADMIN_EXPORT,
|
||||
AppConfig,
|
||||
)
|
||||
from constants import ERROR_MESSAGES
|
||||
|
||||
@@ -74,7 +80,7 @@ class SPAStaticFiles(StaticFiles):
|
||||
|
||||
|
||||
print(
|
||||
f"""
|
||||
rf"""
|
||||
___ __ __ _ _ _ ___
|
||||
/ _ \ _ __ ___ _ __ \ \ / /__| |__ | | | |_ _|
|
||||
| | | | '_ \ / _ \ '_ \ \ \ /\ / / _ \ '_ \| | | || |
|
||||
@@ -88,18 +94,45 @@ https://github.com/open-webui/open-webui
|
||||
"""
|
||||
)
|
||||
|
||||
app = FastAPI(docs_url="/docs" if ENV == "dev" else None, redoc_url=None)
|
||||
|
||||
app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
|
||||
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
if ENABLE_LITELLM:
|
||||
asyncio.create_task(start_litellm_background())
|
||||
yield
|
||||
if ENABLE_LITELLM:
|
||||
await shutdown_litellm_background()
|
||||
|
||||
app.state.WEBHOOK_URL = WEBHOOK_URL
|
||||
|
||||
app = FastAPI(
|
||||
docs_url="/docs" if ENV == "dev" else None, redoc_url=None, lifespan=lifespan
|
||||
)
|
||||
|
||||
app.state.config = AppConfig()
|
||||
app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
|
||||
app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
||||
|
||||
app.state.config.WEBHOOK_URL = WEBHOOK_URL
|
||||
|
||||
origins = ["*"]
|
||||
|
||||
|
||||
# Custom middleware to add security headers
|
||||
# class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
||||
# async def dispatch(self, request: Request, call_next):
|
||||
# response: Response = await call_next(request)
|
||||
# response.headers["Cross-Origin-Opener-Policy"] = "same-origin"
|
||||
# response.headers["Cross-Origin-Embedder-Policy"] = "require-corp"
|
||||
# return response
|
||||
|
||||
|
||||
# app.add_middleware(SecurityHeadersMiddleware)
|
||||
|
||||
|
||||
class RAGMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
return_citations = False
|
||||
|
||||
if request.method == "POST" and (
|
||||
"/api/chat" in request.url.path or "/chat/completions" in request.url.path
|
||||
):
|
||||
@@ -112,23 +145,29 @@ class RAGMiddleware(BaseHTTPMiddleware):
|
||||
# Parse string to JSON
|
||||
data = json.loads(body_str) if body_str else {}
|
||||
|
||||
return_citations = data.get("citations", False)
|
||||
if "citations" in data:
|
||||
del data["citations"]
|
||||
|
||||
# Example: Add a new key-value pair or modify existing ones
|
||||
# data["modified"] = True # Example modification
|
||||
if "docs" in data:
|
||||
data = {**data}
|
||||
data["messages"] = rag_messages(
|
||||
data["messages"], citations = rag_messages(
|
||||
docs=data["docs"],
|
||||
messages=data["messages"],
|
||||
template=rag_app.state.RAG_TEMPLATE,
|
||||
template=rag_app.state.config.RAG_TEMPLATE,
|
||||
embedding_function=rag_app.state.EMBEDDING_FUNCTION,
|
||||
k=rag_app.state.TOP_K,
|
||||
k=rag_app.state.config.TOP_K,
|
||||
reranking_function=rag_app.state.sentence_transformer_rf,
|
||||
r=rag_app.state.RELEVANCE_THRESHOLD,
|
||||
hybrid_search=rag_app.state.ENABLE_RAG_HYBRID_SEARCH,
|
||||
r=rag_app.state.config.RELEVANCE_THRESHOLD,
|
||||
hybrid_search=rag_app.state.config.ENABLE_RAG_HYBRID_SEARCH,
|
||||
)
|
||||
del data["docs"]
|
||||
|
||||
log.debug(f"data['messages']: {data['messages']}")
|
||||
log.debug(
|
||||
f"data['messages']: {data['messages']}, citations: {citations}"
|
||||
)
|
||||
|
||||
modified_body_bytes = json.dumps(data).encode("utf-8")
|
||||
|
||||
@@ -146,11 +185,36 @@ class RAGMiddleware(BaseHTTPMiddleware):
|
||||
]
|
||||
|
||||
response = await call_next(request)
|
||||
|
||||
if return_citations:
|
||||
# Inject the citations into the response
|
||||
if isinstance(response, StreamingResponse):
|
||||
# If it's a streaming response, inject it as SSE event or NDJSON line
|
||||
content_type = response.headers.get("Content-Type")
|
||||
if "text/event-stream" in content_type:
|
||||
return StreamingResponse(
|
||||
self.openai_stream_wrapper(response.body_iterator, citations),
|
||||
)
|
||||
if "application/x-ndjson" in content_type:
|
||||
return StreamingResponse(
|
||||
self.ollama_stream_wrapper(response.body_iterator, citations),
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
async def _receive(self, body: bytes):
|
||||
return {"type": "http.request", "body": body, "more_body": False}
|
||||
|
||||
async def openai_stream_wrapper(self, original_generator, citations):
|
||||
yield f"data: {json.dumps({'citations': citations})}\n\n"
|
||||
async for data in original_generator:
|
||||
yield data
|
||||
|
||||
async def ollama_stream_wrapper(self, original_generator, citations):
|
||||
yield f"{json.dumps({'citations': citations})}\n"
|
||||
async for data in original_generator:
|
||||
yield data
|
||||
|
||||
|
||||
app.add_middleware(RAGMiddleware)
|
||||
|
||||
@@ -174,15 +238,15 @@ async def check_url(request: Request, call_next):
|
||||
return response
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def on_startup():
|
||||
if ENABLE_LITELLM:
|
||||
asyncio.create_task(start_litellm_background())
|
||||
@app.middleware("http")
|
||||
async def update_embedding_function(request: Request, call_next):
|
||||
response = await call_next(request)
|
||||
if "/embedding/update" in request.url.path:
|
||||
webui_app.state.EMBEDDING_FUNCTION = rag_app.state.EMBEDDING_FUNCTION
|
||||
return response
|
||||
|
||||
|
||||
app.mount("/api/v1", webui_app)
|
||||
app.mount("/litellm/api", litellm_app)
|
||||
|
||||
app.mount("/ollama", ollama_app)
|
||||
app.mount("/openai/api", openai_app)
|
||||
|
||||
@@ -190,6 +254,10 @@ app.mount("/images/api/v1", images_app)
|
||||
app.mount("/audio/api/v1", audio_app)
|
||||
app.mount("/rag/api/v1", rag_app)
|
||||
|
||||
app.mount("/api/v1", webui_app)
|
||||
|
||||
webui_app.state.EMBEDDING_FUNCTION = rag_app.state.EMBEDDING_FUNCTION
|
||||
|
||||
|
||||
@app.get("/api/config")
|
||||
async def get_app_config():
|
||||
@@ -204,10 +272,11 @@ async def get_app_config():
|
||||
"status": True,
|
||||
"name": WEBUI_NAME,
|
||||
"version": VERSION,
|
||||
"auth": WEBUI_AUTH,
|
||||
"default_locale": default_locale,
|
||||
"images": images_app.state.ENABLED,
|
||||
"default_models": webui_app.state.DEFAULT_MODELS,
|
||||
"default_prompt_suggestions": webui_app.state.DEFAULT_PROMPT_SUGGESTIONS,
|
||||
"images": images_app.state.config.ENABLED,
|
||||
"default_models": webui_app.state.config.DEFAULT_MODELS,
|
||||
"default_prompt_suggestions": webui_app.state.config.DEFAULT_PROMPT_SUGGESTIONS,
|
||||
"trusted_header_auth": bool(webui_app.state.AUTH_TRUSTED_EMAIL_HEADER),
|
||||
"admin_export_enabled": ENABLE_ADMIN_EXPORT,
|
||||
}
|
||||
@@ -216,8 +285,8 @@ async def get_app_config():
|
||||
@app.get("/api/config/model/filter")
|
||||
async def get_model_filter_config(user=Depends(get_admin_user)):
|
||||
return {
|
||||
"enabled": app.state.ENABLE_MODEL_FILTER,
|
||||
"models": app.state.MODEL_FILTER_LIST,
|
||||
"enabled": app.state.config.ENABLE_MODEL_FILTER,
|
||||
"models": app.state.config.MODEL_FILTER_LIST,
|
||||
}
|
||||
|
||||
|
||||
@@ -230,28 +299,28 @@ class ModelFilterConfigForm(BaseModel):
|
||||
async def update_model_filter_config(
|
||||
form_data: ModelFilterConfigForm, user=Depends(get_admin_user)
|
||||
):
|
||||
app.state.ENABLE_MODEL_FILTER = form_data.enabled
|
||||
app.state.MODEL_FILTER_LIST = form_data.models
|
||||
app.state.config.ENABLE_MODEL_FILTER = form_data.enabled
|
||||
app.state.config.MODEL_FILTER_LIST = form_data.models
|
||||
|
||||
ollama_app.state.ENABLE_MODEL_FILTER = app.state.ENABLE_MODEL_FILTER
|
||||
ollama_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
|
||||
ollama_app.state.config.ENABLE_MODEL_FILTER = app.state.config.ENABLE_MODEL_FILTER
|
||||
ollama_app.state.config.MODEL_FILTER_LIST = app.state.config.MODEL_FILTER_LIST
|
||||
|
||||
openai_app.state.ENABLE_MODEL_FILTER = app.state.ENABLE_MODEL_FILTER
|
||||
openai_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
|
||||
openai_app.state.config.ENABLE_MODEL_FILTER = app.state.config.ENABLE_MODEL_FILTER
|
||||
openai_app.state.config.MODEL_FILTER_LIST = app.state.config.MODEL_FILTER_LIST
|
||||
|
||||
litellm_app.state.ENABLE_MODEL_FILTER = app.state.ENABLE_MODEL_FILTER
|
||||
litellm_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
|
||||
litellm_app.state.ENABLE_MODEL_FILTER = app.state.config.ENABLE_MODEL_FILTER
|
||||
litellm_app.state.MODEL_FILTER_LIST = app.state.config.MODEL_FILTER_LIST
|
||||
|
||||
return {
|
||||
"enabled": app.state.ENABLE_MODEL_FILTER,
|
||||
"models": app.state.MODEL_FILTER_LIST,
|
||||
"enabled": app.state.config.ENABLE_MODEL_FILTER,
|
||||
"models": app.state.config.MODEL_FILTER_LIST,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/webhook")
|
||||
async def get_webhook_url(user=Depends(get_admin_user)):
|
||||
return {
|
||||
"url": app.state.WEBHOOK_URL,
|
||||
"url": app.state.config.WEBHOOK_URL,
|
||||
}
|
||||
|
||||
|
||||
@@ -261,12 +330,12 @@ class UrlForm(BaseModel):
|
||||
|
||||
@app.post("/api/webhook")
|
||||
async def update_webhook_url(form_data: UrlForm, user=Depends(get_admin_user)):
|
||||
app.state.WEBHOOK_URL = form_data.url
|
||||
app.state.config.WEBHOOK_URL = form_data.url
|
||||
|
||||
webui_app.state.WEBHOOK_URL = app.state.WEBHOOK_URL
|
||||
webui_app.state.WEBHOOK_URL = app.state.config.WEBHOOK_URL
|
||||
|
||||
return {
|
||||
"url": app.state.WEBHOOK_URL,
|
||||
"url": app.state.config.WEBHOOK_URL,
|
||||
}
|
||||
|
||||
|
||||
@@ -311,21 +380,40 @@ async def get_manifest_json():
|
||||
"background_color": "#343541",
|
||||
"theme_color": "#343541",
|
||||
"orientation": "portrait-primary",
|
||||
"icons": [{"src": "/favicon.png", "type": "image/png", "sizes": "844x884"}],
|
||||
"icons": [{"src": "/static/logo.png", "type": "image/png", "sizes": "500x500"}],
|
||||
}
|
||||
|
||||
|
||||
@app.get("/opensearch.xml")
|
||||
async def get_opensearch_xml():
|
||||
xml_content = rf"""
|
||||
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">
|
||||
<ShortName>{WEBUI_NAME}</ShortName>
|
||||
<Description>Search {WEBUI_NAME}</Description>
|
||||
<InputEncoding>UTF-8</InputEncoding>
|
||||
<Image width="16" height="16" type="image/x-icon">{WEBUI_URL}/favicon.png</Image>
|
||||
<Url type="text/html" method="get" template="{WEBUI_URL}/?q={"{searchTerms}"}"/>
|
||||
<moz:SearchForm>{WEBUI_URL}</moz:SearchForm>
|
||||
</OpenSearchDescription>
|
||||
"""
|
||||
return Response(content=xml_content, media_type="application/xml")
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def healthcheck():
|
||||
return {"status": True}
|
||||
|
||||
|
||||
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
||||
app.mount("/cache", StaticFiles(directory=CACHE_DIR), name="cache")
|
||||
|
||||
app.mount(
|
||||
"/",
|
||||
SPAStaticFiles(directory=FRONTEND_BUILD_DIR, html=True),
|
||||
name="spa-static-files",
|
||||
)
|
||||
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown_event():
|
||||
if ENABLE_LITELLM:
|
||||
await shutdown_litellm_background()
|
||||
if os.path.exists(FRONTEND_BUILD_DIR):
|
||||
app.mount(
|
||||
"/",
|
||||
SPAStaticFiles(directory=FRONTEND_BUILD_DIR, html=True),
|
||||
name="spa-static-files",
|
||||
)
|
||||
else:
|
||||
log.warning(
|
||||
f"Frontend build directory not found at '{FRONTEND_BUILD_DIR}'. Serving API only."
|
||||
)
|
||||
|
||||
+48
-47
@@ -1,59 +1,60 @@
|
||||
fastapi
|
||||
uvicorn[standard]
|
||||
pydantic
|
||||
python-multipart
|
||||
fastapi==0.109.2
|
||||
uvicorn[standard]==0.22.0
|
||||
pydantic==2.7.1
|
||||
python-multipart==0.0.9
|
||||
|
||||
flask
|
||||
flask_cors
|
||||
Flask==3.0.3
|
||||
Flask-Cors==4.0.0
|
||||
|
||||
python-socketio
|
||||
python-jose
|
||||
passlib[bcrypt]
|
||||
uuid
|
||||
python-socketio==5.11.2
|
||||
python-jose==3.3.0
|
||||
passlib[bcrypt]==1.7.4
|
||||
|
||||
requests
|
||||
aiohttp
|
||||
peewee
|
||||
peewee-migrate
|
||||
psycopg2-binary
|
||||
pymysql
|
||||
bcrypt
|
||||
requests==2.31.0
|
||||
aiohttp==3.9.5
|
||||
peewee==3.17.3
|
||||
peewee-migrate==1.12.2
|
||||
psycopg2-binary==2.9.9
|
||||
PyMySQL==1.1.0
|
||||
bcrypt==4.1.2
|
||||
|
||||
litellm==1.35.17
|
||||
litellm[proxy]==1.35.17
|
||||
litellm[proxy]==1.35.28
|
||||
|
||||
boto3
|
||||
boto3==1.34.95
|
||||
|
||||
argon2-cffi
|
||||
apscheduler
|
||||
google-generativeai
|
||||
argon2-cffi==23.1.0
|
||||
APScheduler==3.10.4
|
||||
google-generativeai==0.5.2
|
||||
|
||||
langchain
|
||||
langchain-chroma
|
||||
langchain-community
|
||||
fake_useragent
|
||||
chromadb
|
||||
sentence_transformers
|
||||
pypdf
|
||||
docx2txt
|
||||
unstructured
|
||||
markdown
|
||||
pypandoc
|
||||
pandas
|
||||
openpyxl
|
||||
pyxlsb
|
||||
xlrd
|
||||
langchain==0.1.16
|
||||
langchain-community==0.0.34
|
||||
langchain-chroma==0.1.0
|
||||
|
||||
opencv-python-headless
|
||||
rapidocr-onnxruntime
|
||||
fake-useragent==1.5.1
|
||||
chromadb==0.4.24
|
||||
sentence-transformers==2.7.0
|
||||
pypdf==4.2.0
|
||||
docx2txt==0.8
|
||||
unstructured==0.11.8
|
||||
Markdown==3.6
|
||||
pypandoc==1.13
|
||||
pandas==2.2.2
|
||||
openpyxl==3.1.2
|
||||
pyxlsb==1.0.10
|
||||
xlrd==2.0.1
|
||||
validators==0.28.1
|
||||
|
||||
fpdf2
|
||||
rank_bm25
|
||||
opencv-python-headless==4.9.0.80
|
||||
rapidocr-onnxruntime==1.2.3
|
||||
|
||||
faster-whisper
|
||||
fpdf2==2.7.8
|
||||
rank-bm25==0.2.2
|
||||
|
||||
PyJWT
|
||||
pyjwt[crypto]
|
||||
faster-whisper==1.0.1
|
||||
|
||||
black
|
||||
langfuse
|
||||
PyJWT[crypto]==2.8.0
|
||||
|
||||
black==24.4.2
|
||||
langfuse==2.27.3
|
||||
youtube-transcript-api==0.6.2
|
||||
pytube
|
||||
+1
-1
@@ -8,7 +8,7 @@ KEY_FILE=.webui_secret_key
|
||||
PORT="${PORT:-8080}"
|
||||
HOST="${HOST:-0.0.0.0}"
|
||||
if test "$WEBUI_SECRET_KEY $WEBUI_JWT_SECRET_KEY" = " "; then
|
||||
echo "No WEBUI_SECRET_KEY provided"
|
||||
echo "Loading WEBUI_SECRET_KEY from file, not provided as an environment variable."
|
||||
|
||||
if ! [ -e "$KEY_FILE" ]; then
|
||||
echo "Generating WEBUI_SECRET_KEY"
|
||||
|
||||
@@ -13,7 +13,7 @@ SET "WEBUI_JWT_SECRET_KEY=%WEBUI_JWT_SECRET_KEY%"
|
||||
|
||||
:: Check if WEBUI_SECRET_KEY and WEBUI_JWT_SECRET_KEY are not set
|
||||
IF "%WEBUI_SECRET_KEY%%WEBUI_JWT_SECRET_KEY%" == " " (
|
||||
echo No WEBUI_SECRET_KEY provided
|
||||
echo Loading WEBUI_SECRET_KEY from file, not provided as an environment variable.
|
||||
|
||||
IF NOT EXIST "%KEY_FILE%" (
|
||||
echo Generating WEBUI_SECRET_KEY
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
@@ -0,0 +1 @@
|
||||
Name,Email,Password,Role
|
||||
|
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
@@ -38,9 +38,10 @@ def calculate_sha256_string(string):
|
||||
|
||||
|
||||
def validate_email_format(email: str) -> bool:
|
||||
if not re.match(r"[^@]+@[^@]+\.[^@]+", email):
|
||||
return False
|
||||
return True
|
||||
if email.endswith("@localhost"):
|
||||
return True
|
||||
|
||||
return bool(re.match(r"[^@]+@[^@]+\.[^@]+", email))
|
||||
|
||||
|
||||
def sanitize_filename(file_name):
|
||||
|
||||
+34
-2
@@ -21,14 +21,14 @@ describe('Settings', () => {
|
||||
// Click on the model selector
|
||||
cy.get('button[aria-label="Select a model"]').click();
|
||||
// Select the first model
|
||||
cy.get('div[role="option"][data-value]').first().click();
|
||||
cy.get('button[aria-label="model-item"]').first().click();
|
||||
});
|
||||
|
||||
it('user can perform text chat', () => {
|
||||
// Click on the model selector
|
||||
cy.get('button[aria-label="Select a model"]').click();
|
||||
// Select the first model
|
||||
cy.get('div[role="option"][data-value]').first().click();
|
||||
cy.get('button[aria-label="model-item"]').first().click();
|
||||
// Type a message
|
||||
cy.get('#chat-textarea').type('Hi, what can you do? A single sentence only please.', {
|
||||
force: true
|
||||
@@ -42,5 +42,37 @@ describe('Settings', () => {
|
||||
.find('div[aria-label="Generation Info"]', { timeout: 120_000 }) // Generation Info is created after the stop token is received
|
||||
.should('exist');
|
||||
});
|
||||
|
||||
it('user can share chat', () => {
|
||||
// Click on the model selector
|
||||
cy.get('button[aria-label="Select a model"]').click();
|
||||
// Select the first model
|
||||
cy.get('button[aria-label="model-item"]').first().click();
|
||||
// Type a message
|
||||
cy.get('#chat-textarea').type('Hi, what can you do? A single sentence only please.', {
|
||||
force: true
|
||||
});
|
||||
// Send the message
|
||||
cy.get('button[type="submit"]').click();
|
||||
// User's message should be visible
|
||||
cy.get('.chat-user').should('exist');
|
||||
// Wait for the response
|
||||
cy.get('.chat-assistant', { timeout: 120_000 }) // .chat-assistant is created after the first token is received
|
||||
.find('div[aria-label="Generation Info"]', { timeout: 120_000 }) // Generation Info is created after the stop token is received
|
||||
.should('exist');
|
||||
// spy on requests
|
||||
const spy = cy.spy();
|
||||
cy.intercept('GET', '/api/v1/chats/*', spy);
|
||||
// Open context menu
|
||||
cy.get('#chat-context-menu-button').click();
|
||||
// Click share button
|
||||
cy.get('#chat-share-button').click();
|
||||
// Check if the share dialog is visible
|
||||
cy.get('#copy-and-share-chat-button').should('exist');
|
||||
cy.wrap({}, { timeout: 5000 }).should(() => {
|
||||
// Check if the request was made twice (once for to replace chat object and once more due to change event)
|
||||
expect(spy).to.be.callCount(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,12 +15,8 @@ describe('Settings', () => {
|
||||
cy.loginAdmin();
|
||||
// Visit the home page
|
||||
cy.visit('/');
|
||||
// Open the sidebar if it is not already open
|
||||
cy.get('[aria-label="Open sidebar"]').then(() => {
|
||||
cy.get('button[id="sidebar-toggle-button"]').click();
|
||||
});
|
||||
// Click on the profile link
|
||||
cy.get('button').contains(adminUser.name).click();
|
||||
// Click on the user menu
|
||||
cy.get('button[aria-label="User Menu"]').click();
|
||||
// Click on the settings link
|
||||
cy.get('button').contains('Settings').click();
|
||||
});
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
ollama:
|
||||
# Expose Ollama API outside the container stack
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
ollama:
|
||||
volumes:
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
ollama:
|
||||
# GPU support
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
ollama:
|
||||
volumes:
|
||||
|
||||
@@ -17,7 +17,7 @@ If your issue or contribution pertains directly to the core Ollama technology, p
|
||||
|
||||
### 🚨 Reporting Issues
|
||||
|
||||
Noticed something off? Have an idea? Check our [Issues tab](https://github.com/open-webui/oopen-webui/issues) to see if it's already been reported or suggested. If not, feel free to open a new issue. When reporting an issue, please follow our issue templates. These templates are designed to ensure that all necessary details are provided from the start, enabling us to address your concerns more efficiently.
|
||||
Noticed something off? Have an idea? Check our [Issues tab](https://github.com/open-webui/open-webui/issues) to see if it's already been reported or suggested. If not, feel free to open a new issue. When reporting an issue, please follow our issue templates. These templates are designed to ensure that all necessary details are provided from the start, enabling us to address your concerns more efficiently.
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
@@ -54,7 +54,7 @@ Help us make Open WebUI more accessible by improving documentation, writing tuto
|
||||
|
||||
Help us make Open WebUI available to a wider audience. In this section, we'll guide you through the process of adding new translations to the project.
|
||||
|
||||
We use JSON files to store translations. You can find the existing translation files in the `src/lib/i18n/locales` directory. Each directory corresponds to a specific language, for example, `en-US` for English (US), `fr-FR` for French (France) and so on. You can refer to [ISO 639 Language Codes][http://www.lingoes.net/en/translator/langcode.htm] to find the appropriate code for a specific language.
|
||||
We use JSON files to store translations. You can find the existing translation files in the `src/lib/i18n/locales` directory. Each directory corresponds to a specific language, for example, `en-US` for English (US), `fr-FR` for French (France) and so on. You can refer to [ISO 639 Language Codes](http://www.lingoes.net/en/translator/langcode.htm) to find the appropriate code for a specific language.
|
||||
|
||||
To add a new language:
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
values-minikube.yaml
|
||||
@@ -1,21 +0,0 @@
|
||||
apiVersion: v2
|
||||
name: open-webui
|
||||
version: 1.0.0
|
||||
appVersion: "latest"
|
||||
|
||||
home: https://www.openwebui.com/
|
||||
icon: https://raw.githubusercontent.com/open-webui/open-webui/main/static/favicon.png
|
||||
|
||||
description: "Open WebUI: A User-Friendly Web Interface for Chat Interactions 👋"
|
||||
keywords:
|
||||
- llm
|
||||
- chat
|
||||
- web-ui
|
||||
|
||||
sources:
|
||||
- https://github.com/open-webui/open-webui/tree/main/kubernetes/helm
|
||||
- https://hub.docker.com/r/ollama/ollama
|
||||
- https://github.com/open-webui/open-webui/pkgs/container/open-webui
|
||||
|
||||
annotations:
|
||||
licenses: MIT
|
||||
@@ -0,0 +1,4 @@
|
||||
# Helm Charts
|
||||
Open WebUI Helm Charts are now hosted in a separate repo, which can be found here: https://github.com/open-webui/helm-charts
|
||||
|
||||
The charts are released at https://helm.openwebui.com.
|
||||
@@ -1,51 +0,0 @@
|
||||
{{- define "open-webui.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "ollama.name" -}}
|
||||
ollama
|
||||
{{- end -}}
|
||||
|
||||
{{- define "ollama.url" -}}
|
||||
{{- if .Values.ollama.externalHost }}
|
||||
{{- printf .Values.ollama.externalHost }}
|
||||
{{- else }}
|
||||
{{- printf "http://%s.%s.svc.cluster.local:%d" (include "ollama.name" .) (.Release.Namespace) (.Values.ollama.service.port | int) }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "chart.name" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "base.labels" -}}
|
||||
helm.sh/chart: {{ include "chart.name" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "base.selectorLabels" -}}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "open-webui.selectorLabels" -}}
|
||||
{{ include "base.selectorLabels" . }}
|
||||
app.kubernetes.io/component: {{ .Chart.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "open-webui.labels" -}}
|
||||
{{ include "base.labels" . }}
|
||||
{{ include "open-webui.selectorLabels" . }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "ollama.selectorLabels" -}}
|
||||
{{ include "base.selectorLabels" . }}
|
||||
app.kubernetes.io/component: {{ include "ollama.name" . }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "ollama.labels" -}}
|
||||
{{ include "base.labels" . }}
|
||||
{{ include "ollama.selectorLabels" . }}
|
||||
{{- end }}
|
||||
@@ -1,23 +0,0 @@
|
||||
{{- if not .Values.ollama.externalHost }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "ollama.name" . }}
|
||||
labels:
|
||||
{{- include "ollama.labels" . | nindent 4 }}
|
||||
{{- with .Values.ollama.service.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
selector:
|
||||
{{- include "ollama.selectorLabels" . | nindent 4 }}
|
||||
{{- with .Values.ollama.service }}
|
||||
type: {{ .type }}
|
||||
ports:
|
||||
- protocol: TCP
|
||||
name: http
|
||||
port: {{ .port }}
|
||||
targetPort: http
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,98 +0,0 @@
|
||||
{{- if not .Values.ollama.externalHost }}
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: {{ include "ollama.name" . }}
|
||||
labels:
|
||||
{{- include "ollama.labels" . | nindent 4 }}
|
||||
{{- with .Values.ollama.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
serviceName: {{ include "ollama.name" . }}
|
||||
replicas: {{ .Values.ollama.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "ollama.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "ollama.labels" . | nindent 8 }}
|
||||
{{- with .Values.ollama.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
enableServiceLinks: false
|
||||
automountServiceAccountToken: false
|
||||
{{- with .Values.ollama.runtimeClassName }}
|
||||
runtimeClassName: {{ . }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: {{ include "ollama.name" . }}
|
||||
{{- with .Values.ollama.image }}
|
||||
image: {{ .repository }}:{{ .tag }}
|
||||
imagePullPolicy: {{ .pullPolicy }}
|
||||
{{- end }}
|
||||
tty: true
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.ollama.service.containerPort }}
|
||||
env:
|
||||
{{- if .Values.ollama.gpu.enabled }}
|
||||
- name: PATH
|
||||
value: /usr/local/nvidia/bin:/usr/local/cuda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
- name: LD_LIBRARY_PATH
|
||||
value: /usr/local/nvidia/lib:/usr/local/nvidia/lib64
|
||||
- name: NVIDIA_DRIVER_CAPABILITIES
|
||||
value: compute,utility
|
||||
{{- end }}
|
||||
{{- with .Values.ollama.resources }}
|
||||
resources: {{- toYaml . | nindent 10 }}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /root/.ollama
|
||||
{{- with .Values.ollama.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.ollama.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
{{- if and .Values.ollama.persistence.enabled .Values.ollama.persistence.existingClaim }}
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ .Values.ollama.persistence.existingClaim }}
|
||||
{{- else if not .Values.ollama.persistence.enabled }}
|
||||
- name: data
|
||||
emptyDir: {}
|
||||
{{- else if and .Values.ollama.persistence.enabled (not .Values.ollama.persistence.existingClaim) }}
|
||||
[]
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: data
|
||||
labels:
|
||||
{{- include "ollama.selectorLabels" . | nindent 8 }}
|
||||
{{- with .Values.ollama.persistence.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
accessModes:
|
||||
{{- range .Values.ollama.persistence.accessModes }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.ollama.persistence.size | quote }}
|
||||
storageClassName: {{ .Values.ollama.persistence.storageClass }}
|
||||
{{- with .Values.ollama.persistence.selector }}
|
||||
selector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,62 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "open-webui.name" . }}
|
||||
labels:
|
||||
{{- include "open-webui.labels" . | nindent 4 }}
|
||||
{{- with .Values.webui.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
replicas: {{ .Values.webui.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "open-webui.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "open-webui.labels" . | nindent 8 }}
|
||||
{{- with .Values.webui.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
enableServiceLinks: false
|
||||
automountServiceAccountToken: false
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
{{- with .Values.webui.image }}
|
||||
image: {{ .repository }}:{{ .tag | default $.Chart.AppVersion }}
|
||||
imagePullPolicy: {{ .pullPolicy }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.webui.service.containerPort }}
|
||||
{{- with .Values.webui.resources }}
|
||||
resources: {{- toYaml . | nindent 10 }}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /app/backend/data
|
||||
env:
|
||||
- name: OLLAMA_BASE_URL
|
||||
value: {{ include "ollama.url" . | quote }}
|
||||
tty: true
|
||||
{{- with .Values.webui.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
{{- if and .Values.webui.persistence.enabled .Values.webui.persistence.existingClaim }}
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ .Values.webui.persistence.existingClaim }}
|
||||
{{- else if not .Values.webui.persistence.enabled }}
|
||||
- name: data
|
||||
emptyDir: {}
|
||||
{{- else if and .Values.webui.persistence.enabled (not .Values.webui.persistence.existingClaim) }}
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ include "open-webui.name" . }}
|
||||
{{- end }}
|
||||
@@ -1,33 +0,0 @@
|
||||
{{- if .Values.webui.ingress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "open-webui.name" . }}
|
||||
labels:
|
||||
{{- include "open-webui.labels" . | nindent 4 }}
|
||||
{{- with .Values.webui.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.webui.ingress.class }}
|
||||
ingressClassName: {{ . }}
|
||||
{{- end }}
|
||||
{{- if .Values.webui.ingress.tls }}
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ .Values.webui.ingress.host | quote }}
|
||||
secretName: {{ default (printf "%s-tls" .Release.Name) .Values.webui.ingress.existingSecret }}
|
||||
{{- end }}
|
||||
rules:
|
||||
- host: {{ .Values.webui.ingress.host }}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "open-webui.name" . }}
|
||||
port:
|
||||
name: http
|
||||
{{- end }}
|
||||
@@ -1,27 +0,0 @@
|
||||
{{- if and .Values.webui.persistence.enabled (not .Values.webui.persistence.existingClaim) }}
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ include "open-webui.name" . }}
|
||||
labels:
|
||||
{{- include "open-webui.selectorLabels" . | nindent 4 }}
|
||||
{{- with .Values.webui.persistence.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
accessModes:
|
||||
{{- range .Values.webui.persistence.accessModes }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.webui.persistence.size }}
|
||||
{{- if .Values.webui.persistence.storageClass }}
|
||||
storageClassName: {{ .Values.webui.persistence.storageClass }}
|
||||
{{- end }}
|
||||
{{- with .Values.webui.persistence.selector }}
|
||||
selector:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,29 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "open-webui.name" . }}
|
||||
labels:
|
||||
{{- include "open-webui.labels" . | nindent 4 }}
|
||||
{{- with .Values.webui.service.labels }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .Values.webui.service.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
selector:
|
||||
{{- include "open-webui.selectorLabels" . | nindent 4 }}
|
||||
type: {{ .Values.webui.service.type | default "ClusterIP" }}
|
||||
ports:
|
||||
- protocol: TCP
|
||||
name: http
|
||||
port: {{ .Values.webui.service.port }}
|
||||
targetPort: http
|
||||
{{- if .Values.webui.service.nodePort }}
|
||||
nodePort: {{ .Values.webui.service.nodePort | int }}
|
||||
{{- end }}
|
||||
{{- if .Values.webui.service.loadBalancerClass }}
|
||||
loadBalancerClass: {{ .Values.webui.service.loadBalancerClass | quote }}
|
||||
{{- end }}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
ollama:
|
||||
resources:
|
||||
requests:
|
||||
cpu: "2000m"
|
||||
memory: "2Gi"
|
||||
limits:
|
||||
cpu: "4000m"
|
||||
memory: "4Gi"
|
||||
nvidia.com/gpu: "0"
|
||||
service:
|
||||
type: ClusterIP
|
||||
gpu:
|
||||
enabled: false
|
||||
|
||||
webui:
|
||||
resources:
|
||||
requests:
|
||||
cpu: "500m"
|
||||
memory: "500Mi"
|
||||
limits:
|
||||
cpu: "1000m"
|
||||
memory: "1Gi"
|
||||
ingress:
|
||||
enabled: true
|
||||
host: open-webui.minikube.local
|
||||
service:
|
||||
type: NodePort
|
||||
@@ -1,75 +0,0 @@
|
||||
nameOverride: ""
|
||||
|
||||
ollama:
|
||||
externalHost: ""
|
||||
annotations: {}
|
||||
podAnnotations: {}
|
||||
replicaCount: 1
|
||||
image:
|
||||
repository: ollama/ollama
|
||||
tag: latest
|
||||
pullPolicy: Always
|
||||
resources: {}
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 30Gi
|
||||
existingClaim: ""
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClass: ""
|
||||
selector: {}
|
||||
annotations: {}
|
||||
nodeSelector: {}
|
||||
# -- If using a special runtime container such as nvidia, set it here.
|
||||
runtimeClassName: ""
|
||||
tolerations:
|
||||
- key: nvidia.com/gpu
|
||||
operator: Exists
|
||||
effect: NoSchedule
|
||||
service:
|
||||
type: ClusterIP
|
||||
annotations: {}
|
||||
port: 80
|
||||
containerPort: 11434
|
||||
gpu:
|
||||
# -- Enable additional ENV values to help Ollama discover GPU usage
|
||||
enabled: false
|
||||
|
||||
webui:
|
||||
annotations: {}
|
||||
podAnnotations: {}
|
||||
replicaCount: 1
|
||||
image:
|
||||
repository: ghcr.io/open-webui/open-webui
|
||||
tag: ""
|
||||
pullPolicy: Always
|
||||
resources: {}
|
||||
ingress:
|
||||
enabled: false
|
||||
class: ""
|
||||
# -- Use appropriate annotations for your Ingress controller, e.g., for NGINX:
|
||||
# nginx.ingress.kubernetes.io/rewrite-target: /
|
||||
annotations: {}
|
||||
host: ""
|
||||
tls: false
|
||||
existingSecret: ""
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 2Gi
|
||||
existingClaim: ""
|
||||
# -- If using multiple replicas, you must update accessModes to ReadWriteMany
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClass: ""
|
||||
selector: {}
|
||||
annotations: {}
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
service:
|
||||
type: ClusterIP
|
||||
annotations: {}
|
||||
port: 80
|
||||
containerPort: 8080
|
||||
nodePort: ""
|
||||
labels: {}
|
||||
loadBalancerClass: ""
|
||||
Generated
+1155
-5
File diff suppressed because it is too large
Load Diff
+13
-7
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "open-webui",
|
||||
"version": "0.1.122",
|
||||
"version": "0.1.125",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev --host",
|
||||
"build": "vite build",
|
||||
"dev": "npm run pyodide:fetch && vite dev --host",
|
||||
"build": "npm run pyodide:fetch && vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
@@ -12,10 +12,12 @@
|
||||
"lint:frontend": "eslint . --fix",
|
||||
"lint:types": "npm run check",
|
||||
"lint:backend": "pylint backend/",
|
||||
"format": "prettier --plugin-search-dir --write '**/*.{js,ts,svelte,css,md,html,json}'",
|
||||
"format": "prettier --plugin-search-dir --write \"**/*.{js,ts,svelte,css,md,html,json}\"",
|
||||
"format:backend": "black . --exclude \"/venv/\"",
|
||||
"i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write 'src/lib/i18n/**/*.{js,json}'",
|
||||
"cy:open": "cypress open"
|
||||
"i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write \"src/lib/i18n/**/*.{js,json}\"",
|
||||
"cy:open": "cypress open",
|
||||
"test:frontend": "vitest",
|
||||
"pyodide:fetch": "node scripts/prepare-pyodide.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
@@ -41,14 +43,17 @@
|
||||
"tailwindcss": "^3.3.3",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.4.2"
|
||||
"vite": "^4.4.2",
|
||||
"vitest": "^1.6.0"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@pyscript/core": "^0.4.32",
|
||||
"@sveltejs/adapter-node": "^1.3.1",
|
||||
"async": "^3.2.5",
|
||||
"bits-ui": "^0.19.7",
|
||||
"dayjs": "^1.11.10",
|
||||
"eventsource-parser": "^1.1.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"highlight.js": "^11.9.0",
|
||||
"i18next": "^23.10.0",
|
||||
@@ -58,6 +63,7 @@
|
||||
"js-sha256": "^0.10.1",
|
||||
"katex": "^0.16.9",
|
||||
"marked": "^9.1.0",
|
||||
"pyodide": "^0.26.0-alpha.4",
|
||||
"svelte-sonner": "^0.3.19",
|
||||
"tippy.js": "^6.3.7",
|
||||
"uuid": "^9.0.1"
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
const packages = [
|
||||
'requests',
|
||||
'beautifulsoup4',
|
||||
'numpy',
|
||||
'pandas',
|
||||
'matplotlib',
|
||||
'scikit-learn',
|
||||
'scipy',
|
||||
'regex',
|
||||
'seaborn'
|
||||
];
|
||||
|
||||
import { loadPyodide } from 'pyodide';
|
||||
import { writeFile, copyFile, readdir } from 'fs/promises';
|
||||
|
||||
async function downloadPackages() {
|
||||
console.log('Setting up pyodide + micropip');
|
||||
const pyodide = await loadPyodide({
|
||||
packageCacheDir: 'static/pyodide'
|
||||
});
|
||||
await pyodide.loadPackage('micropip');
|
||||
const micropip = pyodide.pyimport('micropip');
|
||||
console.log('Downloading Pyodide packages:', packages);
|
||||
await micropip.install(packages);
|
||||
console.log('Pyodide packages downloaded, freezing into lock file');
|
||||
const lockFile = await micropip.freeze();
|
||||
await writeFile('static/pyodide/pyodide-lock.json', lockFile);
|
||||
}
|
||||
|
||||
async function copyPyodide() {
|
||||
console.log('Copying Pyodide files into static directory');
|
||||
// Copy all files from node_modules/pyodide to static/pyodide
|
||||
for await (const entry of await readdir('node_modules/pyodide')) {
|
||||
await copyFile(`node_modules/pyodide/${entry}`, `static/pyodide/${entry}`);
|
||||
}
|
||||
}
|
||||
|
||||
await downloadPackages();
|
||||
await copyPyodide();
|
||||
+29
@@ -82,3 +82,32 @@ select {
|
||||
.katex-mathml {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.scrollbar-hidden:active::-webkit-scrollbar-thumb,
|
||||
.scrollbar-hidden:focus::-webkit-scrollbar-thumb,
|
||||
.scrollbar-hidden:hover::-webkit-scrollbar-thumb {
|
||||
visibility: visible;
|
||||
}
|
||||
.scrollbar-hidden::-webkit-scrollbar-thumb {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.scrollbar-none::-webkit-scrollbar {
|
||||
display: none; /* for Chrome, Safari and Opera */
|
||||
}
|
||||
|
||||
.scrollbar-none {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
/* display: none; <- Crashes Chrome on hover */
|
||||
-webkit-appearance: none;
|
||||
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
|
||||
}
|
||||
|
||||
input[type='number'] {
|
||||
-moz-appearance: textfield; /* Firefox */
|
||||
}
|
||||
|
||||
@@ -6,6 +6,13 @@
|
||||
<link rel="manifest" href="%sveltekit.assets%/manifest.json" crossorigin="use-credentials" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<meta name="robots" content="noindex,nofollow" />
|
||||
<link
|
||||
rel="search"
|
||||
type="application/opensearchdescription+xml"
|
||||
title="Open WebUI"
|
||||
href="/opensearch.xml"
|
||||
/>
|
||||
|
||||
<script>
|
||||
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
|
||||
(() => {
|
||||
|
||||
@@ -30,6 +30,8 @@ export const getAudioConfig = async (token: string) => {
|
||||
type OpenAIConfigForm = {
|
||||
url: string;
|
||||
key: string;
|
||||
model: string;
|
||||
speaker: string;
|
||||
};
|
||||
|
||||
export const updateAudioConfig = async (token: string, payload: OpenAIConfigForm) => {
|
||||
@@ -95,7 +97,8 @@ export const transcribeAudio = async (token: string, file: File) => {
|
||||
export const synthesizeOpenAISpeech = async (
|
||||
token: string = '',
|
||||
speaker: string = 'alloy',
|
||||
text: string = ''
|
||||
text: string = '',
|
||||
model: string = 'tts-1'
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
@@ -106,7 +109,7 @@ export const synthesizeOpenAISpeech = async (
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'tts-1',
|
||||
model: model,
|
||||
input: text,
|
||||
voice: speaker
|
||||
})
|
||||
|
||||
@@ -95,6 +95,45 @@ export const userSignUp = async (
|
||||
return res;
|
||||
};
|
||||
|
||||
export const addUser = async (
|
||||
token: string,
|
||||
name: string,
|
||||
email: string,
|
||||
password: string,
|
||||
role: string = 'pending'
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/add`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: name,
|
||||
email: email,
|
||||
password: password,
|
||||
role: role
|
||||
})
|
||||
})
|
||||
.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 updateUserProfile = async (token: string, name: string, profileImageUrl: string) => {
|
||||
let error = null;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
import { getTimeRange } from '$lib/utils';
|
||||
|
||||
export const createNewChat = async (token: string, chat: object) => {
|
||||
let error = null;
|
||||
@@ -59,7 +60,10 @@ export const getChatList = async (token: string = '') => {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
return res.map((chat) => ({
|
||||
...chat,
|
||||
time_range: getTimeRange(chat.updated_at)
|
||||
}));
|
||||
};
|
||||
|
||||
export const getChatListByUserId = async (token: string = '', userId: string) => {
|
||||
@@ -90,7 +94,10 @@ export const getChatListByUserId = async (token: string = '', userId: string) =>
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
return res.map((chat) => ({
|
||||
...chat,
|
||||
time_range: getTimeRange(chat.updated_at)
|
||||
}));
|
||||
};
|
||||
|
||||
export const getArchivedChatList = async (token: string = '') => {
|
||||
@@ -220,13 +227,16 @@ export const getAllChatTags = async (token: string) => {
|
||||
export const getChatListByTagName = async (token: string = '', tagName: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/tags/tag/${tagName}`, {
|
||||
method: 'GET',
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/tags`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
}
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: tagName
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
@@ -245,7 +255,10 @@ export const getChatListByTagName = async (token: string = '', tagName: string)
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
return res.map((chat) => ({
|
||||
...chat,
|
||||
time_range: getTimeRange(chat.updated_at)
|
||||
}));
|
||||
};
|
||||
|
||||
export const getChatById = async (token: string, id: string) => {
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
export const getMemories = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/memories`, {
|
||||
method: 'GET',
|
||||
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;
|
||||
};
|
||||
|
||||
export const addNewMemory = async (token: string, content: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/memories/add`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: content
|
||||
})
|
||||
})
|
||||
.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;
|
||||
};
|
||||
|
||||
export const queryMemory = async (token: string, content: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/memories/query`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: content
|
||||
})
|
||||
})
|
||||
.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;
|
||||
};
|
||||
|
||||
export const deleteMemoryById = async (token: string, id: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/memories/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const deleteMemoriesByUserId = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/memories/user`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
@@ -159,7 +159,11 @@ export const generateTitle = async (
|
||||
body: JSON.stringify({
|
||||
model: model,
|
||||
prompt: template,
|
||||
stream: false
|
||||
stream: false,
|
||||
options: {
|
||||
// Restrict the number of tokens generated to 50
|
||||
num_predict: 50
|
||||
}
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
@@ -178,7 +182,7 @@ export const generateTitle = async (
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res?.response ?? 'New Chat';
|
||||
return res?.response.replace(/["']/g, '') ?? 'New Chat';
|
||||
};
|
||||
|
||||
export const generatePrompt = async (token: string = '', model: string, conversation: string) => {
|
||||
|
||||
@@ -1,6 +1,73 @@
|
||||
import { OPENAI_API_BASE_URL } from '$lib/constants';
|
||||
import { promptTemplate } from '$lib/utils';
|
||||
|
||||
export const getOpenAIConfig = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${OPENAI_API_BASE_URL}/config`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
} else {
|
||||
error = 'Server connection failed';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const updateOpenAIConfig = async (token: string = '', enable_openai_api: boolean) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${OPENAI_API_BASE_URL}/config/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
},
|
||||
body: JSON.stringify({
|
||||
enable_openai_api: enable_openai_api
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
} else {
|
||||
error = 'Server connection failed';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getOpenAIUrls = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
@@ -239,7 +306,8 @@ export const generateOpenAIChatCompletion = async (
|
||||
export const synthesizeOpenAISpeech = async (
|
||||
token: string = '',
|
||||
speaker: string = 'alloy',
|
||||
text: string = ''
|
||||
text: string = '',
|
||||
model: string = 'tts-1'
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
@@ -250,7 +318,7 @@ export const synthesizeOpenAISpeech = async (
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'tts-1',
|
||||
model: model,
|
||||
input: text,
|
||||
voice: speaker
|
||||
})
|
||||
@@ -295,7 +363,9 @@ export const generateTitle = async (
|
||||
content: template
|
||||
}
|
||||
],
|
||||
stream: false
|
||||
stream: false,
|
||||
// Restricting the max tokens to 50 to avoid long titles
|
||||
max_tokens: 50
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
@@ -314,5 +384,5 @@ export const generateTitle = async (
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res?.choices[0]?.message?.content ?? 'New Chat';
|
||||
return res?.choices[0]?.message?.content.replace(/["']/g, '') ?? 'New Chat';
|
||||
};
|
||||
|
||||
@@ -32,9 +32,16 @@ type ChunkConfigForm = {
|
||||
chunk_overlap: number;
|
||||
};
|
||||
|
||||
type YoutubeConfigForm = {
|
||||
language: string[];
|
||||
translation?: string | null;
|
||||
};
|
||||
|
||||
type RAGConfigForm = {
|
||||
pdf_extract_images: boolean;
|
||||
chunk: ChunkConfigForm;
|
||||
pdf_extract_images?: boolean;
|
||||
chunk?: ChunkConfigForm;
|
||||
web_loader_ssl_verification?: boolean;
|
||||
youtube?: YoutubeConfigForm;
|
||||
};
|
||||
|
||||
export const updateRAGConfig = async (token: string, payload: RAGConfigForm) => {
|
||||
@@ -221,6 +228,37 @@ export const uploadWebToVectorDB = async (token: string, collection_name: string
|
||||
return res;
|
||||
};
|
||||
|
||||
export const uploadYoutubeTranscriptionToVectorDB = async (token: string, url: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${RAG_API_BASE_URL}/youtube`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: url
|
||||
})
|
||||
})
|
||||
.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;
|
||||
};
|
||||
|
||||
export const queryDoc = async (
|
||||
token: string,
|
||||
collection_name: string,
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
import { EventSourceParserStream } from 'eventsource-parser/stream';
|
||||
import type { ParsedEvent } from 'eventsource-parser';
|
||||
|
||||
type TextStreamUpdate = {
|
||||
done: boolean;
|
||||
value: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
citations?: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
error?: any;
|
||||
};
|
||||
|
||||
// createOpenAITextStream takes a ReadableStreamDefaultReader from an SSE response,
|
||||
// createOpenAITextStream takes a responseBody with a SSE response,
|
||||
// and returns an async generator that emits delta updates with large deltas chunked into random sized chunks
|
||||
export async function createOpenAITextStream(
|
||||
messageStream: ReadableStreamDefaultReader,
|
||||
responseBody: ReadableStream<Uint8Array>,
|
||||
splitLargeDeltas: boolean
|
||||
): Promise<AsyncGenerator<TextStreamUpdate>> {
|
||||
let iterator = openAIStreamToIterator(messageStream);
|
||||
const eventStream = responseBody
|
||||
.pipeThrough(new TextDecoderStream())
|
||||
.pipeThrough(new EventSourceParserStream())
|
||||
.getReader();
|
||||
let iterator = openAIStreamToIterator(eventStream);
|
||||
if (splitLargeDeltas) {
|
||||
iterator = streamLargeDeltasAsRandomChunks(iterator);
|
||||
}
|
||||
@@ -17,7 +28,7 @@ export async function createOpenAITextStream(
|
||||
}
|
||||
|
||||
async function* openAIStreamToIterator(
|
||||
reader: ReadableStreamDefaultReader
|
||||
reader: ReadableStreamDefaultReader<ParsedEvent>
|
||||
): AsyncGenerator<TextStreamUpdate> {
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
@@ -25,31 +36,32 @@ async function* openAIStreamToIterator(
|
||||
yield { done: true, value: '' };
|
||||
break;
|
||||
}
|
||||
const lines = value.split('\n');
|
||||
for (let line of lines) {
|
||||
if (line.endsWith('\r')) {
|
||||
// Remove trailing \r
|
||||
line = line.slice(0, -1);
|
||||
}
|
||||
if (line !== '') {
|
||||
console.log(line);
|
||||
if (line === 'data: [DONE]') {
|
||||
yield { done: true, value: '' };
|
||||
} else if (line.startsWith(':')) {
|
||||
// Events starting with : are comments https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format
|
||||
// OpenRouter sends heartbeats like ": OPENROUTER PROCESSING"
|
||||
continue;
|
||||
} else {
|
||||
try {
|
||||
const data = JSON.parse(line.replace(/^data: /, ''));
|
||||
console.log(data);
|
||||
if (!value) {
|
||||
continue;
|
||||
}
|
||||
const data = value.data;
|
||||
if (data.startsWith('[DONE]')) {
|
||||
yield { done: true, value: '' };
|
||||
break;
|
||||
}
|
||||
|
||||
yield { done: false, value: data.choices?.[0]?.delta?.content ?? '' };
|
||||
} catch (e) {
|
||||
console.error('Error extracting delta from SSE event:', e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
const parsedData = JSON.parse(data);
|
||||
console.log(parsedData);
|
||||
|
||||
if (parsedData.error) {
|
||||
yield { done: true, value: '', error: parsedData.error };
|
||||
break;
|
||||
}
|
||||
|
||||
if (parsedData.citations) {
|
||||
yield { done: false, value: '', citations: parsedData.citations };
|
||||
continue;
|
||||
}
|
||||
|
||||
yield { done: false, value: parsedData.choices?.[0]?.delta?.content ?? '' };
|
||||
} catch (e) {
|
||||
console.error('Error extracting delta from SSE event:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,6 +76,10 @@ async function* streamLargeDeltasAsRandomChunks(
|
||||
yield textStreamUpdate;
|
||||
return;
|
||||
}
|
||||
if (textStreamUpdate.citations) {
|
||||
yield textStreamUpdate;
|
||||
continue;
|
||||
}
|
||||
let content = textStreamUpdate.value;
|
||||
if (content.length < 5) {
|
||||
yield { done: false, value: content };
|
||||
@@ -73,7 +89,11 @@ async function* streamLargeDeltasAsRandomChunks(
|
||||
const chunkSize = Math.min(Math.floor(Math.random() * 3) + 1, content.length);
|
||||
const chunk = content.slice(0, chunkSize);
|
||||
yield { done: false, value: chunk };
|
||||
await sleep(5);
|
||||
// Do not sleep if the tab is hidden
|
||||
// Timers are throttled to 1s in hidden tabs
|
||||
if (document?.visibilityState !== 'hidden') {
|
||||
await sleep(5);
|
||||
}
|
||||
content = content.slice(chunkSize);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +115,33 @@ export const getUsers = async (token: string) => {
|
||||
return res ? res : [];
|
||||
};
|
||||
|
||||
export const getUserById = async (token: string, userId: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/users/${userId}`, {
|
||||
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 deleteUserById = async (token: string, userId: string) => {
|
||||
let error = null;
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
</script>
|
||||
|
||||
<Modal bind:show>
|
||||
<div class="px-5 py-4 dark:text-gray-300">
|
||||
<div class="px-5 pt-4 dark:text-gray-300 text-gray-700">
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="text-xl font-bold">
|
||||
{$i18n.t('What’s New in')}
|
||||
@@ -57,10 +57,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-800" />
|
||||
|
||||
<div class=" w-full p-4 px-5">
|
||||
<div class=" overflow-y-scroll max-h-80">
|
||||
<div class=" w-full p-4 px-5 text-gray-700 dark:text-gray-100">
|
||||
<div class=" overflow-y-scroll max-h-80 scrollbar-hidden">
|
||||
<div class="mb-3">
|
||||
{#if changelog}
|
||||
{#each Object.keys(changelog) as version}
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { addUser } from '$lib/apis/auths';
|
||||
|
||||
import Modal from '../common/Modal.svelte';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let show = false;
|
||||
|
||||
let loading = false;
|
||||
let tab = '';
|
||||
let inputFiles;
|
||||
|
||||
let _user = {
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
role: 'user'
|
||||
};
|
||||
|
||||
$: if (show) {
|
||||
_user = {
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
role: 'user'
|
||||
};
|
||||
}
|
||||
|
||||
const submitHandler = async () => {
|
||||
const stopLoading = () => {
|
||||
dispatch('save');
|
||||
loading = false;
|
||||
};
|
||||
|
||||
if (tab === '') {
|
||||
loading = true;
|
||||
|
||||
const res = await addUser(
|
||||
localStorage.token,
|
||||
_user.name,
|
||||
_user.email,
|
||||
_user.password,
|
||||
_user.role
|
||||
).catch((error) => {
|
||||
toast.error(error);
|
||||
});
|
||||
|
||||
if (res) {
|
||||
stopLoading();
|
||||
show = false;
|
||||
}
|
||||
} else {
|
||||
if (inputFiles) {
|
||||
loading = true;
|
||||
|
||||
const file = inputFiles[0];
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = async (e) => {
|
||||
const csv = e.target.result;
|
||||
const rows = csv.split('\n');
|
||||
|
||||
let userCount = 0;
|
||||
|
||||
for (const [idx, row] of rows.entries()) {
|
||||
const columns = row.split(',').map((col) => col.trim());
|
||||
console.log(idx, columns);
|
||||
|
||||
if (idx > 0) {
|
||||
if (
|
||||
columns.length === 4 &&
|
||||
['admin', 'user', 'pending'].includes(columns[3].toLowerCase())
|
||||
) {
|
||||
const res = await addUser(
|
||||
localStorage.token,
|
||||
columns[0],
|
||||
columns[1],
|
||||
columns[2],
|
||||
columns[3].toLowerCase()
|
||||
).catch((error) => {
|
||||
toast.error(`Row ${idx + 1}: ${error}`);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
userCount = userCount + 1;
|
||||
}
|
||||
} else {
|
||||
toast.error(`Row ${idx + 1}: invalid format.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toast.success(`Successfully imported ${userCount} users.`);
|
||||
inputFiles = null;
|
||||
const uploadInputElement = document.getElementById('upload-user-csv-input');
|
||||
|
||||
if (uploadInputElement) {
|
||||
uploadInputElement.value = null;
|
||||
}
|
||||
|
||||
stopLoading();
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
} else {
|
||||
toast.error($i18n.t('File not found.'));
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal size="sm" bind:show>
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
|
||||
<div class=" text-lg font-medium self-center">{$i18n.t('Add User')}</div>
|
||||
<button
|
||||
class="self-center"
|
||||
on:click={() => {
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row w-full px-5 pb-4 md:space-x-4 dark:text-gray-200">
|
||||
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
||||
<form
|
||||
class="flex flex-col w-full"
|
||||
on:submit|preventDefault={() => {
|
||||
submitHandler();
|
||||
}}
|
||||
>
|
||||
<div class="flex text-center text-sm font-medium rounded-xl bg-transparent/10 p-1 mb-2">
|
||||
<button
|
||||
class="w-full rounded-lg p-1.5 {tab === '' ? 'bg-gray-50 dark:bg-gray-850' : ''}"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
tab = '';
|
||||
}}>Form</button
|
||||
>
|
||||
|
||||
<button
|
||||
class="w-full rounded-lg p-1 {tab === 'import' ? 'bg-gray-50 dark:bg-gray-850' : ''}"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
tab = 'import';
|
||||
}}>CSV Import</button
|
||||
>
|
||||
</div>
|
||||
<div class="px-1">
|
||||
{#if tab === ''}
|
||||
<div class="flex flex-col w-full">
|
||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Role')}</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<select
|
||||
class="w-full capitalize rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
|
||||
bind:value={_user.role}
|
||||
placeholder={$i18n.t('Enter Your Role')}
|
||||
required
|
||||
>
|
||||
<option value="pending"> pending </option>
|
||||
<option value="user"> user </option>
|
||||
<option value="admin"> admin </option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col w-full mt-2">
|
||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
|
||||
type="text"
|
||||
bind:value={_user.name}
|
||||
placeholder={$i18n.t('Enter Your Full Name')}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-800 my-3 w-full" />
|
||||
|
||||
<div class="flex flex-col w-full">
|
||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
|
||||
type="email"
|
||||
bind:value={_user.email}
|
||||
placeholder={$i18n.t('Enter Your Email')}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col w-full mt-2">
|
||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Password')}</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
|
||||
type="password"
|
||||
bind:value={_user.password}
|
||||
placeholder={$i18n.t('Enter Your Password')}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else if tab === 'import'}
|
||||
<div>
|
||||
<div class="mb-3 w-full">
|
||||
<input
|
||||
id="upload-user-csv-input"
|
||||
hidden
|
||||
bind:files={inputFiles}
|
||||
type="file"
|
||||
accept=".csv"
|
||||
/>
|
||||
|
||||
<button
|
||||
class="w-full text-sm font-medium py-3 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-800 dark:hover:bg-gray-850 text-center rounded-xl"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
document.getElementById('upload-user-csv-input')?.click();
|
||||
}}
|
||||
>
|
||||
{#if inputFiles}
|
||||
{inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected.
|
||||
{:else}
|
||||
{$i18n.t('Click here to select a csv file.')}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class=" text-xs text-gray-500">
|
||||
ⓘ {$i18n.t(
|
||||
'Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.'
|
||||
)}
|
||||
<a
|
||||
class="underline dark:text-gray-200"
|
||||
href="{WEBUI_BASE_URL}/static/user-import.csv"
|
||||
>
|
||||
Click here to download user import template file.
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||
<button
|
||||
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg flex flex-row space-x-1 items-center {loading
|
||||
? ' cursor-not-allowed'
|
||||
: ''}"
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
>
|
||||
{$i18n.t('Submit')}
|
||||
|
||||
{#if loading}
|
||||
<div class="ml-2 self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
/* display: none; <- Crashes Chrome on hover */
|
||||
-webkit-appearance: none;
|
||||
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
|
||||
}
|
||||
|
||||
.tabs::-webkit-scrollbar {
|
||||
display: none; /* for Chrome, Safari and Opera */
|
||||
}
|
||||
|
||||
.tabs {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
input[type='number'] {
|
||||
-moz-appearance: textfield; /* Firefox */
|
||||
}
|
||||
</style>
|
||||
@@ -123,7 +123,7 @@
|
||||
|
||||
<div class="flex mt-2 space-x-2">
|
||||
<input
|
||||
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="text"
|
||||
placeholder={`https://example.com/webhook`}
|
||||
bind:value={webhookUrl}
|
||||
@@ -140,7 +140,7 @@
|
||||
|
||||
<div class="flex mt-2 space-x-2">
|
||||
<input
|
||||
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="text"
|
||||
placeholder={`e.g.) "30m","1h", "10d". `}
|
||||
bind:value={JWTExpiresIn}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
<Modal bind:show>
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
||||
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
|
||||
<div class=" text-lg font-medium self-center">{$i18n.t('Admin Settings')}</div>
|
||||
<button
|
||||
class="self-center"
|
||||
@@ -35,7 +35,6 @@
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<hr class=" dark:border-gray-800" />
|
||||
|
||||
<div class="flex flex-col md:flex-row w-full p-4 md:space-x-4">
|
||||
<div
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
>
|
||||
<tr>
|
||||
<th scope="col" class="px-3 py-2"> {$i18n.t('Name')} </th>
|
||||
<th scope="col" class="px-3 py-2 hidden md:flex"> {$i18n.t('Created At')} </th>
|
||||
<th scope="col" class="px-3 py-2 hidden md:flex"> {$i18n.t('Created at')} </th>
|
||||
<th scope="col" class="px-3 py-2 text-right" />
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -96,7 +96,7 @@
|
||||
|
||||
<td class="px-3 py-1 text-right">
|
||||
<div class="flex justify-end w-full">
|
||||
<Tooltip content="Delete Chat">
|
||||
<Tooltip content={$i18n.t('Delete Chat')}>
|
||||
<button
|
||||
class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
||||
on:click={async () => {
|
||||
@@ -133,7 +133,10 @@
|
||||
{/each} -->
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-left text-sm w-full mb-8">You have no archived conversations.</div>
|
||||
<div class="text-left text-sm w-full mb-8">
|
||||
{user.name}
|
||||
{$i18n.t('has no conversations.')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -24,7 +24,7 @@
|
||||
{
|
||||
name: 'All Documents',
|
||||
type: 'collection',
|
||||
title: 'All Documents',
|
||||
title: $i18n.t('All Documents'),
|
||||
collection_names: $documents.map((doc) => doc.collection_name)
|
||||
}
|
||||
]
|
||||
@@ -87,6 +87,17 @@
|
||||
chatInputElement?.focus();
|
||||
await tick();
|
||||
};
|
||||
|
||||
const confirmSelectYoutube = async (url) => {
|
||||
dispatch('youtube', url);
|
||||
|
||||
prompt = removeFirstHashWord(prompt);
|
||||
const chatInputElement = document.getElementById('chat-textarea');
|
||||
|
||||
await tick();
|
||||
chatInputElement?.focus();
|
||||
await tick();
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
|
||||
@@ -132,7 +143,34 @@
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
{#if prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
|
||||
{#if prompt
|
||||
.split(' ')
|
||||
.some((s) => s.substring(1).startsWith('https://www.youtube.com') || s
|
||||
.substring(1)
|
||||
.startsWith('https://youtu.be'))}
|
||||
<button
|
||||
class="px-3 py-1.5 rounded-xl w-full text-left bg-gray-100 selected-command-option-button"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
const url = prompt.split(' ')?.at(0)?.substring(1);
|
||||
if (isValidHttpUrl(url)) {
|
||||
confirmSelectYoutube(url);
|
||||
} else {
|
||||
toast.error(
|
||||
$i18n.t(
|
||||
'Oops! Looks like the URL is invalid. Please double-check and try again.'
|
||||
)
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class=" font-medium text-black line-clamp-1">
|
||||
{prompt.split(' ')?.at(0)?.substring(1)}
|
||||
</div>
|
||||
|
||||
<div class=" text-xs text-gray-600 line-clamp-1">{$i18n.t('Youtube')}</div>
|
||||
</button>
|
||||
{:else if prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
|
||||
<button
|
||||
class="px-3 py-1.5 rounded-xl w-full text-left bg-gray-100 selected-command-option-button"
|
||||
type="button"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
import { generatePrompt } from '$lib/apis/ollama';
|
||||
import { models } from '$lib/stores';
|
||||
import { splitStream } from '$lib/utils';
|
||||
@@ -7,6 +9,8 @@
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let prompt = '';
|
||||
export let user = null;
|
||||
|
||||
@@ -17,12 +21,7 @@
|
||||
let filteredModels = [];
|
||||
|
||||
$: filteredModels = $models
|
||||
.filter(
|
||||
(p) =>
|
||||
p.name !== 'hr' &&
|
||||
!p.external &&
|
||||
p.name.includes(prompt.split(' ')?.at(0)?.substring(1) ?? '')
|
||||
)
|
||||
.filter((p) => p.name.includes(prompt.split(' ')?.at(0)?.substring(1) ?? ''))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
$: if (prompt) {
|
||||
@@ -38,6 +37,11 @@
|
||||
};
|
||||
|
||||
const confirmSelect = async (model) => {
|
||||
prompt = '';
|
||||
dispatch('select', model);
|
||||
};
|
||||
|
||||
const confirmSelectCollaborativeChat = async (model) => {
|
||||
// dispatch('select', model);
|
||||
prompt = '';
|
||||
user = JSON.parse(JSON.stringify(model.name));
|
||||
@@ -127,40 +131,42 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if filteredModels.length > 0}
|
||||
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
|
||||
<div class="flex w-full px-2">
|
||||
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
|
||||
<div class=" text-lg font-semibold mt-2">@</div>
|
||||
</div>
|
||||
{#if prompt.charAt(0) === '@'}
|
||||
{#if filteredModels.length > 0}
|
||||
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
|
||||
<div class="flex w-full px-2">
|
||||
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
|
||||
<div class=" text-lg font-semibold mt-2">@</div>
|
||||
</div>
|
||||
|
||||
<div class="max-h-60 flex flex-col w-full rounded-r-xl bg-white">
|
||||
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5">
|
||||
{#each filteredModels as model, modelIdx}
|
||||
<button
|
||||
class=" px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx
|
||||
? ' bg-gray-100 selected-command-option-button'
|
||||
: ''}"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
confirmSelect(model);
|
||||
}}
|
||||
on:mousemove={() => {
|
||||
selectedIdx = modelIdx;
|
||||
}}
|
||||
on:focus={() => {}}
|
||||
>
|
||||
<div class=" font-medium text-black line-clamp-1">
|
||||
{model.name}
|
||||
</div>
|
||||
<div class="max-h-60 flex flex-col w-full rounded-r-xl bg-white">
|
||||
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5">
|
||||
{#each filteredModels as model, modelIdx}
|
||||
<button
|
||||
class=" px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx
|
||||
? ' bg-gray-100 selected-command-option-button'
|
||||
: ''}"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
confirmSelect(model);
|
||||
}}
|
||||
on:mousemove={() => {
|
||||
selectedIdx = modelIdx;
|
||||
}}
|
||||
on:focus={() => {}}
|
||||
>
|
||||
<div class=" font-medium text-black line-clamp-1">
|
||||
{model.name}
|
||||
</div>
|
||||
|
||||
<!-- <div class=" text-xs text-gray-600 line-clamp-1">
|
||||
<!-- <div class=" text-xs text-gray-600 line-clamp-1">
|
||||
{doc.title}
|
||||
</div> -->
|
||||
</button>
|
||||
{/each}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -1,56 +1,118 @@
|
||||
<script lang="ts">
|
||||
import Bolt from '$lib/components/icons/Bolt.svelte';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let submitPrompt: Function;
|
||||
export let suggestionPrompts = [];
|
||||
|
||||
let prompts = [];
|
||||
|
||||
$: prompts =
|
||||
suggestionPrompts.length <= 4
|
||||
? suggestionPrompts
|
||||
: suggestionPrompts.sort(() => Math.random() - 0.5).slice(0, 4);
|
||||
$: prompts = suggestionPrompts
|
||||
.reduce((acc, current) => [...acc, ...[current]], [])
|
||||
.sort(() => Math.random() - 0.5);
|
||||
// suggestionPrompts.length <= 4
|
||||
// ? suggestionPrompts
|
||||
// : suggestionPrompts.sort(() => Math.random() - 0.5).slice(0, 4);
|
||||
|
||||
onMount(() => {
|
||||
const containerElement = document.getElementById('suggestions-container');
|
||||
|
||||
if (containerElement) {
|
||||
containerElement.addEventListener('wheel', function (event) {
|
||||
if (event.deltaY !== 0) {
|
||||
// If scrolling vertically, prevent default behavior
|
||||
event.preventDefault();
|
||||
// Adjust horizontal scroll position based on vertical scroll
|
||||
containerElement.scrollLeft += event.deltaY;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class=" mb-3 md:p-1 text-left w-full">
|
||||
<div class=" flex flex-wrap-reverse px-2 text-left">
|
||||
{#if prompts.length > 0}
|
||||
<div class="mb-2 flex gap-1 text-sm font-medium items-center text-gray-400 dark:text-gray-600">
|
||||
<Bolt />
|
||||
{$i18n.t('Suggested')}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="w-full">
|
||||
<div
|
||||
class="relative w-full flex gap-2 snap-x snap-mandatory md:snap-none overflow-x-auto tabs"
|
||||
id="suggestions-container"
|
||||
>
|
||||
{#each prompts as prompt, promptIdx}
|
||||
<div
|
||||
class="{promptIdx > 1 ? 'hidden sm:inline-flex' : ''} basis-full sm:basis-1/2 p-[5px] px-1"
|
||||
>
|
||||
<div class="snap-center shrink-0">
|
||||
<button
|
||||
class=" flex-1 flex justify-between w-full h-full px-4 py-2.5 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 rounded-2xl transition group"
|
||||
class="flex flex-col flex-1 shrink-0 w-64 justify-between h-36 p-5 px-6 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 rounded-3xl transition group"
|
||||
on:click={() => {
|
||||
submitPrompt(prompt.content);
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col text-left self-center">
|
||||
<div class="flex flex-col text-left">
|
||||
{#if prompt.title && prompt.title[0] !== ''}
|
||||
<div class="text-sm font-medium dark:text-gray-300">{prompt.title[0]}</div>
|
||||
<div class="text-sm text-gray-500 line-clamp-1">{prompt.title[1]}</div>
|
||||
<div
|
||||
class=" font-medium dark:text-gray-300 dark:group-hover:text-gray-200 transition"
|
||||
>
|
||||
{prompt.title[0]}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600 font-normal line-clamp-2">{prompt.title[1]}</div>
|
||||
{:else}
|
||||
<div class=" self-center text-sm font-medium dark:text-gray-300 line-clamp-2">
|
||||
<div
|
||||
class=" self-center text-sm font-medium dark:text-gray-300 dark:group-hover:text-gray-100 transition line-clamp-2"
|
||||
>
|
||||
{prompt.content}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="self-center p-1 rounded-lg text-gray-50 group-hover:text-gray-800 dark:text-gray-850 dark:group-hover:text-gray-100 transition"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
<div class="w-full flex justify-between">
|
||||
<div
|
||||
class="text-xs text-gray-400 group-hover:text-gray-500 dark:text-gray-600 dark:group-hover:text-gray-500 transition self-center"
|
||||
>
|
||||
<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>
|
||||
{$i18n.t('Prompt')}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="self-end p-1 rounded-lg text-gray-300 group-hover:text-gray-800 dark:text-gray-700 dark:group-hover:text-gray-100 transition"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<!-- <div class="snap-center shrink-0">
|
||||
<img
|
||||
class="shrink-0 w-80 h-40 rounded-lg shadow-xl bg-white"
|
||||
src="https://images.unsplash.com/photo-1604999565976-8913ad2ddb7c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=320&h=160&q=80"
|
||||
/>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.tabs::-webkit-scrollbar {
|
||||
display: none; /* for Chrome, Safari and Opera */
|
||||
}
|
||||
|
||||
.tabs {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { chats, config, modelfiles, settings, user } from '$lib/stores';
|
||||
import { chats, config, modelfiles, settings, user as _user, mobile } from '$lib/stores';
|
||||
import { tick, getContext } from 'svelte';
|
||||
|
||||
import { toast } from 'svelte-sonner';
|
||||
@@ -12,7 +12,9 @@
|
||||
import Placeholder from './Messages/Placeholder.svelte';
|
||||
import Spinner from '../common/Spinner.svelte';
|
||||
import { imageGenerations } from '$lib/apis/images';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
import { copyToClipboard, findWordIndices } from '$lib/utils';
|
||||
import CompareMessages from './Messages/CompareMessages.svelte';
|
||||
import { stringify } from 'postcss';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -22,13 +24,16 @@
|
||||
export let continueGeneration: Function;
|
||||
export let regenerateResponse: Function;
|
||||
|
||||
export let user = $_user;
|
||||
export let prompt;
|
||||
export let suggestionPrompts = [];
|
||||
export let processing = '';
|
||||
export let bottomPadding = false;
|
||||
export let autoScroll;
|
||||
export let selectedModels;
|
||||
export let history = {};
|
||||
export let messages = [];
|
||||
|
||||
export let selectedModels;
|
||||
export let selectedModelfiles = [];
|
||||
|
||||
$: if (autoScroll && bottomPadding) {
|
||||
@@ -60,7 +65,8 @@
|
||||
childrenIds: [],
|
||||
role: 'user',
|
||||
content: userPrompt,
|
||||
...(history.messages[messageId].files && { files: history.messages[messageId].files })
|
||||
...(history.messages[messageId].files && { files: history.messages[messageId].files }),
|
||||
models: selectedModels.filter((m, mIdx) => selectedModels.indexOf(m) === mIdx)
|
||||
};
|
||||
|
||||
let messageParentId = history.messages[messageId].parentId;
|
||||
@@ -76,7 +82,7 @@
|
||||
history.currentId = userMessageId;
|
||||
|
||||
await tick();
|
||||
await sendPrompt(userPrompt, userMessageId, chatId);
|
||||
await sendPrompt(userPrompt, userMessageId);
|
||||
};
|
||||
|
||||
const updateChatMessages = async () => {
|
||||
@@ -236,108 +242,145 @@
|
||||
history: history
|
||||
});
|
||||
};
|
||||
|
||||
// const messageDeleteHandler = async (messageId) => {
|
||||
// const message = history.messages[messageId];
|
||||
// const parentId = message.parentId;
|
||||
// const childrenIds = message.childrenIds ?? [];
|
||||
// const grandchildrenIds = [];
|
||||
|
||||
// // Iterate through childrenIds to find grandchildrenIds
|
||||
// for (const childId of childrenIds) {
|
||||
// const childMessage = history.messages[childId];
|
||||
// const grandChildrenIds = childMessage.childrenIds ?? [];
|
||||
|
||||
// for (const grandchildId of grandchildrenIds) {
|
||||
// const childMessage = history.messages[grandchildId];
|
||||
// childMessage.parentId = parentId;
|
||||
// }
|
||||
// grandchildrenIds.push(...grandChildrenIds);
|
||||
// }
|
||||
|
||||
// history.messages[parentId].childrenIds.push(...grandchildrenIds);
|
||||
// history.messages[parentId].childrenIds = history.messages[parentId].childrenIds.filter(
|
||||
// (id) => id !== messageId
|
||||
// );
|
||||
|
||||
// // Select latest message
|
||||
// let currentMessageId = grandchildrenIds.at(-1);
|
||||
// if (currentMessageId) {
|
||||
// let messageChildrenIds = history.messages[currentMessageId].childrenIds;
|
||||
// while (messageChildrenIds.length !== 0) {
|
||||
// currentMessageId = messageChildrenIds.at(-1);
|
||||
// messageChildrenIds = history.messages[currentMessageId].childrenIds;
|
||||
// }
|
||||
// history.currentId = currentMessageId;
|
||||
// }
|
||||
|
||||
// await updateChatById(localStorage.token, chatId, { messages, history });
|
||||
// };
|
||||
</script>
|
||||
|
||||
{#if messages.length == 0}
|
||||
<Placeholder models={selectedModels} modelfiles={selectedModelfiles} />
|
||||
{:else}
|
||||
<div class=" pb-10">
|
||||
{#key chatId}
|
||||
{#each messages as message, messageIdx}
|
||||
<div class=" w-full">
|
||||
<div
|
||||
class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null
|
||||
? 'max-w-full'
|
||||
: 'max-w-3xl'} mx-auto rounded-lg group"
|
||||
>
|
||||
{#if message.role === 'user'}
|
||||
<UserMessage
|
||||
on:delete={() => messageDeleteHandler(message.id)}
|
||||
user={$user}
|
||||
{readOnly}
|
||||
{message}
|
||||
isFirstMessage={messageIdx === 0}
|
||||
siblings={message.parentId !== null
|
||||
? history.messages[message.parentId]?.childrenIds ?? []
|
||||
: Object.values(history.messages)
|
||||
.filter((message) => message.parentId === null)
|
||||
.map((message) => message.id) ?? []}
|
||||
{confirmEditMessage}
|
||||
{showPreviousMessage}
|
||||
{showNextMessage}
|
||||
copyToClipboard={copyToClipboardWithToast}
|
||||
/>
|
||||
{:else}
|
||||
<ResponseMessage
|
||||
{message}
|
||||
modelfiles={selectedModelfiles}
|
||||
siblings={history.messages[message.parentId]?.childrenIds ?? []}
|
||||
isLastMessage={messageIdx + 1 === messages.length}
|
||||
{readOnly}
|
||||
{updateChatMessages}
|
||||
{confirmEditResponseMessage}
|
||||
{showPreviousMessage}
|
||||
{showNextMessage}
|
||||
{rateMessage}
|
||||
copyToClipboard={copyToClipboardWithToast}
|
||||
{continueGeneration}
|
||||
{regenerateResponse}
|
||||
on:save={async (e) => {
|
||||
console.log('save', e);
|
||||
<div class="h-full flex mb-16">
|
||||
{#if messages.length == 0}
|
||||
<Placeholder
|
||||
models={selectedModels}
|
||||
modelfiles={selectedModelfiles}
|
||||
{suggestionPrompts}
|
||||
submitPrompt={async (p) => {
|
||||
let text = p;
|
||||
|
||||
const message = e.detail;
|
||||
history.messages[message.id] = message;
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
messages: messages,
|
||||
history: history
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
if (p.includes('{{CLIPBOARD}}')) {
|
||||
const clipboardText = await navigator.clipboard.readText().catch((err) => {
|
||||
toast.error($i18n.t('Failed to read clipboard contents'));
|
||||
return '{{CLIPBOARD}}';
|
||||
});
|
||||
|
||||
text = p.replaceAll('{{CLIPBOARD}}', clipboardText);
|
||||
}
|
||||
|
||||
prompt = text;
|
||||
|
||||
await tick();
|
||||
|
||||
const chatInputElement = document.getElementById('chat-textarea');
|
||||
if (chatInputElement) {
|
||||
prompt = p;
|
||||
|
||||
chatInputElement.style.height = '';
|
||||
chatInputElement.style.height = Math.min(chatInputElement.scrollHeight, 200) + 'px';
|
||||
chatInputElement.focus();
|
||||
|
||||
const words = findWordIndices(prompt);
|
||||
|
||||
if (words.length > 0) {
|
||||
const word = words.at(0);
|
||||
chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1);
|
||||
}
|
||||
}
|
||||
|
||||
await tick();
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<div class="w-full pt-2">
|
||||
{#key chatId}
|
||||
{#each messages as message, messageIdx}
|
||||
<div class=" w-full {messageIdx === messages.length - 1 ? 'pb-28' : ''}">
|
||||
<div
|
||||
class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null
|
||||
? 'max-w-full'
|
||||
: 'max-w-5xl'} mx-auto rounded-lg group"
|
||||
>
|
||||
{#if message.role === 'user'}
|
||||
<UserMessage
|
||||
on:delete={() => messageDeleteHandler(message.id)}
|
||||
{user}
|
||||
{readOnly}
|
||||
{message}
|
||||
isFirstMessage={messageIdx === 0}
|
||||
siblings={message.parentId !== null
|
||||
? history.messages[message.parentId]?.childrenIds ?? []
|
||||
: Object.values(history.messages)
|
||||
.filter((message) => message.parentId === null)
|
||||
.map((message) => message.id) ?? []}
|
||||
{confirmEditMessage}
|
||||
{showPreviousMessage}
|
||||
{showNextMessage}
|
||||
copyToClipboard={copyToClipboardWithToast}
|
||||
/>
|
||||
{:else if $mobile || (history.messages[message.parentId]?.models?.length ?? 1) === 1}
|
||||
{#key message.id}
|
||||
<ResponseMessage
|
||||
{message}
|
||||
modelfiles={selectedModelfiles}
|
||||
siblings={history.messages[message.parentId]?.childrenIds ?? []}
|
||||
isLastMessage={messageIdx + 1 === messages.length}
|
||||
{readOnly}
|
||||
{updateChatMessages}
|
||||
{confirmEditResponseMessage}
|
||||
{showPreviousMessage}
|
||||
{showNextMessage}
|
||||
{rateMessage}
|
||||
copyToClipboard={copyToClipboardWithToast}
|
||||
{continueGeneration}
|
||||
{regenerateResponse}
|
||||
on:save={async (e) => {
|
||||
console.log('save', e);
|
||||
|
||||
const message = e.detail;
|
||||
history.messages[message.id] = message;
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
messages: messages,
|
||||
history: history
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{/key}
|
||||
{:else}
|
||||
{#key message.parentId}
|
||||
<CompareMessages
|
||||
bind:history
|
||||
{messages}
|
||||
{chatId}
|
||||
parentMessage={history.messages[message.parentId]}
|
||||
{messageIdx}
|
||||
{selectedModelfiles}
|
||||
{updateChatMessages}
|
||||
{confirmEditResponseMessage}
|
||||
{rateMessage}
|
||||
copyToClipboard={copyToClipboardWithToast}
|
||||
{continueGeneration}
|
||||
{regenerateResponse}
|
||||
on:change={async () => {
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
messages: messages,
|
||||
history: history
|
||||
});
|
||||
|
||||
if (autoScroll) {
|
||||
const element = document.getElementById('messages-container');
|
||||
autoScroll =
|
||||
element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
|
||||
setTimeout(() => {
|
||||
scrollToBottom();
|
||||
}, 100);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/each}
|
||||
|
||||
{#if bottomPadding}
|
||||
<div class=" mb-10" />
|
||||
{/if}
|
||||
{/key}
|
||||
</div>
|
||||
{/if}
|
||||
{#if bottomPadding}
|
||||
<div class=" pb-20" />
|
||||
{/if}
|
||||
{/key}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onMount, tick } from 'svelte';
|
||||
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let show = false;
|
||||
export let citation;
|
||||
|
||||
let mergedDocuments = [];
|
||||
|
||||
$: if (citation) {
|
||||
mergedDocuments = citation.document?.map((c, i) => {
|
||||
return {
|
||||
source: citation.source,
|
||||
document: c,
|
||||
metadata: citation.metadata?.[i]
|
||||
};
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal size="lg" bind:show>
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
|
||||
<div class=" text-lg font-medium self-center capitalize">
|
||||
{$i18n.t('Citation')}
|
||||
</div>
|
||||
<button
|
||||
class="self-center"
|
||||
on:click={() => {
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row w-full px-6 pb-5 md:space-x-4">
|
||||
<div
|
||||
class="flex flex-col w-full dark:text-gray-200 overflow-y-scroll max-h-[22rem] scrollbar-hidden"
|
||||
>
|
||||
{#each mergedDocuments as document, documentIdx}
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="text-sm font-medium dark:text-gray-300">
|
||||
{$i18n.t('Source')}
|
||||
</div>
|
||||
<div class="text-sm dark:text-gray-400">
|
||||
{document.source?.name ?? $i18n.t('No source available')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col w-full">
|
||||
<div class=" text-sm font-medium dark:text-gray-300">
|
||||
{$i18n.t('Content')}
|
||||
</div>
|
||||
<pre class="text-sm dark:text-gray-400 whitespace-pre-line">
|
||||
{document.document}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{#if documentIdx !== mergedDocuments.length - 1}
|
||||
<hr class=" dark:border-gray-850 my-3" />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -1,11 +1,23 @@
|
||||
<script lang="ts">
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
import hljs from 'highlight.js';
|
||||
import 'highlight.js/styles/github-dark.min.css';
|
||||
import { loadPyodide } from 'pyodide';
|
||||
import { tick } from 'svelte';
|
||||
import PyodideWorker from '$lib/workers/pyodide.worker?worker';
|
||||
|
||||
export let id = '';
|
||||
|
||||
export let lang = '';
|
||||
export let code = '';
|
||||
|
||||
let executing = false;
|
||||
|
||||
let stdout = null;
|
||||
let stderr = null;
|
||||
let result = null;
|
||||
|
||||
let copied = false;
|
||||
|
||||
const copyCode = async () => {
|
||||
@@ -17,22 +29,233 @@
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const checkPythonCode = (str) => {
|
||||
// Check if the string contains typical Python syntax characters
|
||||
const pythonSyntax = [
|
||||
'def ',
|
||||
'else:',
|
||||
'elif ',
|
||||
'try:',
|
||||
'except:',
|
||||
'finally:',
|
||||
'yield ',
|
||||
'lambda ',
|
||||
'assert ',
|
||||
'nonlocal ',
|
||||
'del ',
|
||||
'True',
|
||||
'False',
|
||||
'None',
|
||||
' and ',
|
||||
' or ',
|
||||
' not ',
|
||||
' in ',
|
||||
' is ',
|
||||
' with '
|
||||
];
|
||||
|
||||
for (let syntax of pythonSyntax) {
|
||||
if (str.includes(syntax)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// If none of the above conditions met, it's probably not Python code
|
||||
return false;
|
||||
};
|
||||
|
||||
const executePython = async (code) => {
|
||||
if (!code.includes('input') && !code.includes('matplotlib')) {
|
||||
executePythonAsWorker(code);
|
||||
} else {
|
||||
result = null;
|
||||
stdout = null;
|
||||
stderr = null;
|
||||
|
||||
executing = true;
|
||||
|
||||
document.pyodideMplTarget = document.getElementById(`plt-canvas-${id}`);
|
||||
|
||||
let pyodide = await loadPyodide({
|
||||
indexURL: '/pyodide/',
|
||||
stdout: (text) => {
|
||||
console.log('Python output:', text);
|
||||
|
||||
if (stdout) {
|
||||
stdout += `${text}\n`;
|
||||
} else {
|
||||
stdout = `${text}\n`;
|
||||
}
|
||||
},
|
||||
stderr: (text) => {
|
||||
console.log('An error occured:', text);
|
||||
if (stderr) {
|
||||
stderr += `${text}\n`;
|
||||
} else {
|
||||
stderr = `${text}\n`;
|
||||
}
|
||||
},
|
||||
packages: ['micropip']
|
||||
});
|
||||
|
||||
try {
|
||||
const micropip = pyodide.pyimport('micropip');
|
||||
|
||||
await micropip.set_index_urls('https://pypi.org/pypi/{package_name}/json');
|
||||
|
||||
let packages = [
|
||||
code.includes('requests') ? 'requests' : null,
|
||||
code.includes('bs4') ? 'beautifulsoup4' : null,
|
||||
code.includes('numpy') ? 'numpy' : null,
|
||||
code.includes('pandas') ? 'pandas' : null,
|
||||
code.includes('matplotlib') ? 'matplotlib' : null,
|
||||
code.includes('sklearn') ? 'scikit-learn' : null,
|
||||
code.includes('scipy') ? 'scipy' : null,
|
||||
code.includes('re') ? 'regex' : null,
|
||||
code.includes('seaborn') ? 'seaborn' : null
|
||||
].filter(Boolean);
|
||||
|
||||
console.log(packages);
|
||||
await micropip.install(packages);
|
||||
|
||||
result = await pyodide.runPythonAsync(`from js import prompt
|
||||
def input(p):
|
||||
return prompt(p)
|
||||
__builtins__.input = input`);
|
||||
|
||||
result = await pyodide.runPython(code);
|
||||
|
||||
if (!result) {
|
||||
result = '[NO OUTPUT]';
|
||||
}
|
||||
|
||||
console.log(result);
|
||||
console.log(stdout);
|
||||
console.log(stderr);
|
||||
|
||||
const pltCanvasElement = document.getElementById(`plt-canvas-${id}`);
|
||||
|
||||
if (pltCanvasElement?.innerHTML !== '') {
|
||||
pltCanvasElement.classList.add('pt-4');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
stderr = error;
|
||||
}
|
||||
|
||||
executing = false;
|
||||
}
|
||||
};
|
||||
|
||||
const executePythonAsWorker = async (code) => {
|
||||
result = null;
|
||||
stdout = null;
|
||||
stderr = null;
|
||||
|
||||
executing = true;
|
||||
|
||||
let packages = [
|
||||
code.includes('requests') ? 'requests' : null,
|
||||
code.includes('bs4') ? 'beautifulsoup4' : null,
|
||||
code.includes('numpy') ? 'numpy' : null,
|
||||
code.includes('pandas') ? 'pandas' : null,
|
||||
code.includes('sklearn') ? 'scikit-learn' : null,
|
||||
code.includes('scipy') ? 'scipy' : null,
|
||||
code.includes('re') ? 'regex' : null,
|
||||
code.includes('seaborn') ? 'seaborn' : null
|
||||
].filter(Boolean);
|
||||
|
||||
console.log(packages);
|
||||
|
||||
const pyodideWorker = new PyodideWorker();
|
||||
|
||||
pyodideWorker.postMessage({
|
||||
id: id,
|
||||
code: code,
|
||||
packages: packages
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (executing) {
|
||||
executing = false;
|
||||
stderr = 'Execution Time Limit Exceeded';
|
||||
pyodideWorker.terminate();
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
pyodideWorker.onmessage = (event) => {
|
||||
console.log('pyodideWorker.onmessage', event);
|
||||
const { id, ...data } = event.data;
|
||||
|
||||
console.log(id, data);
|
||||
|
||||
data['stdout'] && (stdout = data['stdout']);
|
||||
data['stderr'] && (stderr = data['stderr']);
|
||||
data['result'] && (result = data['result']);
|
||||
|
||||
executing = false;
|
||||
};
|
||||
|
||||
pyodideWorker.onerror = (event) => {
|
||||
console.log('pyodideWorker.onerror', event);
|
||||
executing = false;
|
||||
};
|
||||
};
|
||||
|
||||
$: highlightedCode = code ? hljs.highlightAuto(code, hljs.getLanguage(lang)?.aliases).value : '';
|
||||
</script>
|
||||
|
||||
{#if code}
|
||||
<div class="mb-4">
|
||||
<div class="mb-4" dir="ltr">
|
||||
<div
|
||||
class="flex justify-between bg-[#202123] text-white text-xs px-4 pt-1 pb-0.5 rounded-t-lg overflow-x-auto"
|
||||
>
|
||||
<div class="p-1">{@html lang}</div>
|
||||
<button class="copy-code-button bg-none border-none p-1" on:click={copyCode}
|
||||
>{copied ? 'Copied' : 'Copy Code'}</button
|
||||
>
|
||||
|
||||
<div class="flex items-center">
|
||||
{#if ['', 'python'].includes(lang) && (lang === 'python' || checkPythonCode(code))}
|
||||
{#if executing}
|
||||
<div class="copy-code-button bg-none border-none p-1 cursor-not-allowed">Running</div>
|
||||
{:else}
|
||||
<button
|
||||
class="copy-code-button bg-none border-none p-1"
|
||||
on:click={() => {
|
||||
executePython(code);
|
||||
}}>Run</button
|
||||
>
|
||||
{/if}
|
||||
{/if}
|
||||
<button class="copy-code-button bg-none border-none p-1" on:click={copyCode}
|
||||
>{copied ? 'Copied' : 'Copy Code'}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<pre class=" rounded-b-lg hljs p-4 px-5 overflow-x-auto rounded-t-none"><code
|
||||
<pre
|
||||
class=" hljs p-4 px-5 overflow-x-auto"
|
||||
style="border-top-left-radius: 0px; border-top-right-radius: 0px; {(executing ||
|
||||
stdout ||
|
||||
stderr ||
|
||||
result) &&
|
||||
'border-bottom-left-radius: 0px; border-bottom-right-radius: 0px;'}"><code
|
||||
class="language-{lang} rounded-t-none whitespace-pre">{@html highlightedCode || code}</code
|
||||
></pre>
|
||||
|
||||
<div
|
||||
id="plt-canvas-{id}"
|
||||
class="bg-[#202123] text-white max-w-full overflow-x-auto scrollbar-hidden"
|
||||
/>
|
||||
|
||||
{#if executing}
|
||||
<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
|
||||
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
|
||||
<div class="text-sm">Running...</div>
|
||||
</div>
|
||||
{:else if stdout || stderr || result}
|
||||
<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
|
||||
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
|
||||
<div class="text-sm">{stdout || stderr || result}</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
import { updateChatById } from '$lib/apis/chats';
|
||||
import { onMount, tick } from 'svelte';
|
||||
import ResponseMessage from './ResponseMessage.svelte';
|
||||
|
||||
export let chatId;
|
||||
|
||||
export let history;
|
||||
export let messages = [];
|
||||
export let messageIdx;
|
||||
|
||||
export let parentMessage;
|
||||
|
||||
export let selectedModelfiles;
|
||||
|
||||
export let updateChatMessages: Function;
|
||||
export let confirmEditResponseMessage: Function;
|
||||
export let rateMessage: Function;
|
||||
|
||||
export let copyToClipboard: Function;
|
||||
export let continueGeneration: Function;
|
||||
export let regenerateResponse: Function;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let currentMessageId;
|
||||
|
||||
let groupedMessagesIdx = {};
|
||||
let groupedMessages = {};
|
||||
|
||||
$: groupedMessages = parentMessage?.models.reduce((a, model) => {
|
||||
const modelMessages = parentMessage?.childrenIds
|
||||
.map((id) => history.messages[id])
|
||||
.filter((m) => m.model === model);
|
||||
|
||||
return {
|
||||
...a,
|
||||
[model]: { messages: modelMessages }
|
||||
};
|
||||
}, {});
|
||||
|
||||
const showPreviousMessage = (model) => {
|
||||
groupedMessagesIdx[model] = Math.max(0, groupedMessagesIdx[model] - 1);
|
||||
let messageId = groupedMessages[model].messages[groupedMessagesIdx[model]].id;
|
||||
|
||||
console.log(messageId);
|
||||
let messageChildrenIds = history.messages[messageId].childrenIds;
|
||||
|
||||
while (messageChildrenIds.length !== 0) {
|
||||
messageId = messageChildrenIds.at(-1);
|
||||
messageChildrenIds = history.messages[messageId].childrenIds;
|
||||
}
|
||||
|
||||
history.currentId = messageId;
|
||||
|
||||
dispatch('change');
|
||||
};
|
||||
|
||||
const showNextMessage = (model) => {
|
||||
groupedMessagesIdx[model] = Math.min(
|
||||
groupedMessages[model].messages.length - 1,
|
||||
groupedMessagesIdx[model] + 1
|
||||
);
|
||||
|
||||
let messageId = groupedMessages[model].messages[groupedMessagesIdx[model]].id;
|
||||
console.log(messageId);
|
||||
|
||||
let messageChildrenIds = history.messages[messageId].childrenIds;
|
||||
|
||||
while (messageChildrenIds.length !== 0) {
|
||||
messageId = messageChildrenIds.at(-1);
|
||||
messageChildrenIds = history.messages[messageId].childrenIds;
|
||||
}
|
||||
|
||||
history.currentId = messageId;
|
||||
|
||||
dispatch('change');
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
await tick();
|
||||
currentMessageId = messages[messageIdx].id;
|
||||
|
||||
for (const model of parentMessage?.models) {
|
||||
const idx = groupedMessages[model].messages.findIndex((m) => m.id === currentMessageId);
|
||||
|
||||
if (idx !== -1) {
|
||||
groupedMessagesIdx[model] = idx;
|
||||
} else {
|
||||
groupedMessagesIdx[model] = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="flex snap-x snap-mandatory overflow-x-auto scrollbar-hidden"
|
||||
id="responses-container-{parentMessage.id}"
|
||||
>
|
||||
{#each Object.keys(groupedMessages) as model}
|
||||
{#if groupedMessagesIdx[model] !== undefined && groupedMessages[model].messages.length > 0}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
|
||||
<div
|
||||
class=" snap-center min-w-80 w-full max-w-full m-1 border {history.messages[
|
||||
currentMessageId
|
||||
].model === model
|
||||
? 'border-gray-100 dark:border-gray-700 border-[1.5px]'
|
||||
: 'border-gray-50 dark:border-gray-850 '} transition p-5 rounded-3xl"
|
||||
on:click={() => {
|
||||
currentMessageId = groupedMessages[model].messages[groupedMessagesIdx[model]].id;
|
||||
|
||||
let messageId = groupedMessages[model].messages[groupedMessagesIdx[model]].id;
|
||||
|
||||
console.log(messageId);
|
||||
let messageChildrenIds = history.messages[messageId].childrenIds;
|
||||
|
||||
while (messageChildrenIds.length !== 0) {
|
||||
messageId = messageChildrenIds.at(-1);
|
||||
messageChildrenIds = history.messages[messageId].childrenIds;
|
||||
}
|
||||
|
||||
history.currentId = messageId;
|
||||
dispatch('change');
|
||||
}}
|
||||
>
|
||||
<ResponseMessage
|
||||
message={groupedMessages[model].messages[groupedMessagesIdx[model]]}
|
||||
modelfiles={selectedModelfiles}
|
||||
siblings={groupedMessages[model].messages.map((m) => m.id)}
|
||||
isLastMessage={true}
|
||||
{updateChatMessages}
|
||||
{confirmEditResponseMessage}
|
||||
showPreviousMessage={() => showPreviousMessage(model)}
|
||||
showNextMessage={() => showNextMessage(model)}
|
||||
{rateMessage}
|
||||
{copyToClipboard}
|
||||
{continueGeneration}
|
||||
regenerateResponse={async (message) => {
|
||||
regenerateResponse(message);
|
||||
await tick();
|
||||
groupedMessagesIdx[model] = groupedMessages[model].messages.length - 1;
|
||||
}}
|
||||
on:save={async (e) => {
|
||||
console.log('save', e);
|
||||
|
||||
const message = e.detail;
|
||||
history.messages[message.id] = message;
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
messages: messages,
|
||||
history: history
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,3 +1,3 @@
|
||||
<div class=" self-center font-bold mb-0.5 capitalize line-clamp-1">
|
||||
<div class=" self-center font-bold mb-0.5 line-clamp-1 contents">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -3,11 +3,19 @@
|
||||
import { user } from '$lib/stores';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
|
||||
import { blur, fade } from 'svelte/transition';
|
||||
|
||||
import Suggestions from '../MessageInput/Suggestions.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let models = [];
|
||||
export let modelfiles = [];
|
||||
|
||||
export let submitPrompt;
|
||||
export let suggestionPrompts;
|
||||
|
||||
let mounted = false;
|
||||
let modelfile = null;
|
||||
let selectedModelIdx = 0;
|
||||
|
||||
@@ -17,12 +25,16 @@
|
||||
$: if (models.length > 0) {
|
||||
selectedModelIdx = models.length - 1;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
mounted = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if models.length > 0}
|
||||
<div class="m-auto text-center max-w-md px-2">
|
||||
<div class="flex justify-center mt-8">
|
||||
<div class="flex -space-x-4 mb-1">
|
||||
{#key mounted}
|
||||
<div class="m-auto w-full max-w-6xl px-8 lg:px-24 pb-16">
|
||||
<div class="flex justify-start">
|
||||
<div class="flex -space-x-4 mb-1" in:fade={{ duration: 200 }}>
|
||||
{#each models as model, modelIdx}
|
||||
<button
|
||||
on:click={() => {
|
||||
@@ -31,17 +43,19 @@
|
||||
>
|
||||
{#if model in modelfiles}
|
||||
<img
|
||||
crossorigin="anonymous"
|
||||
src={modelfiles[model]?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`}
|
||||
alt="modelfile"
|
||||
class=" size-12 rounded-full border-[1px] border-gray-200 dark:border-none"
|
||||
class=" size-[2.7rem] rounded-full border-[1px] border-gray-200 dark:border-none"
|
||||
draggable="false"
|
||||
/>
|
||||
{:else}
|
||||
<img
|
||||
src={models.length === 1
|
||||
? `${WEBUI_BASE_URL}/static/favicon.png`
|
||||
crossorigin="anonymous"
|
||||
src={$i18n.language === 'dg-DG'
|
||||
? `/doge.png`
|
||||
: `${WEBUI_BASE_URL}/static/favicon.png`}
|
||||
class=" size-12 rounded-full border-[1px] border-gray-200 dark:border-none"
|
||||
class=" size-[2.7rem] rounded-full border-[1px] border-gray-200 dark:border-none"
|
||||
alt="logo"
|
||||
draggable="false"
|
||||
/>
|
||||
@@ -50,26 +64,42 @@
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class=" mt-2 mb-5 text-2xl text-gray-800 dark:text-gray-100 font-semibold">
|
||||
{#if modelfile}
|
||||
<span class=" capitalize">
|
||||
{modelfile.title}
|
||||
</span>
|
||||
<div class="mt-0.5 text-base font-normal text-gray-600 dark:text-gray-400">
|
||||
{modelfile.desc}
|
||||
</div>
|
||||
{#if modelfile.user}
|
||||
<div class="mt-0.5 text-sm font-normal text-gray-500 dark:text-gray-500">
|
||||
By <a href="https://openwebui.com/m/{modelfile.user.username}"
|
||||
>{modelfile.user.name ? modelfile.user.name : `@${modelfile.user.username}`}</a
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class=" line-clamp-1">{$i18n.t('Hello, {{name}}', { name: $user.name })}</div>
|
||||
|
||||
<div>{$i18n.t('How can I help you today?')}</div>
|
||||
{/if}
|
||||
<div
|
||||
class=" mt-2 mb-4 text-3xl text-gray-800 dark:text-gray-100 font-semibold text-left flex items-center gap-4"
|
||||
>
|
||||
<div>
|
||||
<div class=" capitalize line-clamp-1" in:fade={{ duration: 200 }}>
|
||||
{#if modelfile}
|
||||
{modelfile.title}
|
||||
{:else}
|
||||
{$i18n.t('Hello, {{name}}', { name: $user.name })}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div in:fade={{ duration: 200, delay: 200 }}>
|
||||
{#if modelfile}
|
||||
<div class="mt-0.5 text-base font-normal text-gray-500 dark:text-gray-400">
|
||||
{modelfile.desc}
|
||||
</div>
|
||||
{#if modelfile.user}
|
||||
<div class="mt-0.5 text-sm font-normal text-gray-400 dark:text-gray-500">
|
||||
By <a href="https://openwebui.com/m/{modelfile.user.username}"
|
||||
>{modelfile.user.name ? modelfile.user.name : `@${modelfile.user.username}`}</a
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class=" font-medium text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('How can I help you today?')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" w-full" in:fade={{ duration: 200, delay: 300 }}>
|
||||
<Suggestions {suggestionPrompts} {submitPrompt} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/key}
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { settings } from '$lib/stores';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
export let src = '/user.png';
|
||||
</script>
|
||||
|
||||
<div class=" mr-4">
|
||||
<img {src} class=" max-w-[28px] object-cover rounded-full" alt="profile" draggable="false" />
|
||||
<div class={($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'}>
|
||||
<img
|
||||
crossorigin="anonymous"
|
||||
src={src.startsWith(WEBUI_BASE_URL) ||
|
||||
src.startsWith('https://www.gravatar.com/avatar/') ||
|
||||
src.startsWith('data:')
|
||||
? src
|
||||
: `/user.png`}
|
||||
class=" w-8 object-cover rounded-full"
|
||||
alt="profile"
|
||||
draggable="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -39,9 +39,9 @@
|
||||
let selectedReason = null;
|
||||
let comment = '';
|
||||
|
||||
$: if (message.annotation.rating === 1) {
|
||||
$: if (message?.annotation?.rating === 1) {
|
||||
reasons = LIKE_REASONS;
|
||||
} else if (message.annotation.rating === -1) {
|
||||
} else if (message?.annotation?.rating === -1) {
|
||||
reasons = DISLIKE_REASONS;
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
submitHandler();
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
{$i18n.t('Submit')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,15 +23,16 @@
|
||||
revertSanitizedResponseContent,
|
||||
sanitizeResponseContent
|
||||
} from '$lib/utils';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
import Name from './Name.svelte';
|
||||
import ProfileImage from './ProfileImage.svelte';
|
||||
import Skeleton from './Skeleton.svelte';
|
||||
import CodeBlock from './CodeBlock.svelte';
|
||||
import Image from '$lib/components/common/Image.svelte';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import RateComment from './RateComment.svelte';
|
||||
import CitationsModal from '$lib/components/chat/Messages/CitationsModal.svelte';
|
||||
|
||||
export let modelfiles = [];
|
||||
export let message;
|
||||
@@ -64,8 +65,11 @@
|
||||
let generatingImage = false;
|
||||
|
||||
let showRateComment = false;
|
||||
let showCitationModal = false;
|
||||
|
||||
$: tokens = marked.lexer(sanitizeResponseContent(message.content));
|
||||
let selectedCitation = null;
|
||||
|
||||
$: tokens = marked.lexer(sanitizeResponseContent(message?.content));
|
||||
|
||||
const renderer = new marked.Renderer();
|
||||
|
||||
@@ -137,20 +141,22 @@
|
||||
.getElementById(`message-${message.id}`)
|
||||
?.getElementsByClassName('chat-assistant');
|
||||
|
||||
for (const element of chatMessageElements) {
|
||||
auto_render(element, {
|
||||
// customised options
|
||||
// • auto-render specific keys, e.g.:
|
||||
delimiters: [
|
||||
{ left: '$$', right: '$$', display: false },
|
||||
{ left: '$ ', right: ' $', display: false },
|
||||
{ left: '\\(', right: '\\)', display: false },
|
||||
{ left: '\\[', right: '\\]', display: false },
|
||||
{ left: '[ ', right: ' ]', display: false }
|
||||
],
|
||||
// • rendering keys, e.g.:
|
||||
throwOnError: false
|
||||
});
|
||||
if (chatMessageElements) {
|
||||
for (const element of chatMessageElements) {
|
||||
auto_render(element, {
|
||||
// customised options
|
||||
// • auto-render specific keys, e.g.:
|
||||
delimiters: [
|
||||
{ left: '$$', right: '$$', display: false },
|
||||
{ left: '$ ', right: ' $', display: false },
|
||||
{ left: '\\(', right: '\\)', display: false },
|
||||
{ left: '\\[', right: '\\]', display: false },
|
||||
{ left: '[ ', right: ' ]', display: false }
|
||||
],
|
||||
// • rendering keys, e.g.:
|
||||
throwOnError: false
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -221,7 +227,8 @@
|
||||
const res = await synthesizeOpenAISpeech(
|
||||
localStorage.token,
|
||||
$settings?.audio?.speaker,
|
||||
sentence
|
||||
sentence,
|
||||
$settings?.audio?.model
|
||||
).catch((error) => {
|
||||
toast.error(error);
|
||||
|
||||
@@ -322,13 +329,20 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<CitationsModal bind:show={showCitationModal} citation={selectedCitation} />
|
||||
|
||||
{#key message.id}
|
||||
<div class=" flex w-full message-{message.id}" id="message-{message.id}">
|
||||
<div
|
||||
class=" flex w-full message-{message.id}"
|
||||
id="message-{message.id}"
|
||||
dir={$settings.chatDirection}
|
||||
>
|
||||
<ProfileImage
|
||||
src={modelfiles[message.model]?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`}
|
||||
src={modelfiles[message.model]?.imageUrl ??
|
||||
($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
|
||||
/>
|
||||
|
||||
<div class="w-full overflow-hidden">
|
||||
<div class="w-full overflow-hidden pl-1">
|
||||
<Name>
|
||||
{#if message.model in modelfiles}
|
||||
{modelfiles[message.model]?.title}
|
||||
@@ -337,166 +351,214 @@
|
||||
{/if}
|
||||
|
||||
{#if message.timestamp}
|
||||
<span class=" invisible group-hover:visible text-gray-400 text-xs font-medium">
|
||||
{dayjs(message.timestamp * 1000).format($i18n.t('DD/MM/YYYY HH:mm'))}
|
||||
<span
|
||||
class=" self-center invisible group-hover:visible text-gray-400 text-xs font-medium uppercase"
|
||||
>
|
||||
{dayjs(message.timestamp * 1000).format($i18n.t('h:mm a'))}
|
||||
</span>
|
||||
{/if}
|
||||
</Name>
|
||||
|
||||
{#if message.content === ''}
|
||||
<Skeleton />
|
||||
{:else}
|
||||
{#if message.files}
|
||||
<div class="my-2.5 w-full flex overflow-x-auto gap-2 flex-wrap">
|
||||
{#each message.files as file}
|
||||
<div>
|
||||
{#if file.type === 'image'}
|
||||
<Image src={file.url} />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if message.files}
|
||||
<div class="my-2.5 w-full flex overflow-x-auto gap-2 flex-wrap">
|
||||
{#each message.files as file}
|
||||
<div>
|
||||
{#if file.type === 'image'}
|
||||
<Image src={file.url} />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-ol:p-0 prose-li:-mb-4 whitespace-pre-line"
|
||||
>
|
||||
<div>
|
||||
{#if edit === true}
|
||||
<div class=" w-full">
|
||||
<textarea
|
||||
id="message-edit-{message.id}"
|
||||
bind:this={editTextAreaElement}
|
||||
class=" bg-transparent outline-none w-full resize-none"
|
||||
bind:value={editedContent}
|
||||
on:input={(e) => {
|
||||
e.target.style.height = '';
|
||||
e.target.style.height = `${e.target.scrollHeight}px`;
|
||||
<div
|
||||
class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-ol:p-0 prose-li:-mb-4 whitespace-pre-line"
|
||||
>
|
||||
<div>
|
||||
{#if edit === true}
|
||||
<div class="w-full bg-gray-50 dark:bg-gray-800 rounded-3xl px-5 py-3 my-2">
|
||||
<textarea
|
||||
id="message-edit-{message.id}"
|
||||
bind:this={editTextAreaElement}
|
||||
class=" bg-transparent outline-none w-full resize-none"
|
||||
bind:value={editedContent}
|
||||
on:input={(e) => {
|
||||
e.target.style.height = '';
|
||||
e.target.style.height = `${e.target.scrollHeight}px`;
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class=" mt-2 mb-1 flex justify-end space-x-1.5 text-sm font-medium">
|
||||
<button
|
||||
id="close-edit-message-button"
|
||||
class=" px-4 py-2 bg-gray-900 hover:bg-gray-850 text-gray-100 transition rounded-3xl"
|
||||
on:click={() => {
|
||||
cancelEditMessage();
|
||||
}}
|
||||
/>
|
||||
>
|
||||
{$i18n.t('Cancel')}
|
||||
</button>
|
||||
|
||||
<div class=" mt-2 mb-1 flex justify-center space-x-2 text-sm font-medium">
|
||||
<button
|
||||
class="px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg-lg"
|
||||
on:click={() => {
|
||||
editMessageConfirmHandler();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Save')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class=" px-4 py-2 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-100 transition outline outline-1 outline-gray-200 dark:outline-gray-600 rounded-lg"
|
||||
on:click={() => {
|
||||
cancelEditMessage();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Cancel')}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
id="save-edit-message-button"
|
||||
class="px-4 py-2 bg-white hover:bg-gray-100 text-gray-800 transition rounded-3xl"
|
||||
on:click={() => {
|
||||
editMessageConfirmHandler();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="w-full">
|
||||
{#if message?.error === true}
|
||||
<div
|
||||
class="flex mt-2 mb-4 space-x-2 border px-4 py-3 border-red-800 bg-red-800/30 font-medium rounded-lg"
|
||||
</div>
|
||||
{:else}
|
||||
<div class="w-full">
|
||||
{#if message?.error === true}
|
||||
<div
|
||||
class="flex mt-2 mb-4 space-x-2 border px-4 py-3 border-red-800 bg-red-800/30 font-medium rounded-lg"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-5 h-5 self-center"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-5 h-5 self-center"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
|
||||
/>
|
||||
</svg>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<div class=" self-center">
|
||||
{message.content}
|
||||
</div>
|
||||
<div class=" self-center">
|
||||
{message.content}
|
||||
</div>
|
||||
{:else}
|
||||
{#each tokens as token}
|
||||
{#if token.type === 'code'}
|
||||
<CodeBlock
|
||||
lang={token.lang}
|
||||
code={revertSanitizedResponseContent(token.text)}
|
||||
/>
|
||||
{:else}
|
||||
{@html marked.parse(token.raw, {
|
||||
...defaults,
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
renderer
|
||||
})}
|
||||
{/if}
|
||||
{/each}
|
||||
<!-- {@html marked(message.content.replaceAll('\\', '\\\\'))} -->
|
||||
{/if}
|
||||
</div>
|
||||
{:else if message.content === ''}
|
||||
<Skeleton />
|
||||
{:else}
|
||||
{#each tokens as token, tokenIdx}
|
||||
{#if token.type === 'code'}
|
||||
<CodeBlock
|
||||
id={`${message.id}-${tokenIdx}`}
|
||||
lang={token.lang}
|
||||
code={revertSanitizedResponseContent(token.text)}
|
||||
/>
|
||||
{:else}
|
||||
{@html marked.parse(token.raw, {
|
||||
...defaults,
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
renderer
|
||||
})}
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if message.done}
|
||||
<div
|
||||
class=" flex justify-start space-x-1 overflow-x-auto buttons text-gray-700 dark:text-gray-500"
|
||||
>
|
||||
{#if siblings.length > 1}
|
||||
<div class="flex self-center min-w-fit">
|
||||
<button
|
||||
class="self-center dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
showPreviousMessage(message);
|
||||
}}
|
||||
>
|
||||
<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="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{#if message.citations}
|
||||
<div class="mt-1 mb-2 w-full flex gap-1 items-center">
|
||||
{#each message.citations.reduce((acc, citation) => {
|
||||
citation.document.forEach((document, index) => {
|
||||
const metadata = citation.metadata?.[index];
|
||||
const id = metadata?.source ?? 'N/A';
|
||||
|
||||
<div class="text-xs font-bold self-center min-w-fit dark:text-gray-100">
|
||||
{siblings.indexOf(message.id) + 1} / {siblings.length}
|
||||
const existingSource = acc.find((item) => item.id === id);
|
||||
|
||||
if (existingSource) {
|
||||
existingSource.document.push(document);
|
||||
existingSource.metadata.push(metadata);
|
||||
} else {
|
||||
acc.push( { id: id, source: citation?.source, document: [document], metadata: metadata ? [metadata] : [] } );
|
||||
}
|
||||
});
|
||||
return acc;
|
||||
}, []) as citation, idx}
|
||||
<div class="flex gap-1 text-xs font-semibold">
|
||||
<button
|
||||
class="flex dark:text-gray-300 py-1 px-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-xl"
|
||||
on:click={() => {
|
||||
showCitationModal = true;
|
||||
selectedCitation = citation;
|
||||
}}
|
||||
>
|
||||
<div class="bg-white dark:bg-gray-700 rounded-full size-4">
|
||||
{idx + 1}
|
||||
</div>
|
||||
<div class="flex-1 mx-2 line-clamp-1">
|
||||
{citation.source.name}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="self-center dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
showNextMessage(message);
|
||||
}}
|
||||
{#if message.done || siblings.length > 1}
|
||||
<div
|
||||
class=" flex justify-start overflow-x-auto buttons text-gray-600 dark:text-gray-500"
|
||||
>
|
||||
{#if siblings.length > 1}
|
||||
<div class="flex 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"
|
||||
on:click={() => {
|
||||
showPreviousMessage(message);
|
||||
}}
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<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="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15.75 19.5 8.25 12l7.5-7.5"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="text-sm tracking-widest font-semibold self-center dark:text-gray-100 min-w-fit"
|
||||
>
|
||||
{siblings.indexOf(message.id) + 1}/{siblings.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"
|
||||
on:click={() => {
|
||||
showNextMessage(message);
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
{/if}
|
||||
|
||||
{#if message.done}
|
||||
{#if !readOnly}
|
||||
<Tooltip content="Edit" placement="bottom">
|
||||
<Tooltip content={$i18n.t('Edit')} placement="bottom">
|
||||
<button
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
editMessageHandler();
|
||||
}}
|
||||
@@ -505,7 +567,7 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke-width="2.3"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
@@ -519,11 +581,11 @@
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<Tooltip content="Copy" placement="bottom">
|
||||
<Tooltip content={$i18n.t('Copy')} placement="bottom">
|
||||
<button
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition copy-response-button"
|
||||
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition copy-response-button"
|
||||
on:click={() => {
|
||||
copyToClipboard(message.content);
|
||||
}}
|
||||
@@ -532,7 +594,7 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke-width="2.3"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
@@ -545,83 +607,12 @@
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
{#if !readOnly}
|
||||
<Tooltip content="Good Response" placement="bottom">
|
||||
<button
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded {message?.annotation
|
||||
?.rating === 1
|
||||
? 'bg-gray-100 dark:bg-gray-800'
|
||||
: ''} dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
rateMessage(message.id, 1);
|
||||
showRateComment = true;
|
||||
|
||||
window.setTimeout(() => {
|
||||
document
|
||||
.getElementById(`message-feedback-${message.id}`)
|
||||
?.scrollIntoView();
|
||||
}, 0);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="w-4 h-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
|
||||
/></svg
|
||||
>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Bad Response" placement="bottom">
|
||||
<button
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded {message?.annotation
|
||||
?.rating === -1
|
||||
? 'bg-gray-100 dark:bg-gray-800'
|
||||
: ''} dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
rateMessage(message.id, -1);
|
||||
showRateComment = true;
|
||||
window.setTimeout(() => {
|
||||
document
|
||||
.getElementById(`message-feedback-${message.id}`)
|
||||
?.scrollIntoView();
|
||||
}, 0);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="w-4 h-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
|
||||
/></svg
|
||||
>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<Tooltip content="Read Aloud" placement="bottom">
|
||||
<Tooltip content={$i18n.t('Read Aloud')} placement="bottom">
|
||||
<button
|
||||
id="speak-button-{message.id}"
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
if (!loadingSpeech) {
|
||||
toggleSpeakMessage(message);
|
||||
@@ -668,7 +659,7 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke-width="2.3"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
@@ -683,7 +674,7 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke-width="2.3"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
@@ -702,7 +693,7 @@
|
||||
<button
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
if (!generatingImage) {
|
||||
generateImage(message);
|
||||
@@ -749,7 +740,7 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke-width="2.3"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
@@ -765,11 +756,11 @@
|
||||
{/if}
|
||||
|
||||
{#if message.info}
|
||||
<Tooltip content="Generation Info" placement="bottom">
|
||||
<Tooltip content={$i18n.t('Generation Info')} placement="bottom">
|
||||
<button
|
||||
class=" {isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition whitespace-pre-wrap"
|
||||
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition whitespace-pre-wrap"
|
||||
on:click={() => {
|
||||
console.log(message);
|
||||
}}
|
||||
@@ -779,7 +770,7 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke-width="2.3"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
@@ -793,13 +784,84 @@
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
{#if !readOnly}
|
||||
<Tooltip content={$i18n.t('Good Response')} placement="bottom">
|
||||
<button
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg {message
|
||||
?.annotation?.rating === 1
|
||||
? 'bg-gray-100 dark:bg-gray-800'
|
||||
: ''} dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
rateMessage(message.id, 1);
|
||||
showRateComment = true;
|
||||
|
||||
window.setTimeout(() => {
|
||||
document
|
||||
.getElementById(`message-feedback-${message.id}`)
|
||||
?.scrollIntoView();
|
||||
}, 0);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-width="2.3"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="w-4 h-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
|
||||
/></svg
|
||||
>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={$i18n.t('Bad Response')} placement="bottom">
|
||||
<button
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg {message
|
||||
?.annotation?.rating === -1
|
||||
? 'bg-gray-100 dark:bg-gray-800'
|
||||
: ''} dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
rateMessage(message.id, -1);
|
||||
showRateComment = true;
|
||||
window.setTimeout(() => {
|
||||
document
|
||||
.getElementById(`message-feedback-${message.id}`)
|
||||
?.scrollIntoView();
|
||||
}, 0);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-width="2.3"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="w-4 h-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
|
||||
/></svg
|
||||
>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
{#if isLastMessage && !readOnly}
|
||||
<Tooltip content="Continue Response" placement="bottom">
|
||||
<Tooltip content={$i18n.t('Continue Response')} placement="bottom">
|
||||
<button
|
||||
type="button"
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button"
|
||||
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
|
||||
on:click={() => {
|
||||
continueGeneration();
|
||||
}}
|
||||
@@ -808,7 +870,7 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke-width="2.3"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
@@ -826,19 +888,21 @@
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Regenerate" placement="bottom">
|
||||
<Tooltip content={$i18n.t('Regenerate')} placement="bottom">
|
||||
<button
|
||||
type="button"
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button"
|
||||
on:click={regenerateResponse}
|
||||
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
|
||||
on:click={() => {
|
||||
regenerateResponse(message);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke-width="2.3"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
@@ -851,24 +915,24 @@
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showRateComment}
|
||||
<RateComment
|
||||
messageId={message.id}
|
||||
bind:show={showRateComment}
|
||||
bind:message
|
||||
on:submit={() => {
|
||||
updateChatMessages();
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if message.done && showRateComment}
|
||||
<RateComment
|
||||
messageId={message.id}
|
||||
bind:show={showRateComment}
|
||||
bind:message
|
||||
on:submit={() => {
|
||||
updateChatMessages();
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/key}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="w-full mt-3">
|
||||
<div class="w-full mt-3 mb-4">
|
||||
<div class="animate-pulse flex w-full">
|
||||
<div class="space-y-2 w-full">
|
||||
<div class="h-2 bg-gray-200 dark:bg-gray-600 rounded mr-14" />
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
import { modelfiles, settings } from '$lib/stores';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
||||
import { user as _user } from '$lib/stores';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -54,49 +56,55 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class=" flex w-full">
|
||||
<ProfileImage
|
||||
src={message.user
|
||||
? $modelfiles.find((modelfile) => modelfile.tagName === message.user)?.imageUrl ?? '/user.png'
|
||||
: user?.profile_image_url ?? '/user.png'}
|
||||
/>
|
||||
|
||||
<div class="w-full overflow-hidden">
|
||||
<div class="user-message">
|
||||
<Name>
|
||||
{#if message.user}
|
||||
{#if $modelfiles.map((modelfile) => modelfile.tagName).includes(message.user)}
|
||||
{$modelfiles.find((modelfile) => modelfile.tagName === message.user)?.title}
|
||||
<div class=" flex w-full user-message" dir={$settings.chatDirection}>
|
||||
{#if !($settings?.chatBubble ?? true)}
|
||||
<ProfileImage
|
||||
src={message.user
|
||||
? $modelfiles.find((modelfile) => modelfile.tagName === message.user)?.imageUrl ??
|
||||
'/user.png'
|
||||
: user?.profile_image_url ?? '/user.png'}
|
||||
/>
|
||||
{/if}
|
||||
<div class="w-full overflow-hidden pl-1">
|
||||
{#if !($settings?.chatBubble ?? true)}
|
||||
<div>
|
||||
<Name>
|
||||
{#if message.user}
|
||||
{#if $modelfiles.map((modelfile) => modelfile.tagName).includes(message.user)}
|
||||
{$modelfiles.find((modelfile) => modelfile.tagName === message.user)?.title}
|
||||
{:else}
|
||||
{$i18n.t('You')}
|
||||
<span class=" text-gray-500 text-sm font-medium">{message?.user ?? ''}</span>
|
||||
{/if}
|
||||
{:else if $settings.showUsername || $_user.name !== user.name}
|
||||
{user.name}
|
||||
{:else}
|
||||
{$i18n.t('You')}
|
||||
<span class=" text-gray-500 text-sm font-medium">{message?.user ?? ''}</span>
|
||||
{/if}
|
||||
{:else if $settings.showUsername}
|
||||
{user.name}
|
||||
{:else}
|
||||
{$i18n.t('You')}
|
||||
{/if}
|
||||
|
||||
{#if message.timestamp}
|
||||
<span class=" invisible group-hover:visible text-gray-400 text-xs font-medium">
|
||||
{dayjs(message.timestamp * 1000).format($i18n.t('DD/MM/YYYY HH:mm'))}
|
||||
</span>
|
||||
{/if}
|
||||
</Name>
|
||||
</div>
|
||||
{#if message.timestamp}
|
||||
<span
|
||||
class=" invisible group-hover:visible text-gray-400 text-xs font-medium uppercase"
|
||||
>
|
||||
{dayjs(message.timestamp * 1000).format($i18n.t('h:mm a'))}
|
||||
</span>
|
||||
{/if}
|
||||
</Name>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-p:my-0 prose-p:-mb-4 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-6 prose-li:-mb-4 whitespace-pre-line"
|
||||
class="prose chat-{message.role} w-full max-w-full flex flex-col justify-end dark:prose-invert prose-headings:my-0 prose-p:my-0 prose-p:-mb-4 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-6 prose-li:-mb-4 whitespace-pre-line"
|
||||
>
|
||||
{#if message.files}
|
||||
<div class="my-2.5 w-full flex overflow-x-auto gap-2 flex-wrap">
|
||||
<div class="mt-2.5 mb-1 w-full flex flex-col justify-end overflow-x-auto gap-1 flex-wrap">
|
||||
{#each message.files as file}
|
||||
<div>
|
||||
<div class={$settings?.chatBubble ?? true ? 'self-end' : ''}>
|
||||
{#if file.type === 'image'}
|
||||
<img src={file.url} alt="input" class=" max-h-96 rounded-lg" draggable="false" />
|
||||
{:else if file.type === 'doc'}
|
||||
<button
|
||||
class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none text-left"
|
||||
class="h-16 w-72 flex items-center space-x-3 px-2.5 dark:bg-gray-850 rounded-xl border border-gray-200 dark:border-none text-left"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
if (file?.url) {
|
||||
@@ -132,7 +140,7 @@
|
||||
</button>
|
||||
{:else if file.type === 'collection'}
|
||||
<button
|
||||
class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none text-left"
|
||||
class="h-16 w-72 flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none text-left"
|
||||
type="button"
|
||||
>
|
||||
<div class="p-2.5 bg-red-400 text-white rounded-lg">
|
||||
@@ -166,7 +174,7 @@
|
||||
{/if}
|
||||
|
||||
{#if edit === true}
|
||||
<div class=" w-full">
|
||||
<div class=" w-full bg-gray-50 dark:bg-gray-800 rounded-3xl px-5 py-3 mb-2">
|
||||
<textarea
|
||||
id="message-edit-{message.id}"
|
||||
bind:this={messageEditTextAreaElement}
|
||||
@@ -190,85 +198,104 @@
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class=" mt-2 mb-1 flex justify-center space-x-2 text-sm font-medium">
|
||||
<button
|
||||
id="save-edit-message-button"
|
||||
class="px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg-lg"
|
||||
on:click={() => {
|
||||
editMessageConfirmHandler();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Save & Submit')}
|
||||
</button>
|
||||
|
||||
<div class=" mt-2 mb-1 flex justify-end space-x-1.5 text-sm font-medium">
|
||||
<button
|
||||
id="close-edit-message-button"
|
||||
class=" px-4 py-2 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-100 transition outline outline-1 outline-gray-200 dark:outline-gray-600 rounded-lg"
|
||||
class=" px-4 py-2 bg-gray-900 hover:bg-gray-850 text-gray-100 transition rounded-3xl"
|
||||
on:click={() => {
|
||||
cancelEditMessage();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Cancel')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
id="save-edit-message-button"
|
||||
class="px-4 py-2 bg-white hover:bg-gray-100 text-gray-800 transition rounded-3xl"
|
||||
on:click={() => {
|
||||
editMessageConfirmHandler();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Send')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="w-full">
|
||||
<pre id="user-message">{message.content}</pre>
|
||||
<div class="flex {$settings?.chatBubble ?? true ? 'justify-end' : ''} mb-2">
|
||||
<div
|
||||
class="rounded-3xl {$settings?.chatBubble ?? true
|
||||
? `max-w-[90%] px-5 py-2 bg-gray-50 dark:bg-gray-850 ${
|
||||
message.files ? 'rounded-tr-lg' : ''
|
||||
}`
|
||||
: ''} "
|
||||
>
|
||||
<pre id="user-message">{message.content}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" flex justify-start space-x-1 text-gray-700 dark:text-gray-500">
|
||||
{#if siblings.length > 1}
|
||||
<div class="flex self-center">
|
||||
<button
|
||||
class="self-center dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
showPreviousMessage(message);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
<div
|
||||
class=" flex {$settings?.chatBubble ?? true
|
||||
? 'justify-end'
|
||||
: ''} text-gray-600 dark:text-gray-500"
|
||||
>
|
||||
{#if !($settings?.chatBubble ?? true)}
|
||||
{#if siblings.length > 1}
|
||||
<div class="flex self-center" 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"
|
||||
on:click={() => {
|
||||
showPreviousMessage(message);
|
||||
}}
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<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 font-bold self-center dark:text-gray-100">
|
||||
{siblings.indexOf(message.id) + 1} / {siblings.length}
|
||||
<div class="text-sm tracking-widest font-semibold self-center dark:text-gray-100">
|
||||
{siblings.indexOf(message.id) + 1}/{siblings.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"
|
||||
on:click={() => {
|
||||
showNextMessage(message);
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
|
||||
<button
|
||||
class="self-center dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
showNextMessage(message);
|
||||
}}
|
||||
>
|
||||
<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="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if !readOnly}
|
||||
<Tooltip content="Edit" placement="bottom">
|
||||
<Tooltip content={$i18n.t('Edit')} placement="bottom">
|
||||
<button
|
||||
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button"
|
||||
class="invisible group-hover:visible p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition edit-user-message-button"
|
||||
on:click={() => {
|
||||
editMessageHandler();
|
||||
}}
|
||||
@@ -277,7 +304,7 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke-width="2.3"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
@@ -291,9 +318,9 @@
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<Tooltip content="Copy" placement="bottom">
|
||||
<Tooltip content={$i18n.t('Copy')} placement="bottom">
|
||||
<button
|
||||
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||
class="invisible group-hover:visible p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
copyToClipboard(message.content);
|
||||
}}
|
||||
@@ -302,7 +329,7 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke-width="2.3"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
@@ -316,7 +343,7 @@
|
||||
</Tooltip>
|
||||
|
||||
{#if !isFirstMessage && !readOnly}
|
||||
<Tooltip content="Delete" placement="bottom">
|
||||
<Tooltip content={$i18n.t('Delete')} placement="bottom">
|
||||
<button
|
||||
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
@@ -340,6 +367,60 @@
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
{#if $settings?.chatBubble ?? true}
|
||||
{#if siblings.length > 1}
|
||||
<div class="flex self-center" 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"
|
||||
on:click={() => {
|
||||
showPreviousMessage(message);
|
||||
}}
|
||||
>
|
||||
<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-sm tracking-widest font-semibold self-center dark:text-gray-100">
|
||||
{siblings.indexOf(message.id) + 1}/{siblings.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"
|
||||
on:click={() => {
|
||||
showNextMessage(message);
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Collapsible } from 'bits-ui';
|
||||
|
||||
import { setDefaultModels } from '$lib/apis/configs';
|
||||
import { models, showSettings, settings, user } from '$lib/stores';
|
||||
import { models, showSettings, settings, user, mobile } from '$lib/stores';
|
||||
import { onMount, tick, getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import Selector from './ModelSelector/Selector.svelte';
|
||||
@@ -13,6 +13,8 @@
|
||||
export let selectedModels = [''];
|
||||
export let disabled = false;
|
||||
|
||||
export let showSetDefault = true;
|
||||
|
||||
const saveDefaultModel = async () => {
|
||||
const hasEmptyModel = selectedModels.filter((it) => it === '');
|
||||
if (hasEmptyModel.length) {
|
||||
@@ -36,11 +38,11 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col mt-0.5 w-full">
|
||||
<div class="flex flex-col w-full items-center md:items-start">
|
||||
{#each selectedModels as selectedModel, selectedModelIdx}
|
||||
<div class="flex w-full">
|
||||
<div class="flex w-full max-w-fit">
|
||||
<div class="overflow-hidden w-full">
|
||||
<div class="mr-0.5 max-w-full">
|
||||
<div class="mr-1 max-w-full">
|
||||
<Selector
|
||||
placeholder={$i18n.t('Select a model')}
|
||||
items={$models
|
||||
@@ -57,7 +59,7 @@
|
||||
|
||||
{#if selectedModelIdx === 0}
|
||||
<div class=" self-center mr-2 disabled:text-gray-600 disabled:hover:text-gray-600">
|
||||
<Tooltip content="Add Model">
|
||||
<Tooltip content={$i18n.t('Add Model')}>
|
||||
<button
|
||||
class=" "
|
||||
{disabled}
|
||||
@@ -69,9 +71,9 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
class="size-3.5"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m6-6H6" />
|
||||
</svg>
|
||||
@@ -80,7 +82,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class=" self-center disabled:text-gray-600 disabled:hover:text-gray-600 mr-2">
|
||||
<Tooltip content="Remove Model">
|
||||
<Tooltip content={$i18n.t('Remove Model')}>
|
||||
<button
|
||||
{disabled}
|
||||
on:click={() => {
|
||||
@@ -92,9 +94,9 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
class="size-3.5"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
|
||||
</svg>
|
||||
@@ -106,6 +108,8 @@
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="text-left mt-0.5 ml-1 text-[0.7rem] text-gray-500">
|
||||
<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
|
||||
</div>
|
||||
{#if showSetDefault && !$mobile}
|
||||
<div class="text-left mt-0.5 ml-1 text-[0.7rem] text-gray-500">
|
||||
<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Select } from 'bits-ui';
|
||||
import { DropdownMenu } from 'bits-ui';
|
||||
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
import { cancelOllamaRequest, deleteModel, getOllamaVersion, pullModel } from '$lib/apis/ollama';
|
||||
|
||||
import { user, MODEL_DOWNLOAD_POOL, models } from '$lib/stores';
|
||||
import { user, MODEL_DOWNLOAD_POOL, models, mobile } from '$lib/stores';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { capitalizeFirstLetter, getModels, splitStream } from '$lib/utils';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
@@ -21,15 +21,22 @@
|
||||
export let value = '';
|
||||
export let placeholder = 'Select a model';
|
||||
export let searchEnabled = true;
|
||||
export let searchPlaceholder = 'Search a model';
|
||||
export let searchPlaceholder = $i18n.t('Search a model');
|
||||
|
||||
export let items = [{ value: 'mango', label: 'Mango' }];
|
||||
|
||||
export let className = 'w-[30rem]';
|
||||
|
||||
let show = false;
|
||||
|
||||
let selectedModel = '';
|
||||
$: selectedModel = items.find((item) => item.value === value) ?? '';
|
||||
|
||||
let searchValue = '';
|
||||
let ollamaVersion = null;
|
||||
|
||||
$: filteredItems = searchValue
|
||||
? items.filter((item) => item.value.includes(searchValue.toLowerCase()))
|
||||
? items.filter((item) => item.value.toLowerCase().includes(searchValue.toLowerCase()))
|
||||
: items;
|
||||
|
||||
const pullModelHandler = async () => {
|
||||
@@ -144,7 +151,7 @@
|
||||
|
||||
models.set(await getModels(localStorage.token));
|
||||
} else {
|
||||
toast.error('Download canceled');
|
||||
toast.error($i18n.t('Download canceled'));
|
||||
}
|
||||
|
||||
delete $MODEL_DOWNLOAD_POOL[sanitizedModelTag];
|
||||
@@ -175,27 +182,32 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<Select.Root
|
||||
{items}
|
||||
<DropdownMenu.Root
|
||||
bind:open={show}
|
||||
onOpenChange={async () => {
|
||||
searchValue = '';
|
||||
window.setTimeout(() => document.getElementById('model-search-input')?.focus(), 0);
|
||||
}}
|
||||
selected={items.find((item) => item.value === value) ?? ''}
|
||||
onSelectedChange={(selectedItem) => {
|
||||
value = selectedItem.value;
|
||||
}}
|
||||
>
|
||||
<Select.Trigger class="relative w-full" aria-label={placeholder}>
|
||||
<Select.Value
|
||||
class="flex text-left px-0.5 outline-none bg-transparent truncate text-lg font-semibold placeholder-gray-400 focus:outline-none"
|
||||
{placeholder}
|
||||
/>
|
||||
<ChevronDown className="absolute end-2 top-1/2 -translate-y-[45%] size-3.5" strokeWidth="2.5" />
|
||||
</Select.Trigger>
|
||||
<Select.Content
|
||||
class=" z-40 w-full rounded-lg bg-white dark:bg-gray-900 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/50 outline-none"
|
||||
<DropdownMenu.Trigger class="relative w-full" aria-label={placeholder}>
|
||||
<div
|
||||
class="flex w-full text-left px-0.5 outline-none bg-transparent truncate text-lg font-semibold placeholder-gray-400 focus:outline-none"
|
||||
>
|
||||
{#if selectedModel}
|
||||
{selectedModel.label}
|
||||
{:else}
|
||||
{placeholder}
|
||||
{/if}
|
||||
<ChevronDown className=" self-center ml-2 size-3" strokeWidth="2.5" />
|
||||
</div>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content
|
||||
class=" z-40 {$mobile
|
||||
? `w-full`
|
||||
: `${className}`} max-w-[calc(100vw-1rem)] justify-start rounded-xl bg-white dark:bg-gray-850 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/50 outline-none "
|
||||
transition={flyAndScale}
|
||||
side={$mobile ? 'bottom' : 'bottom-start'}
|
||||
sideOffset={4}
|
||||
>
|
||||
<slot>
|
||||
@@ -208,18 +220,23 @@
|
||||
bind:value={searchValue}
|
||||
class="w-full text-sm bg-transparent outline-none"
|
||||
placeholder={searchPlaceholder}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<hr class="border-gray-100 dark:border-gray-800" />
|
||||
{/if}
|
||||
|
||||
<div class="px-3 my-2 max-h-72 overflow-y-auto">
|
||||
<div class="px-3 my-2 max-h-64 overflow-y-auto scrollbar-hidden">
|
||||
{#each filteredItems as item}
|
||||
<Select.Item
|
||||
class="flex w-full 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-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
|
||||
value={item.value}
|
||||
label={item.label}
|
||||
<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-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
|
||||
on:click={() => {
|
||||
value = item.value;
|
||||
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="line-clamp-1">
|
||||
@@ -287,23 +304,23 @@
|
||||
<Check />
|
||||
</div>
|
||||
{/if}
|
||||
</Select.Item>
|
||||
</button>
|
||||
{:else}
|
||||
<div>
|
||||
<div class="block px-3 py-2 text-sm text-gray-700 dark:text-gray-100">
|
||||
No results found
|
||||
{$i18n.t('No results found')}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if !(searchValue.trim() in $MODEL_DOWNLOAD_POOL) && searchValue && ollamaVersion && $user.role === 'admin'}
|
||||
<button
|
||||
class="flex w-full 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-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
|
||||
class="flex w-full 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-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
|
||||
on:click={() => {
|
||||
pullModelHandler();
|
||||
}}
|
||||
>
|
||||
Pull "{searchValue}" from Ollama.com
|
||||
{$i18n.t(`Pull "{{searchValue}}" from Ollama.com`, { searchValue: searchValue })}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -354,7 +371,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mr-2 translate-y-0.5">
|
||||
<Tooltip content="Cancel">
|
||||
<Tooltip content={$i18n.t('Cancel')}>
|
||||
<button
|
||||
class="text-gray-800 dark:text-gray-100"
|
||||
on:click={() => {
|
||||
@@ -384,6 +401,20 @@
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="hidden w-[42rem]" />
|
||||
<div class="hidden w-[32rem]" />
|
||||
</slot>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
<style>
|
||||
.scrollbar-hidden:active::-webkit-scrollbar-thumb,
|
||||
.scrollbar-hidden:focus::-webkit-scrollbar-thumb,
|
||||
.scrollbar-hidden:hover::-webkit-scrollbar-thumb {
|
||||
visibility: visible;
|
||||
}
|
||||
.scrollbar-hidden::-webkit-scrollbar-thumb {
|
||||
visibility: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full justify-between text-sm">
|
||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[22rem]">
|
||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[25rem]">
|
||||
<input
|
||||
id="profile-image-input"
|
||||
bind:this={profileImageInputElement}
|
||||
@@ -127,7 +127,7 @@
|
||||
|
||||
if (
|
||||
files.length > 0 &&
|
||||
['image/gif', 'image/jpeg', 'image/png'].includes(files[0]['type'])
|
||||
['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(files[0]['type'])
|
||||
) {
|
||||
reader.readAsDataURL(files[0]);
|
||||
}
|
||||
@@ -447,7 +447,7 @@
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<Tooltip content="Create new key">
|
||||
<Tooltip content={$i18n.t('Create new key')}>
|
||||
<button
|
||||
class=" px-1.5 py-1 dark:hover:bg-gray-850transition rounded-lg"
|
||||
on:click={() => {
|
||||
@@ -479,7 +479,7 @@
|
||||
>
|
||||
<Plus strokeWidth="2" className=" size-3.5" />
|
||||
|
||||
Create new secret key</button
|
||||
{$i18n.t('Create new secret key')}</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
{#if keepAlive !== null}
|
||||
<div class="flex mt-1 space-x-2">
|
||||
<input
|
||||
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="text"
|
||||
placeholder={$i18n.t("e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.")}
|
||||
bind:value={keepAlive}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Seed')}</div>
|
||||
<div class=" flex-1 self-center">
|
||||
<input
|
||||
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="number"
|
||||
placeholder="Enter Seed"
|
||||
bind:value={options.seed}
|
||||
@@ -43,7 +43,7 @@
|
||||
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Stop Sequence')}</div>
|
||||
<div class=" flex-1 self-center">
|
||||
<input
|
||||
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Enter stop sequence')}
|
||||
bind:value={options.stop}
|
||||
@@ -492,8 +492,8 @@
|
||||
<input
|
||||
id="steps-range"
|
||||
type="range"
|
||||
min="1"
|
||||
max="16000"
|
||||
min="-1"
|
||||
max="10240000"
|
||||
step="1"
|
||||
bind:value={options.num_ctx}
|
||||
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
|
||||
@@ -504,9 +504,8 @@
|
||||
bind:value={options.num_ctx}
|
||||
type="number"
|
||||
class=" bg-transparent text-center w-14"
|
||||
min="1"
|
||||
max="16000"
|
||||
step="1"
|
||||
min="-1"
|
||||
step="10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
|
||||
let voices = [];
|
||||
let speaker = '';
|
||||
let models = [];
|
||||
let model = '';
|
||||
|
||||
const getOpenAIVoices = () => {
|
||||
voices = [
|
||||
@@ -38,6 +40,10 @@
|
||||
];
|
||||
};
|
||||
|
||||
const getOpenAIVoicesModel = () => {
|
||||
models = [{ name: 'tts-1' }, { name: 'tts-1-hd' }];
|
||||
};
|
||||
|
||||
const getWebAPIVoices = () => {
|
||||
const getVoicesLoop = setInterval(async () => {
|
||||
voices = await speechSynthesis.getVoices();
|
||||
@@ -78,12 +84,16 @@
|
||||
if (TTSEngine === 'openai') {
|
||||
const res = await updateAudioConfig(localStorage.token, {
|
||||
url: OpenAIUrl,
|
||||
key: OpenAIKey
|
||||
key: OpenAIKey,
|
||||
model: model,
|
||||
speaker: speaker
|
||||
});
|
||||
|
||||
if (res) {
|
||||
OpenAIUrl = res.OPENAI_API_BASE_URL;
|
||||
OpenAIKey = res.OPENAI_API_KEY;
|
||||
model = res.OPENAI_API_MODEL;
|
||||
speaker = res.OPENAI_API_VOICE;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -98,9 +108,11 @@
|
||||
STTEngine = settings?.audio?.STTEngine ?? '';
|
||||
TTSEngine = settings?.audio?.TTSEngine ?? '';
|
||||
speaker = settings?.audio?.speaker ?? '';
|
||||
model = settings?.audio?.model ?? '';
|
||||
|
||||
if (TTSEngine === 'openai') {
|
||||
getOpenAIVoices();
|
||||
getOpenAIVoicesModel();
|
||||
} else {
|
||||
getWebAPIVoices();
|
||||
}
|
||||
@@ -111,6 +123,8 @@
|
||||
if (res) {
|
||||
OpenAIUrl = res.OPENAI_API_BASE_URL;
|
||||
OpenAIKey = res.OPENAI_API_KEY;
|
||||
model = res.OPENAI_API_MODEL;
|
||||
speaker = res.OPENAI_API_VOICE;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -126,13 +140,14 @@
|
||||
audio: {
|
||||
STTEngine: STTEngine !== '' ? STTEngine : undefined,
|
||||
TTSEngine: TTSEngine !== '' ? TTSEngine : undefined,
|
||||
speaker: speaker !== '' ? speaker : undefined
|
||||
speaker: speaker !== '' ? speaker : undefined,
|
||||
model: model !== '' ? model : undefined
|
||||
}
|
||||
});
|
||||
dispatch('save');
|
||||
}}
|
||||
>
|
||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[22rem]">
|
||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[25rem]">
|
||||
<div>
|
||||
<div class=" mb-1 text-sm font-medium">{$i18n.t('STT Settings')}</div>
|
||||
|
||||
@@ -215,6 +230,7 @@
|
||||
if (e.target.value === 'openai') {
|
||||
getOpenAIVoices();
|
||||
speaker = 'alloy';
|
||||
model = 'tts-1';
|
||||
} else {
|
||||
getWebAPIVoices();
|
||||
speaker = '';
|
||||
@@ -307,10 +323,29 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Model')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
list="model-list"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={model}
|
||||
placeholder="Select a model"
|
||||
/>
|
||||
|
||||
<datalist id="model-list">
|
||||
{#each models as model}
|
||||
<option value={model.name} />
|
||||
{/each}
|
||||
</datalist>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||
<div class="flex justify-end text-sm font-medium">
|
||||
<button
|
||||
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||
type="submit"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user