Compare commits
720 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bc29d5d3c3 | |||
| fe18aebdd9 | |||
| 57df49274c | |||
| 9193c9bd7d | |||
| 656e75372c | |||
| 6f9080dfe0 | |||
| 86f822fd9a | |||
| 8b3972c6df | |||
| 58ca5e42c2 | |||
| cd62d89dd9 | |||
| 3686cf7365 | |||
| aaa4350eb4 | |||
| 2113c2f57f | |||
| ee22ba9676 | |||
| 466974f344 | |||
| 95616e92d7 | |||
| c8c41e07e9 | |||
| d195b83c9e | |||
| 0bff6d38ee | |||
| b0bf6b5d31 | |||
| 7abdf8e342 | |||
| cfb7357103 | |||
| 05c15b017d | |||
| c8e609c3d1 | |||
| 4623fdfce2 | |||
| 1c1e6a7172 | |||
| 297835628a | |||
| 7b6723a955 | |||
| a40eb87c31 | |||
| 53954e4c05 | |||
| 50b1a3471c | |||
| dc6e68bd60 | |||
| da1e88a427 | |||
| d23f05fe0a | |||
| 90effa925e | |||
| 8a3c9b9a3e | |||
| a7f543d737 | |||
| 4b00036dbf | |||
| babfc97c90 | |||
| 913620ff0c | |||
| e6265897c8 | |||
| 91e1b548a9 | |||
| 8ad44cd690 | |||
| 5f74cfaa51 | |||
| 9ef3fb0bc7 | |||
| 7d1890c7d1 | |||
| eb2aaea6cb | |||
| 9706b76b36 | |||
| 059ac466e0 | |||
| 75780256d1 | |||
| b5efe15f73 | |||
| be657f45e4 | |||
| f295e26f25 | |||
| 0591cd82d4 | |||
| 70ca6527f6 | |||
| cdd024db81 | |||
| 3082d0de7a | |||
| f5e10704c0 | |||
| c90cd6019a | |||
| c26d7ff463 | |||
| b21af908a7 | |||
| 097e90f1de | |||
| ed47e24494 | |||
| aeaf761ecd | |||
| 6d2eaaf602 | |||
| 1815cfe99d | |||
| 539e95a206 | |||
| de59ecf8a3 | |||
| 2981212dfa | |||
| 7ee07df26a | |||
| 008febb6d7 | |||
| 6417ccef00 | |||
| 198749aed1 | |||
| 46bd97c100 | |||
| 1086a0e662 | |||
| 9586749314 | |||
| 54552c3d13 | |||
| ed337b94e1 | |||
| 0f287c8a09 | |||
| dc2e75017d | |||
| c3862bc387 | |||
| 6fdfa62845 | |||
| e0fc3e89a7 | |||
| ed5669410b | |||
| 6d3d80b347 | |||
| d84368a8f6 | |||
| 5adf9ed445 | |||
| 49e9275c5b | |||
| c746fd94cb | |||
| 0bfe28711a | |||
| c09e51c1bf | |||
| 3dd1d1bc4a | |||
| a860e98ab9 | |||
| ed1a2ab5e8 | |||
| bbbd94f69c | |||
| 2e067b0541 | |||
| a0a79fbc5b | |||
| ecd6a135cd | |||
| 9c18cf204d | |||
| a571ae4ec2 | |||
| 1558f64c48 | |||
| 1739313c13 | |||
| 1b8acc42b4 | |||
| 1397bfa84c | |||
| c09af435ac | |||
| f5004fd9d4 | |||
| fb58e842b7 | |||
| dcf8d9c7eb | |||
| a61c54531c | |||
| e184a65dea | |||
| fafeec3d86 | |||
| 55d0ecc879 | |||
| 614f3d5f80 | |||
| 46328333c7 | |||
| 582036fb3f | |||
| 28602b12ea | |||
| f5b6785e53 | |||
| 81440460f2 | |||
| 82ac2e4edb | |||
| fc44924256 | |||
| 9ca2350a13 | |||
| 12a6216477 | |||
| fb5fb27ebe | |||
| cb0f759420 | |||
| 378223aedb | |||
| 61b147441c | |||
| 1f9b5b6456 | |||
| 4ca870bf6d | |||
| 52a3ab5333 | |||
| ff22e2156d | |||
| e0c775f6e3 | |||
| 7036166535 | |||
| 2e231982a3 | |||
| fe7e7abf15 | |||
| 3e7a163660 | |||
| 0319e63999 | |||
| 0bd88090bb | |||
| ed3e1397ca | |||
| 0ad35ffad9 | |||
| 1f488a0072 | |||
| 493745a70b | |||
| c400f40663 | |||
| bc6113f4ba | |||
| b6703be859 | |||
| 62f30ce098 | |||
| 9f812e7022 | |||
| a909aa1c20 | |||
| e3889522d6 | |||
| 79c005a041 | |||
| a6c797d4c2 | |||
| 0e50c8bb31 | |||
| 9ad5ffb8c1 | |||
| f052010d8e | |||
| ee6e41b144 | |||
| 2e267b420a | |||
| 0c618b8145 | |||
| 8f41db2f2e | |||
| a53537ccde | |||
| 5017ca90ff | |||
| f3ee07a8a2 | |||
| 263cc71dd3 | |||
| 8f5e426a13 | |||
| 87700d02a7 | |||
| 8f22e911e0 | |||
| 1647cbef21 | |||
| 528ac51ba9 | |||
| 650ca95784 | |||
| d1b8af6220 | |||
| 332ef045ee | |||
| 0876c9b5ef | |||
| ebc7da6f82 | |||
| 1fe1c27220 | |||
| 8013c152d0 | |||
| 630a78cead | |||
| 17c772831d | |||
| 6147f41589 | |||
| 5e70afc054 | |||
| 75fae69def | |||
| 90ef9f751a | |||
| 544251a7fd | |||
| 05970157f6 | |||
| d834bd2a18 | |||
| e9b68524e8 | |||
| b291271df3 | |||
| 9dd76b72b4 | |||
| 12977e07f3 | |||
| 29467a7057 | |||
| b862dff185 | |||
| 6747478f67 | |||
| 124a17e826 | |||
| 57360b7a61 | |||
| 2d40efe9d6 | |||
| 2fc07fd6a2 | |||
| 78413d0c2e | |||
| 1c01e52f7c | |||
| 15a3c6b171 | |||
| d394f8b7be | |||
| 325ca98773 | |||
| 351b1dbf31 | |||
| a2eadb30f5 | |||
| 2e7e346e19 | |||
| 7c145ce5d5 | |||
| f19b61307d | |||
| a8ec73d01e | |||
| 318f61161e | |||
| 1a26e67611 | |||
| 6974cb248c | |||
| dd5da10d2a | |||
| 9c4b55c86a | |||
| 4aca1e86ad | |||
| b2f09e4623 | |||
| bdb2ce9448 | |||
| 973c08babd | |||
| 7f51ef1838 | |||
| 08969ecf89 | |||
| 3e012f0219 | |||
| 5933d7a216 | |||
| 1b7d363d32 | |||
| c2732a0990 | |||
| c5eb0a9732 | |||
| bf57dd808e | |||
| d353ea449a | |||
| c85cdbde46 | |||
| fb083237cd | |||
| a0fb4a9b84 | |||
| 5c9dd25459 | |||
| 0907c32e10 | |||
| 4752df9bd8 | |||
| 7b9b29253f | |||
| 126aff7a7d | |||
| 6ca1de3601 | |||
| fb8af637e1 | |||
| ab983887cc | |||
| 75c1f47500 | |||
| 3da981d9cd | |||
| 8a0e30a901 | |||
| 736cbb961f | |||
| 8d8c2752dc | |||
| d78a7d431d | |||
| 91ca5c424b | |||
| 7e1c55d2ed | |||
| 3a2c241450 | |||
| 13b6b43cea | |||
| c7a0e45bea | |||
| 6bff5a4d09 | |||
| 37e0d47082 | |||
| e6b91036e1 | |||
| 79a83adc89 | |||
| ffd598c5d7 | |||
| d255251e5f | |||
| 49fe04a627 | |||
| 21c919988d | |||
| 1c4b6b9cd9 | |||
| 3899405864 | |||
| 209828c7c3 | |||
| 7152af949b | |||
| 8206c47a47 | |||
| f7aba20d79 | |||
| 6afc686e17 | |||
| 677c36c3aa | |||
| 6d764ee55e | |||
| 1d8b3b8c51 | |||
| 92dd173b27 | |||
| f2ec020b64 | |||
| d784d5c367 | |||
| b3517c63e8 | |||
| 550075bba4 | |||
| c93a10388b | |||
| 5a168ecc2a | |||
| 276ce3374d | |||
| e77c3ab043 | |||
| d2e2e535dd | |||
| 90ec458c4c | |||
| 9636913de0 | |||
| e039b4ec54 | |||
| 9d2ed3d2be | |||
| b8b994a820 | |||
| 00eb022450 | |||
| 2428878f42 | |||
| af57a2c153 | |||
| 1b349016ff | |||
| c1b4fbf5c2 | |||
| a52e8cd537 | |||
| 5b7cf88915 | |||
| e1103305f5 | |||
| 6e9db3e3c8 | |||
| eab30781e0 | |||
| c30c876659 | |||
| 4ead3c5b80 | |||
| f0f176b80f | |||
| 5b3ee30ca9 | |||
| e13614e11b | |||
| 464b6a329e | |||
| 44d768ecf3 | |||
| 0bd9d59c78 | |||
| be74a4c9c1 | |||
| 719f4da1dc | |||
| fbc9634ffc | |||
| 619c81472b | |||
| d8f71e1d7f | |||
| 1715446b13 | |||
| 8204f06485 | |||
| 4c92a0f571 | |||
| 1d225dd804 | |||
| 15bd5ebd7b | |||
| 3af50f08bd | |||
| 4fe1f2487d | |||
| a4bc0b2829 | |||
| 7b8f923981 | |||
| be41994d40 | |||
| 5361896411 | |||
| c754c53906 | |||
| 78d6647885 | |||
| 4eeb669ac3 | |||
| 26465f3e92 | |||
| 9d25207b83 | |||
| cc19b8049a | |||
| 3a163b6392 | |||
| 405d0561df | |||
| ee33b4e2a3 | |||
| 888479aaf0 | |||
| 82cda6e522 | |||
| 119a7f1933 | |||
| 6c4445d545 | |||
| 92b1acd6fb | |||
| 1767b64135 | |||
| e030f261d1 | |||
| d501ece247 | |||
| 534e4c90ca | |||
| 40f2c3521b | |||
| 07b1327708 | |||
| 525095b3de | |||
| f84513e856 | |||
| bf423b8577 | |||
| ba20c71963 | |||
| 7bbc57f225 | |||
| e703e172e2 | |||
| 3ff52fd1ad | |||
| e19406cdd7 | |||
| 30e65b33f6 | |||
| 3f1255b39e | |||
| 71743b25fe | |||
| c7e93b32c5 | |||
| 3cee507687 | |||
| ff651ddc36 | |||
| c0738cef26 | |||
| a44e9a8dda | |||
| 38b9a63fa5 | |||
| 2d60e42258 | |||
| 60ac69eb27 | |||
| 504d910557 | |||
| e48d66f918 | |||
| 0bfbace9aa | |||
| 019cf8199f | |||
| c5f85eed92 | |||
| 21719ccdf1 | |||
| 299b3d72cf | |||
| 85e9e231ed | |||
| f382a78e31 | |||
| 377cc427b6 | |||
| 7ec72679f0 | |||
| 31b311c3c9 | |||
| e24ab4c6d2 | |||
| 6739983cf1 | |||
| ff00815b61 | |||
| 8f6a927be3 | |||
| f6add92702 | |||
| deedfdceae | |||
| e268ee5675 | |||
| 33d8d818bd | |||
| 9a81a37008 | |||
| 8e620b0c2c | |||
| 214546399a | |||
| fdd27aa321 | |||
| 4ebff2c5ce | |||
| 8266d343bf | |||
| 36ddb19023 | |||
| ebc410d8d4 | |||
| 1772db4712 | |||
| 40d7d7d6dd | |||
| 0d01ea5f2f | |||
| 822c47c171 | |||
| fdf02c4e86 | |||
| 398bc96b1a | |||
| ad82eae6a9 | |||
| d7b64ff447 | |||
| 93cb2be35d | |||
| 8f1b9bdf8a | |||
| f69956bda8 | |||
| e06667ead8 | |||
| 35f64cc53f | |||
| 12f95555fc | |||
| 84c1810b6e | |||
| ba39f9bf56 | |||
| 8eb82265d0 | |||
| ac277e8e9e | |||
| b1521cacad | |||
| 5978e7c9a6 | |||
| fd5e8b4fcf | |||
| 291b6dd744 | |||
| 73ff524a8f | |||
| c2b5bf2130 | |||
| 8532f9da03 | |||
| 2cfe6830df | |||
| 44355a4bdc | |||
| 47a858393b | |||
| d3e80f515d | |||
| c5b93ca631 | |||
| 3335eee1b9 | |||
| 29175405a6 | |||
| d53062a9b0 | |||
| 96ef15362a | |||
| d6d6098378 | |||
| 620e629edc | |||
| c292fd89f9 | |||
| c346130774 | |||
| 768717aaf9 | |||
| d055e1f888 | |||
| 5f84145a2d | |||
| 47a9395a22 | |||
| 8dbac0f7e3 | |||
| 6b463164f4 | |||
| 5f15e9ee68 | |||
| 54d63ece6f | |||
| e35883ca9c | |||
| 19e49e43cb | |||
| e429b5548c | |||
| 839dacc4a4 | |||
| 4225591a26 | |||
| b4adffc3af | |||
| 8fc178ae91 | |||
| 580ad46036 | |||
| f47dffe6e1 | |||
| 00f6b4bf09 | |||
| 9126ceac08 | |||
| ff8a2da751 | |||
| 2f028b45fe | |||
| d3ef9d980b | |||
| cb81dfe4ba | |||
| 426de76690 | |||
| 16a767e04e | |||
| 2adaf9ba3d | |||
| f381850bb2 | |||
| 273787fe78 | |||
| 70d16c3904 | |||
| ee6b1376c3 | |||
| aed8e2156f | |||
| 5d848ad130 | |||
| 98928f6bd7 | |||
| 692f04d457 | |||
| e1ea0c23eb | |||
| 657d443a3e | |||
| 95985e7bbb | |||
| 3a0a1aca11 | |||
| 41926172d3 | |||
| 585b9eb84a | |||
| ffd7d74f77 | |||
| 578d52b89d | |||
| e599d5db3c | |||
| 79b5430a9e | |||
| 9b05fe3c54 | |||
| 5fe2795db8 | |||
| 732f730213 | |||
| c89bb01db9 | |||
| 1cbbf75807 | |||
| bd81fc8bff | |||
| d4df552076 | |||
| b7ad82757d | |||
| d12b6cda4e | |||
| 83855b713b | |||
| bd09b6dbad | |||
| eb9ad47ef8 | |||
| 5b46a252ff | |||
| 39c57c0e94 | |||
| 619dbbe9f5 | |||
| 9be73ea94a | |||
| 4e43663448 | |||
| cd117f5b67 | |||
| 2dad9b9432 | |||
| b0bc36f2af | |||
| 1688f5ecaf | |||
| cba2d31175 | |||
| a2b77fd072 | |||
| 9b83e57372 | |||
| 687cae9b79 | |||
| f8fffdd288 | |||
| 60d6279055 | |||
| 7b330d1490 | |||
| 7078af635c | |||
| e1b57d80a4 | |||
| 8426874426 | |||
| 70dd790afc | |||
| e99cba53fe | |||
| 628d7ae72d | |||
| ff737a9e25 | |||
| 5c16631ec5 | |||
| d52eaf19d6 | |||
| 27dd6ef14e | |||
| ed75f72358 | |||
| 276d629a14 | |||
| aedd77b81d | |||
| 1053863175 | |||
| f448341211 | |||
| 9fc6b999d0 | |||
| ff2fff857a | |||
| dd4cf102cc | |||
| 5d3a89dd25 | |||
| b4f1a0b5a6 | |||
| afa42dd2e4 | |||
| be44af4680 | |||
| e6b6f42139 | |||
| 4d9677e808 | |||
| eacb69074e | |||
| 6477bf37fe | |||
| 1743d3c6c1 | |||
| e723b2a4c6 | |||
| 67f704c98d | |||
| 8d92093570 | |||
| 1ccac9111b | |||
| 98984166f9 | |||
| d1bf18eeb0 | |||
| 2018a6c000 | |||
| 56152230f8 | |||
| 7e8cf5504a | |||
| 3c03d5069d | |||
| 705508a674 | |||
| 771482c6e6 | |||
| d25ccfba5f | |||
| 614c219010 | |||
| 63b3076d64 | |||
| 8e3ad45ce4 | |||
| 984e0c533e | |||
| 5065291f72 | |||
| 8c273ba58a | |||
| 7a9c0946a4 | |||
| 7e0a26ef4e | |||
| 65b7c9898b | |||
| 67f95ddfdc | |||
| d1dbb9a3be | |||
| bb087a5989 | |||
| a0d24105fc | |||
| a5c9160b7d | |||
| 0e2cdbe9fb | |||
| 3c55a46484 | |||
| d6b68f405e | |||
| 351bbdb36c | |||
| 896baf021b | |||
| 499e5e4f60 | |||
| 3ad003bccb | |||
| af92184c93 | |||
| 06debb322b | |||
| b7f0759485 | |||
| bc6f23f82f | |||
| cb9e76c7f9 | |||
| b38986a0aa | |||
| db0c576f48 | |||
| 40d7e5089d | |||
| 9b8d42c670 | |||
| bed3b71860 | |||
| 253791b92c | |||
| 5dd6ae6ec4 | |||
| 902f30c123 | |||
| fa8d7bd9c6 | |||
| d0df2cbe53 | |||
| 27baa00afb | |||
| 869063c743 | |||
| 2f9f568dd9 | |||
| 937ce5f797 | |||
| 1b612209a3 | |||
| d00d3341af | |||
| 3199e26500 | |||
| bc06e7a282 | |||
| 1eec9e2b59 | |||
| d46f652f7e | |||
| d75d638b9a | |||
| b64c9d966a | |||
| 8a928e5356 | |||
| adf958559b | |||
| 82b35492af | |||
| a7ea07036e | |||
| c1d3481c41 | |||
| bc0baa35e6 | |||
| f549cb1f87 | |||
| 823093eea6 | |||
| 939bfd153e | |||
| b943b7d337 | |||
| a1f3ece528 | |||
| 8a4b3e6bc9 | |||
| a9c497612b | |||
| 7df997c992 | |||
| bebc0d2073 | |||
| 857fc3ce99 | |||
| 47e9c12fc2 | |||
| 5970dadead | |||
| 7dc4cb30b2 | |||
| fb122e67d0 | |||
| ca9874757f | |||
| 65d5545cf0 | |||
| e872f5dc78 | |||
| b4ad64586a | |||
| 9661fee554 | |||
| adf26789b8 | |||
| 209e246e6f | |||
| ed2a1e7db9 | |||
| 143ac08c35 | |||
| 53f03f6556 | |||
| 62f1933e3c | |||
| 0b30dc357c | |||
| d5b595a842 | |||
| d05ba042c0 | |||
| eed2d735a1 | |||
| 675403d26d | |||
| 8be6e16513 | |||
| c7fc17da69 | |||
| 4775fe43d8 | |||
| 2a04dd0f9b | |||
| 61ee4bd629 | |||
| dbd661a417 | |||
| 26700ac4ac | |||
| b0defe5524 | |||
| 91cd8c7608 | |||
| 62b01c5f8e | |||
| b33ab6c5fd | |||
| 0886b3a0a4 | |||
| 522afbb0a0 | |||
| d5f13dd9e0 | |||
| 3d6d8c91dd | |||
| 4354f270ce | |||
| 28087ccf40 | |||
| eb0e683b47 | |||
| 1023ff8454 | |||
| 9401f6c821 | |||
| 827c419251 | |||
| 82db64a700 | |||
| f1fae805a2 | |||
| 272e2386dd | |||
| 67e1c0a10b | |||
| f88a86f9b0 | |||
| 601982f52b | |||
| fc839011f6 | |||
| b407f24950 | |||
| d15f57cfff | |||
| 82fbfd69a5 | |||
| 8e6ea49e0e | |||
| 83a3e53d8d | |||
| 698976add0 | |||
| cc1d3c48e0 | |||
| e510c8b11f | |||
| 666086a806 | |||
| 9bdbe88bda | |||
| 50db51ebe0 | |||
| 0beaab51ae | |||
| 0e30b0f9b4 | |||
| 214722d39e | |||
| 7f6dae41f0 | |||
| 9aa8eff44e | |||
| 2544f7eaf0 | |||
| 1a6ce1d5d9 | |||
| 98eaec22d4 | |||
| 2e40719f4e | |||
| 71a2bd2fea | |||
| 1c20db775c | |||
| e2ef36b582 | |||
| 90a064972c | |||
| f4a0b845be | |||
| 1f22aa99d8 | |||
| 4c9ea084d5 | |||
| a3094bcd1b | |||
| 5c8fb4b3d5 | |||
| ff46fe2b4a | |||
| 8d6a424604 | |||
| 8dfbdbd883 | |||
| f533c9750b | |||
| 2f841f9f5a | |||
| 02d5bca44d | |||
| 14eda1bf5b | |||
| 2d4cdc5be9 | |||
| d038e831dc | |||
| 9fe62fc80d | |||
| bfb12a7851 | |||
| 4617f3a4e2 | |||
| 97e331632c | |||
| 4cf777ab5a | |||
| 05c0423d6e | |||
| 377efc8a0c | |||
| b35bbaade2 | |||
| c9107fa87f | |||
| 0c1fab09ff | |||
| 8b67959695 | |||
| 21e81d78d9 | |||
| 31623ff330 | |||
| 7edfffdcc5 | |||
| 3b868be77b | |||
| c3271e84ef | |||
| 1cac9fce46 | |||
| d0869bbfbc | |||
| ca4beb413b | |||
| 7b31ef60fb | |||
| 73576dfcaf | |||
| 8a411decac | |||
| fa786c8e05 | |||
| 85fc35492d | |||
| 1c94f4dd71 | |||
| 74169b0320 | |||
| 83574ccf6c | |||
| a0be3822bf | |||
| 004700c125 | |||
| 19ee110b7d | |||
| 998616c0fd | |||
| 9204498420 | |||
| b1957e5cfe | |||
| 92a88df484 | |||
| f2f713023d | |||
| cf86ba7786 | |||
| 94502d6494 | |||
| 5e3f9ec757 | |||
| d6fc0ccf65 | |||
| a988c53949 | |||
| 3afd66d50f | |||
| 0ead3ed37a |
@@ -8,6 +8,10 @@ assignees: ''
|
||||
|
||||
# Bug Report
|
||||
|
||||
**Important: Before submitting a bug report, please check whether a similar issue or feature request has already been posted in the Issues or Discussions section. It's likely we're already tracking it. In case of uncertainty, initiate a discussion post first. This helps us all to efficiently focus on improving the project.**
|
||||
|
||||
**Let's collaborate respectfully. If you bring negativity, please understand our capacity to engage may be limited. If you're open to learning and communicating constructively, we're more than happy to assist you. Remember, Open WebUI is a volunteer-driven project maintained by a single maintainer, supported by our amazing contributors who also manage full-time jobs. We respect your time; please respect ours. If you have an issue, We highly encourage you to submit a pull request or to fork the project. We actively work to prevent contributor burnout to preserve the quality and continuity of Open WebUI.**
|
||||
|
||||
## Installation Method
|
||||
|
||||
[Describe the method you used to install the project, e.g., git clone, Docker, pip, etc.]
|
||||
|
||||
@@ -6,6 +6,12 @@ labels: ''
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
# Feature Request
|
||||
|
||||
**Important: Before submitting a feature request, please check whether a similar issue or feature request has already been posted in the Issues or Discussions section. It's likely we're already tracking it. In case of uncertainty, initiate a discussion post first. This helps us all to efficiently focus on improving the project.**
|
||||
|
||||
**Let's collaborate respectfully. If you bring negativity, please understand our capacity to engage may be limited. If you're open to learning and communicating constructively, we're more than happy to assist you. Remember, Open WebUI is a volunteer-driven project maintained by a single maintainer, supported by our amazing contributors who also manage full-time jobs. We respect your time; please respect ours. If you have an issue, We highly encourage you to submit a pull request or to fork the project. We actively work to prevent contributor burnout to preserve the quality and continuity of Open WebUI.**
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20' # Or specify any other version you want to use
|
||||
node-version: '22' # Or specify any other version you want to use
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: '22'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
# - uses: actions/checkout@v4
|
||||
|
||||
# - name: Set up Python
|
||||
# uses: actions/setup-python@v4
|
||||
# uses: actions/setup-python@v5
|
||||
# with:
|
||||
# python-version: ${{ matrix.python-version }}
|
||||
|
||||
@@ -157,7 +157,7 @@ jobs:
|
||||
GLOBAL_LOG_LEVEL: debug
|
||||
run: |
|
||||
cd backend
|
||||
uvicorn main:app --port "8080" --forwarded-allow-ips '*' &
|
||||
uvicorn open_webui.main:app --port "8080" --forwarded-allow-ips '*' &
|
||||
UVICORN_PID=$!
|
||||
# Wait up to 40 seconds for the server to start
|
||||
for i in {1..40}; do
|
||||
@@ -182,9 +182,12 @@ jobs:
|
||||
WEBUI_SECRET_KEY: secret-key
|
||||
GLOBAL_LOG_LEVEL: debug
|
||||
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
|
||||
DATABASE_POOL_SIZE: 10
|
||||
DATABASE_POOL_MAX_OVERFLOW: 10
|
||||
DATABASE_POOL_TIMEOUT: 30
|
||||
run: |
|
||||
cd backend
|
||||
uvicorn main:app --port "8081" --forwarded-allow-ips '*' &
|
||||
uvicorn open_webui.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
|
||||
@@ -230,7 +233,7 @@ jobs:
|
||||
# DATABASE_URL: mysql://root:mysql@localhost:3306/mysql
|
||||
# run: |
|
||||
# cd backend
|
||||
# uvicorn main:app --port "8083" --forwarded-allow-ips '*' &
|
||||
# uvicorn open_webui.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
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
- name: Use Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
- name: Install dependencies
|
||||
|
||||
+240
@@ -5,6 +5,246 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.3.32] - 2024-10-06
|
||||
|
||||
### Added
|
||||
|
||||
- **🔢 Workspace Enhancements**: Added a display count for models, prompts, tools, and functions in the workspace, providing a clear overview and easier management.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🖥️ Web and YouTube Attachment Fix**: Resolved an issue where attaching web links and YouTube videos was malfunctioning, ensuring seamless integration and display within chats.
|
||||
- **📞 Call Mode Activation on Landing Page**: Fixed a bug where call mode was not operational from the landing page.
|
||||
|
||||
### Changed
|
||||
|
||||
- **🔄 URL Parameter Refinement**: Updated the 'tool_ids' URL parameter to 'tools' or 'tool-ids' for more intuitive and consistent user experience.
|
||||
- **🎨 Floating Buttons Styling Update**: Refactored the styling of floating buttons to intelligently adjust to the left side when there isn't enough room on the right, improving interface usability and aesthetic.
|
||||
- **🔧 Enhanced Accessibility for Floating Buttons**: Implemented the ability to close floating buttons with the 'Esc' key, making workflow smoother and more efficient for users navigating via keyboard.
|
||||
- **🖇️ Updated Information URL**: Information URLs now direct users to a general release page rather than a version-specific URL, ensuring access to the latest and relevant details all in one place.
|
||||
- **📦 Library Dependencies Update**: Upgraded dependencies to ensure compatibility and performance optimization for pip installs.
|
||||
|
||||
## [0.3.31] - 2024-10-06
|
||||
|
||||
### Added
|
||||
|
||||
- **📚 Knowledge Feature**: Reimagined documents feature, now more performant with a better UI for enhanced organization; includes streamlined API integration for Retrieval-Augmented Generation (RAG). Detailed documentation forthcoming: https://docs.openwebui.com/
|
||||
- **🌐 New Landing Page**: Freshly designed landing page; toggle between the new UI and the classic chat UI from Settings > Interface for a personalized experience.
|
||||
- **📁 Full Document Retrieval Mode**: Toggle between full document retrieval or traditional snippets by clicking on the file item. This mode enhances document capabilities and supports comprehensive tasks like summarization by utilizing the entire content instead of RAG.
|
||||
- **📄 Extracted File Content Display**: View extracted content directly by clicking on the file item, simplifying file analysis.
|
||||
- **🎨 Artifacts Feature**: Render web content and SVGs directly in the interface, supporting quick iterations and live changes.
|
||||
- **🖊️ Editable Code Blocks**: Supercharged code blocks now allow live editing directly in the LLM response, with live reloads supported by artifacts.
|
||||
- **🔧 Code Block Enhancements**: Introduced a floating copy button in code blocks to facilitate easier code copying without scrolling.
|
||||
- **🔍 SVG Pan/Zoom**: Enhanced interaction with SVG images, including Mermaid diagrams, via new pan and zoom capabilities.
|
||||
- **🔍 Text Select Quick Actions**: New floating buttons appear when text is highlighted in LLM responses, offering deeper interactions like "Ask a Question" or "Explain".
|
||||
- **🗃️ Database Pool Configuration**: Enhanced database handling to support scalable user growth.
|
||||
- **🔊 Experimental Audio Compression**: Compress audio files to navigate around the 25MB limit for OpenAI's speech-to-text processing.
|
||||
- **🔍 Query Embedding**: Adjusted embedding behavior to enhance system performance by not repeating query embedding.
|
||||
- **💾 Lazy Load Optimizations**: Implemented lazy loading of large dependencies to minimize initial memory usage, boosting performance.
|
||||
- **🍏 Apple Touch Icon Support**: Optimizes the display of icons for web bookmarks on Apple mobile devices.
|
||||
- **🔽 Expandable Content Markdown Support**: Introducing 'details', 'summary' tag support for creating expandable content sections in markdown, facilitating cleaner, organized documentation and interactive content display.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🔘 Action Button Issue**: Resolved a bug where action buttons were not functioning, enhancing UI reliability.
|
||||
- **🔄 Multi-Model Chat Loop**: Fixed an infinite loop issue in multi-model chat environments, ensuring smoother chat operations.
|
||||
- **📄 Chat PDF/TXT Export Issue**: Resolved problems with exporting chat logs to PDF and TXT formats.
|
||||
- **🔊 Call to Text-to-Speech Issues**: Rectified problems with text-to-speech functions to improve audio interactions.
|
||||
|
||||
### Changed
|
||||
|
||||
- **⚙️ Endpoint Renaming**: Renamed 'rag' endpoints to 'retrieval' for clearer function description.
|
||||
- **🎨 Styling and Interface Updates**: Multiple refinements across the platform to enhance visual appeal and user interaction.
|
||||
|
||||
### Removed
|
||||
|
||||
- **🗑️ Deprecated 'DOCS_DIR'**: Removed the outdated 'docs_dir' variable in favor of more direct file management solutions, with direct file directory syncing and API uploads for a more integrated experience.
|
||||
|
||||
## [0.3.30] - 2024-09-26
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🍞 Update Available Toast Dismissal**: Enhanced user experience by ensuring that once the update available notification is dismissed, it won't reappear for 24 hours.
|
||||
- **📋 Ollama /embed Form Data**: Adjusted the integration inaccuracies in the /embed form data to ensure it perfectly matches with Ollama's specifications.
|
||||
- **🔧 O1 Max Completion Tokens Issue**: Resolved compatibility issues with OpenAI's o1 models max_completion_tokens param to ensure smooth operation.
|
||||
- **🔄 Pip Install Database Issue**: Fixed a critical issue where database changes during pip installations were reverting and not saving chat logs, now ensuring data persistence and reliability in chat operations.
|
||||
- **🏷️ Chat Rename Tab Update**: Fixed the functionality to change the web browser's tab title simultaneously when a chat is renamed, keeping tab titles consistent.
|
||||
|
||||
## [0.3.29] - 2023-09-25
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🔧 KaTeX Rendering Improvement**: Resolved specific corner cases in KaTeX rendering to enhance the display of complex mathematical notation.
|
||||
- **📞 'Call' URL Parameter Fix**: Corrected functionality for 'call' URL search parameter ensuring reliable activation of voice calls through URL triggers.
|
||||
- **🔄 Configuration Reset Fix**: Fixed the RESET_CONFIG_ON_START to ensure settings revert to default correctly upon each startup, improving reliability in configuration management.
|
||||
- **🌍 Filter Outlet Hook Fix**: Addressed issues in the filter outlet hook, ensuring all filter functions operate as intended.
|
||||
|
||||
## [0.3.28] - 2024-09-24
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🔍 Web Search Functionality**: Corrected an issue where the web search option was not functioning properly.
|
||||
|
||||
## [0.3.27] - 2024-09-24
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🔄 Periodic Cleanup Error Resolved**: Fixed a critical RuntimeError related to the 'periodic_usage_pool_cleanup' coroutine, ensuring smooth and efficient performance post-pip install, correcting a persisting issue from version 0.3.26.
|
||||
- **📊 Enhanced LaTeX Rendering**: Improved rendering for LaTeX content, enhancing clarity and visual presentation in documents and mathematical models.
|
||||
|
||||
## [0.3.26] - 2024-09-24
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🔄 Event Loop Error Resolution**: Addressed a critical error where a missing running event loop caused 'periodic_usage_pool_cleanup' to fail with pip installs. This fix ensures smoother and more reliable updates and installations, enhancing overall system stability.
|
||||
|
||||
## [0.3.25] - 2024-09-24
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🖼️ Image Generation Functionality**: Resolved an issue where image generation was not functioning, restoring full capability for visual content creation.
|
||||
- **⚖️ Rate Response Corrections**: Addressed a problem where rate responses were not working, ensuring reliable feedback mechanisms are operational.
|
||||
|
||||
## [0.3.24] - 2024-09-24
|
||||
|
||||
### Added
|
||||
|
||||
- **🚀 Rendering Optimization**: Significantly improved message rendering performance, enhancing user experience and webui responsiveness.
|
||||
- **💖 Favorite Response Feature in Chat Overview**: Users can now mark responses as favorite directly from the chat overview, enhancing ease of retrieval and organization of preferred responses.
|
||||
- **💬 Create Message Pairs with Shortcut**: Implemented creation of new message pairs using Cmd/Ctrl+Shift+Enter, making conversation editing faster and more intuitive.
|
||||
- **🌍 Expanded User Prompt Variables**: Added weekday, timezone, and language information variables to user prompts to match system prompt variables.
|
||||
- **🎵 Enhanced Audio Support**: Now includes support for 'audio/x-m4a' files, broadening compatibility with audio content within the platform.
|
||||
- **🔏 Model URL Search Parameter**: Added an ability to select a model directly via URL parameters, streamlining navigation and model access.
|
||||
- **📄 Enhanced PDF Citations**: PDF citations now open at the associated page, streamlining reference checks and document handling.
|
||||
- **🔧Use of Redis in Sockets**: Enhanced socket implementation to fully support Redis, enabling effective stateless instances suitable for scalable load balancing.
|
||||
- **🌍 Stream Individual Model Responses**: Allows specific models to have individualized streaming settings, enhancing performance and customization.
|
||||
- **🕒 Display Model Hash and Last Modified Timestamp for Ollama Models**: Provides critical model details directly in the Models workspace for enhanced tracking.
|
||||
- **❗ Update Info Notification for Admins**: Ensures administrators receive immediate updates upon login, keeping them informed of the latest changes and system statuses.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🗑️ Temporary File Handling On Windows**: Fixed an issue causing errors when accessing a temporary file being used by another process, Tools & Functions should now work as intended.
|
||||
- **🔓 Authentication Toggle Issue**: Resolved the malfunction where setting 'WEBUI_AUTH=False' did not appropriately disable authentication, ensuring that user experience and system security settings function as configured.
|
||||
- **🔧 Save As Copy Issue for Many Model Chats**: Resolved an error preventing users from save messages as copies in many model chats.
|
||||
- **🔒 Sidebar Closure on Mobile**: Resolved an issue where the mobile sidebar remained open after menu engagement, improving user interface responsivity and comfort.
|
||||
- **🛡️ Tooltip XSS Vulnerability**: Resolved a cross-site scripting (XSS) issue within tooltips, ensuring enhanced security and data integrity during user interactions.
|
||||
|
||||
### Changed
|
||||
|
||||
- **↩️ Deprecated Interface Stream Response Settings**: Moved to advanced parameters to streamline interface settings and enhance user clarity.
|
||||
- **⚙️ Renamed 'speedRate' to 'playbackRate'**: Standardizes terminology, improving usability and understanding in media settings.
|
||||
|
||||
## [0.3.23] - 2024-09-21
|
||||
|
||||
### Added
|
||||
|
||||
- **🚀 WebSocket Redis Support**: Enhanced load balancing capabilities for multiple instance setups, promoting better performance and reliability in WebUI.
|
||||
- **🔧 Adjustable Chat Controls**: Introduced width-adjustable chat controls, enabling a personalized and more comfortable user interface.
|
||||
- **🌎 i18n Updates**: Improved and updated the Chinese translations.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🌐 Task Model Unloading Issue**: Modified task handling to use the Ollama /api/chat endpoint instead of OpenAI compatible endpoint, ensuring models stay loaded and ready with custom parameters, thus minimizing delays in task execution.
|
||||
- **📝 Title Generation Fix for OpenAI Compatible APIs**: Resolved an issue preventing the generation of titles, enhancing consistency and reliability when using multiple API providers.
|
||||
- **🗃️ RAG Duplicate Collection Issue**: Fixed a bug causing repeated processing of the same uploaded file. Now utilizes indexed files to prevent unnecessary duplications, optimizing resource usage.
|
||||
- **🖼️ Image Generation Enhancement**: Refactored OpenAI image generation endpoint to be asynchronous, preventing the WebUI from becoming unresponsive during processing, thus enhancing user experience.
|
||||
- **🔓 Downgrade Authlib**: Reverted Authlib to version 1.3.1 to address and resolve issues concerning OAuth functionality.
|
||||
|
||||
### Changed
|
||||
|
||||
- **🔍 Improved Message Interaction**: Enhanced the message node interface to allow for easier focus redirection with a simple click, streamlining user interaction.
|
||||
- **✨ Styling Refactor**: Updated WebUI styling for a cleaner, more modern look, enhancing user experience across the platform.
|
||||
|
||||
## [0.3.22] - 2024-09-19
|
||||
|
||||
### Added
|
||||
|
||||
- **⭐ Chat Overview**: Introducing a node-based interactive messages diagram for improved visualization of conversation flows.
|
||||
- **🔗 Multiple Vector DB Support**: Now supports multiple vector databases, including the newly added Milvus support. Community contributions for additional database support are highly encouraged!
|
||||
- **📡 Experimental Non-Stream Chat Completion**: Experimental feature allowing the use of OpenAI o1 models, which do not support streaming, ensuring more versatile model deployment.
|
||||
- **🔍 Experimental Colbert-AI Reranker Integration**: Added support for "jinaai/jina-colbert-v2" as a reranker, enhancing search relevance and accuracy. Note: it may not function at all on low-spec computers.
|
||||
- **🕸️ ENABLE_WEBSOCKET_SUPPORT**: Added environment variable for instances to ignore websocket upgrades, stabilizing connections on platforms with websocket issues.
|
||||
- **🔊 Azure Speech Service Integration**: Added support for Azure Speech services for Text-to-Speech (TTS).
|
||||
- **🎚️ Customizable Playback Speed**: Playback speed control is now available in Call mode settings, allowing users to adjust audio playback speed to their preferences.
|
||||
- **🧠 Enhanced Error Messaging**: System now displays helpful error messages directly to users during chat completion issues.
|
||||
- **📂 Save Model as Transparent PNG**: Model profile images are now saved as PNGs, supporting transparency and improving visual integration.
|
||||
- **📱 iPhone Compatibility Adjustments**: Added padding to accommodate the iPhone navigation bar, improving UI display on these devices.
|
||||
- **🔗 Secure Response Headers**: Implemented security response headers, bolstering web application security.
|
||||
- **🔧 Enhanced AUTOMATIC1111 Settings**: Users can now configure 'CFG Scale', 'Sampler', and 'Scheduler' parameters directly in the admin settings, enhancing workflow flexibility without source code modifications.
|
||||
- **🌍 i18n Updates**: Enhanced translations for Chinese, Ukrainian, Russian, and French, fostering a better localized experience.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🛠️ Chat Message Deletion**: Resolved issues with chat message deletion, ensuring a smoother user interaction and system stability.
|
||||
- **🔢 Ordered List Numbering**: Fixed the incorrect ordering in lists.
|
||||
|
||||
### Changed
|
||||
|
||||
- **🎨 Transparent Icon Handling**: Allowed model icons to be displayed on transparent backgrounds, improving UI aesthetics.
|
||||
- **📝 Improved RAG Template**: Enhanced Retrieval-Augmented Generation template, optimizing context handling and error checking for more precise operation.
|
||||
|
||||
## [0.3.21] - 2024-09-08
|
||||
|
||||
### Added
|
||||
|
||||
- **📊 Document Count Display**: Now displays the total number of documents directly within the dashboard.
|
||||
- **🚀 Ollama Embed API Endpoint**: Enabled /api/embed endpoint proxy support.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🐳 Docker Launch Issue**: Resolved the problem preventing Open-WebUI from launching correctly when using Docker.
|
||||
|
||||
### Changed
|
||||
|
||||
- **🔍 Enhanced Search Prompts**: Improved the search query generation prompts for better accuracy and user interaction, enhancing the overall search experience.
|
||||
|
||||
## [0.3.20] - 2024-09-07
|
||||
|
||||
### Added
|
||||
|
||||
- **🌐 Translation Update**: Updated Catalan translations to improve user experience for Catalan speakers.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **📄 PDF Download**: Resolved a configuration issue with fonts directory, ensuring PDFs are now downloaded with the correct formatting.
|
||||
- **🛠️ Installation of Tools & Functions Requirements**: Fixed a bug where necessary requirements for tools and functions were not properly installing.
|
||||
- **🔗 Inline Image Link Rendering**: Enabled rendering of images directly from links in chat.
|
||||
- **📞 Post-Call User Interface Cleanup**: Adjusted UI behavior to automatically close chat controls after a voice call ends, reducing screen clutter.
|
||||
- **🎙️ Microphone Deactivation Post-Call**: Addressed an issue where the microphone remained active after calls.
|
||||
- **✍️ Markdown Spacing Correction**: Corrected spacing in Markdown rendering, ensuring text appears neatly and as expected.
|
||||
- **🔄 Message Re-rendering**: Fixed an issue causing all response messages to re-render with each new message, now improving chat performance.
|
||||
|
||||
### Changed
|
||||
|
||||
- **🌐 Refined Web Search Integration**: Deprecated the Search Query Generation Prompt threshold; introduced a toggle button for "Enable Web Search Query Generation" allowing users to opt-in to using web search more judiciously.
|
||||
- **📝 Default Prompt Templates Update**: Emptied environment variable templates for search and title generation now default to the Open WebUI default prompt templates, simplifying configuration efforts.
|
||||
|
||||
## [0.3.19] - 2024-09-05
|
||||
|
||||
### Added
|
||||
|
||||
- **🌐 Translation Update**: Improved Chinese translations.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **📂 DATA_DIR Overriding**: Fixed an issue to avoid overriding DATA_DIR, preventing errors when directories are set identically, ensuring smoother operation and data management.
|
||||
- **🛠️ Frontmatter Extraction**: Fixed the extraction process for frontmatter in tools and functions.
|
||||
|
||||
### Changed
|
||||
|
||||
- **🎨 UI Styling**: Refined the user interface styling for enhanced visual coherence and user experience.
|
||||
|
||||
## [0.3.18] - 2024-09-04
|
||||
|
||||
### Added
|
||||
|
||||
- **🛠️ Direct Database Execution for Tools & Functions**: Enhanced the execution of Python files for tools and functions, now directly loading from the database for a more streamlined backend process.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🔄 Automatic Rewrite of Import Statements in Tools & Functions**: Tool and function scripts that import 'utils', 'apps', 'main', 'config' will now automatically rename these with 'open_webui.', ensuring compatibility and consistency across different modules.
|
||||
- **🎨 Styling Adjustments**: Minor fixes in the visual styling to improve user experience and interface consistency.
|
||||
|
||||
## [0.3.17] - 2024-09-04
|
||||
|
||||
### Added
|
||||
|
||||
+10
-5
@@ -17,7 +17,7 @@ ARG UID=0
|
||||
ARG GID=0
|
||||
|
||||
######## WebUI frontend ########
|
||||
FROM --platform=$BUILDPLATFORM node:21-alpine3.19 as build
|
||||
FROM --platform=$BUILDPLATFORM node:22-alpine3.20 AS build
|
||||
ARG BUILD_HASH
|
||||
|
||||
WORKDIR /app
|
||||
@@ -30,7 +30,7 @@ ENV APP_BUILD_HASH=${BUILD_HASH}
|
||||
RUN npm run build
|
||||
|
||||
######## WebUI backend ########
|
||||
FROM python:3.11-slim-bookworm as base
|
||||
FROM python:3.11-slim-bookworm AS base
|
||||
|
||||
# Use args
|
||||
ARG USE_CUDA
|
||||
@@ -74,11 +74,15 @@ ENV RAG_EMBEDDING_MODEL="$USE_EMBEDDING_MODEL_DOCKER" \
|
||||
|
||||
## Hugging Face download cache ##
|
||||
ENV HF_HOME="/app/backend/data/cache/embedding/models"
|
||||
|
||||
## Torch Extensions ##
|
||||
# ENV TORCH_EXTENSIONS_DIR="/.cache/torch_extensions"
|
||||
|
||||
#### Other models ##########################################################
|
||||
|
||||
WORKDIR /app/backend
|
||||
|
||||
ENV HOME /root
|
||||
ENV HOME=/root
|
||||
# Create user and group if not root
|
||||
RUN if [ $UID -ne 0 ]; then \
|
||||
if [ $GID -ne 0 ]; then \
|
||||
@@ -96,7 +100,7 @@ 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 curl && \
|
||||
apt-get install -y --no-install-recommends git build-essential pandoc netcat-openbsd curl && \
|
||||
apt-get install -y --no-install-recommends gcc python3-dev && \
|
||||
# for RAG OCR
|
||||
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
|
||||
@@ -109,7 +113,7 @@ RUN if [ "$USE_OLLAMA" = "true" ]; then \
|
||||
else \
|
||||
apt-get update && \
|
||||
# Install pandoc, netcat and gcc
|
||||
apt-get install -y --no-install-recommends pandoc gcc netcat-openbsd curl jq && \
|
||||
apt-get install -y --no-install-recommends git build-essential pandoc gcc netcat-openbsd curl jq && \
|
||||
apt-get install -y --no-install-recommends gcc python3-dev && \
|
||||
# for RAG OCR
|
||||
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
|
||||
@@ -157,5 +161,6 @@ USER $UID:$GID
|
||||
|
||||
ARG BUILD_HASH
|
||||
ENV WEBUI_BUILD_VERSION=${BUILD_HASH}
|
||||
ENV DOCKER=true
|
||||
|
||||
CMD [ "bash", "start.sh"]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Open WebUI (Formerly Ollama WebUI) 👋
|
||||
# Open WebUI 👋
|
||||
|
||||

|
||||

|
||||
@@ -59,11 +59,31 @@ Don't forget to explore our sibling project, [Open WebUI Community](https://open
|
||||
|
||||
## How to Install 🚀
|
||||
|
||||
> [!NOTE]
|
||||
> Please note that for certain Docker environments, additional configurations might be needed. If you encounter any connection issues, our detailed guide on [Open WebUI Documentation](https://docs.openwebui.com/) is ready to assist you.
|
||||
### Installation via Python pip 🐍
|
||||
|
||||
Open WebUI can be installed using pip, the Python package installer. Before proceeding, ensure you're using **Python 3.11** to avoid compatibility issues.
|
||||
|
||||
1. **Install Open WebUI**:
|
||||
Open your terminal and run the following command to install Open WebUI:
|
||||
|
||||
```bash
|
||||
pip install open-webui
|
||||
```
|
||||
|
||||
2. **Running Open WebUI**:
|
||||
After installation, you can start Open WebUI by executing:
|
||||
|
||||
```bash
|
||||
open-webui serve
|
||||
```
|
||||
|
||||
This will start the Open WebUI server, which you can access at [http://localhost:8080](http://localhost:8080)
|
||||
|
||||
### Quick Start with Docker 🐳
|
||||
|
||||
> [!NOTE]
|
||||
> Please note that for certain Docker environments, additional configurations might be needed. If you encounter any connection issues, our detailed guide on [Open WebUI Documentation](https://docs.openwebui.com/) is ready to assist you.
|
||||
|
||||
> [!WARNING]
|
||||
> When using Docker to install Open WebUI, make sure to include the `-v open-webui:/app/backend/data` in your Docker command. This step is crucial as it ensures your database is properly mounted and prevents any loss of data.
|
||||
|
||||
@@ -86,7 +106,7 @@ Don't forget to explore our sibling project, [Open WebUI Community](https://open
|
||||
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:
|
||||
- **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
|
||||
@@ -150,7 +170,7 @@ docker run --rm --volume /var/run/docker.sock:/var/run/docker.sock containrrr/wa
|
||||
|
||||
In the last part of the command, replace `open-webui` with your container name if it is different.
|
||||
|
||||
Check our Migration Guide available in our [Open WebUI Documentation](https://docs.openwebui.com/migration/).
|
||||
Check our Migration Guide available in our [Open WebUI Documentation](https://docs.openwebui.com/tutorials/migration/).
|
||||
|
||||
### Using the Dev Branch 🌙
|
||||
|
||||
@@ -200,4 +220,4 @@ If you have any questions, suggestions, or need assistance, please open an issue
|
||||
|
||||
---
|
||||
|
||||
Created by [Timothy J. Baek](https://github.com/tjbck) - Let's make Open WebUI even more amazing together! 💪
|
||||
Created by [Timothy Jaeryang Baek](https://github.com/tjbck) - Let's make Open WebUI even more amazing together! 💪
|
||||
|
||||
+1
-5
@@ -8,9 +8,5 @@ _test
|
||||
Pipfile
|
||||
!/data
|
||||
/data/*
|
||||
!/data/litellm
|
||||
/data/litellm/*
|
||||
!data/litellm/config.yaml
|
||||
|
||||
!data/config.json
|
||||
/open_webui/data/*
|
||||
.webui_secret_key
|
||||
@@ -16,6 +16,7 @@ def serve(
|
||||
host: str = "0.0.0.0",
|
||||
port: int = 8080,
|
||||
):
|
||||
os.environ["FROM_INIT_PY"] = "true"
|
||||
if os.getenv("WEBUI_SECRET_KEY") is None:
|
||||
typer.echo(
|
||||
"Loading WEBUI_SECRET_KEY from file, not provided as an environment variable."
|
||||
@@ -38,6 +39,20 @@ def serve(
|
||||
"/usr/local/lib/python3.11/site-packages/nvidia/cudnn/lib",
|
||||
]
|
||||
)
|
||||
try:
|
||||
import torch
|
||||
|
||||
assert torch.cuda.is_available(), "CUDA not available"
|
||||
typer.echo("CUDA seems to be working")
|
||||
except Exception as e:
|
||||
typer.echo(
|
||||
"Error when testing CUDA but USE_CUDA_DOCKER is true. "
|
||||
"Resetting USE_CUDA_DOCKER to false and removing "
|
||||
f"LD_LIBRARY_PATH modifications: {e}"
|
||||
)
|
||||
os.environ["USE_CUDA_DOCKER"] = "false"
|
||||
os.environ["LD_LIBRARY_PATH"] = ":".join(LD_LIBRARY_PATH)
|
||||
|
||||
import open_webui.main # we need set environment variables before importing main
|
||||
|
||||
uvicorn.run(open_webui.main.app, host=host, port=port, forwarded_allow_ips="*")
|
||||
|
||||
@@ -5,6 +5,8 @@ import os
|
||||
import uuid
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from pydub import AudioSegment
|
||||
from pydub.silence import split_on_silence
|
||||
|
||||
import requests
|
||||
from open_webui.config import (
|
||||
@@ -19,21 +21,28 @@ from open_webui.config import (
|
||||
AUDIO_TTS_OPENAI_API_KEY,
|
||||
AUDIO_TTS_SPLIT_ON,
|
||||
AUDIO_TTS_VOICE,
|
||||
AUDIO_TTS_AZURE_SPEECH_REGION,
|
||||
AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT,
|
||||
CACHE_DIR,
|
||||
CORS_ALLOW_ORIGIN,
|
||||
DEVICE_TYPE,
|
||||
WHISPER_MODEL,
|
||||
WHISPER_MODEL_AUTO_UPDATE,
|
||||
WHISPER_MODEL_DIR,
|
||||
AppConfig,
|
||||
)
|
||||
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
from open_webui.env import SRC_LOG_LEVELS, DEVICE_TYPE
|
||||
from fastapi import Depends, FastAPI, File, HTTPException, Request, UploadFile, status
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse
|
||||
from pydantic import BaseModel
|
||||
from open_webui.utils.utils import get_admin_user, get_current_user, get_verified_user
|
||||
from open_webui.utils.utils import get_admin_user, get_verified_user
|
||||
|
||||
# Constants
|
||||
MAX_FILE_SIZE_MB = 25
|
||||
MAX_FILE_SIZE = MAX_FILE_SIZE_MB * 1024 * 1024 # Convert MB to bytes
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["AUDIO"])
|
||||
@@ -62,6 +71,9 @@ app.state.config.TTS_VOICE = AUDIO_TTS_VOICE
|
||||
app.state.config.TTS_API_KEY = AUDIO_TTS_API_KEY
|
||||
app.state.config.TTS_SPLIT_ON = AUDIO_TTS_SPLIT_ON
|
||||
|
||||
app.state.config.TTS_AZURE_SPEECH_REGION = AUDIO_TTS_AZURE_SPEECH_REGION
|
||||
app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT = AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT
|
||||
|
||||
# setting device type for whisper model
|
||||
whisper_device_type = DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == "cuda" else "cpu"
|
||||
log.info(f"whisper_device_type: {whisper_device_type}")
|
||||
@@ -78,6 +90,8 @@ class TTSConfigForm(BaseModel):
|
||||
MODEL: str
|
||||
VOICE: str
|
||||
SPLIT_ON: str
|
||||
AZURE_SPEECH_REGION: str
|
||||
AZURE_SPEECH_OUTPUT_FORMAT: str
|
||||
|
||||
|
||||
class STTConfigForm(BaseModel):
|
||||
@@ -130,6 +144,8 @@ async def get_audio_config(user=Depends(get_admin_user)):
|
||||
"MODEL": app.state.config.TTS_MODEL,
|
||||
"VOICE": app.state.config.TTS_VOICE,
|
||||
"SPLIT_ON": app.state.config.TTS_SPLIT_ON,
|
||||
"AZURE_SPEECH_REGION": app.state.config.TTS_AZURE_SPEECH_REGION,
|
||||
"AZURE_SPEECH_OUTPUT_FORMAT": app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT,
|
||||
},
|
||||
"stt": {
|
||||
"OPENAI_API_BASE_URL": app.state.config.STT_OPENAI_API_BASE_URL,
|
||||
@@ -151,6 +167,10 @@ async def update_audio_config(
|
||||
app.state.config.TTS_MODEL = form_data.tts.MODEL
|
||||
app.state.config.TTS_VOICE = form_data.tts.VOICE
|
||||
app.state.config.TTS_SPLIT_ON = form_data.tts.SPLIT_ON
|
||||
app.state.config.TTS_AZURE_SPEECH_REGION = form_data.tts.AZURE_SPEECH_REGION
|
||||
app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT = (
|
||||
form_data.tts.AZURE_SPEECH_OUTPUT_FORMAT
|
||||
)
|
||||
|
||||
app.state.config.STT_OPENAI_API_BASE_URL = form_data.stt.OPENAI_API_BASE_URL
|
||||
app.state.config.STT_OPENAI_API_KEY = form_data.stt.OPENAI_API_KEY
|
||||
@@ -166,6 +186,8 @@ async def update_audio_config(
|
||||
"MODEL": app.state.config.TTS_MODEL,
|
||||
"VOICE": app.state.config.TTS_VOICE,
|
||||
"SPLIT_ON": app.state.config.TTS_SPLIT_ON,
|
||||
"AZURE_SPEECH_REGION": app.state.config.TTS_AZURE_SPEECH_REGION,
|
||||
"AZURE_SPEECH_OUTPUT_FORMAT": app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT,
|
||||
},
|
||||
"stt": {
|
||||
"OPENAI_API_BASE_URL": app.state.config.STT_OPENAI_API_BASE_URL,
|
||||
@@ -301,68 +323,114 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
||||
detail=error_detail,
|
||||
)
|
||||
|
||||
elif app.state.config.TTS_ENGINE == "azure":
|
||||
payload = None
|
||||
try:
|
||||
payload = json.loads(body.decode("utf-8"))
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
raise HTTPException(status_code=400, detail="Invalid JSON payload")
|
||||
|
||||
@app.post("/transcriptions")
|
||||
def transcribe(
|
||||
file: UploadFile = File(...),
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
log.info(f"file.content_type: {file.content_type}")
|
||||
region = app.state.config.TTS_AZURE_SPEECH_REGION
|
||||
language = app.state.config.TTS_VOICE
|
||||
locale = "-".join(app.state.config.TTS_VOICE.split("-")[:1])
|
||||
output_format = app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT
|
||||
url = f"https://{region}.tts.speech.microsoft.com/cognitiveservices/v1"
|
||||
|
||||
if file.content_type not in ["audio/mpeg", "audio/wav"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.FILE_NOT_SUPPORTED,
|
||||
)
|
||||
headers = {
|
||||
"Ocp-Apim-Subscription-Key": app.state.config.TTS_API_KEY,
|
||||
"Content-Type": "application/ssml+xml",
|
||||
"X-Microsoft-OutputFormat": output_format,
|
||||
}
|
||||
|
||||
try:
|
||||
ext = file.filename.split(".")[-1]
|
||||
data = f"""<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="{locale}">
|
||||
<voice name="{language}">{payload["input"]}</voice>
|
||||
</speak>"""
|
||||
|
||||
id = uuid.uuid4()
|
||||
filename = f"{id}.{ext}"
|
||||
response = requests.post(url, headers=headers, data=data)
|
||||
|
||||
file_dir = f"{CACHE_DIR}/audio/transcriptions"
|
||||
os.makedirs(file_dir, exist_ok=True)
|
||||
file_path = f"{file_dir}/{filename}"
|
||||
|
||||
print(filename)
|
||||
|
||||
contents = file.file.read()
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(contents)
|
||||
f.close()
|
||||
|
||||
if app.state.config.STT_ENGINE == "":
|
||||
from faster_whisper import WhisperModel
|
||||
|
||||
whisper_kwargs = {
|
||||
"model_size_or_path": WHISPER_MODEL,
|
||||
"device": whisper_device_type,
|
||||
"compute_type": "int8",
|
||||
"download_root": WHISPER_MODEL_DIR,
|
||||
"local_files_only": not WHISPER_MODEL_AUTO_UPDATE,
|
||||
}
|
||||
|
||||
log.debug(f"whisper_kwargs: {whisper_kwargs}")
|
||||
|
||||
try:
|
||||
model = WhisperModel(**whisper_kwargs)
|
||||
except Exception:
|
||||
log.warning(
|
||||
"WhisperModel initialization failed, attempting download with local_files_only=False"
|
||||
)
|
||||
whisper_kwargs["local_files_only"] = False
|
||||
model = WhisperModel(**whisper_kwargs)
|
||||
|
||||
segments, info = model.transcribe(file_path, beam_size=5)
|
||||
log.info(
|
||||
"Detected language '%s' with probability %f"
|
||||
% (info.language, info.language_probability)
|
||||
if response.status_code == 200:
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(response.content)
|
||||
return FileResponse(file_path)
|
||||
else:
|
||||
log.error(f"Error synthesizing speech - {response.reason}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error synthesizing speech - {response.reason}"
|
||||
)
|
||||
|
||||
transcript = "".join([segment.text for segment in list(segments)])
|
||||
|
||||
data = {"text": transcript.strip()}
|
||||
def transcribe(file_path):
|
||||
print("transcribe", file_path)
|
||||
filename = os.path.basename(file_path)
|
||||
file_dir = os.path.dirname(file_path)
|
||||
id = filename.split(".")[0]
|
||||
|
||||
if app.state.config.STT_ENGINE == "":
|
||||
from faster_whisper import WhisperModel
|
||||
|
||||
whisper_kwargs = {
|
||||
"model_size_or_path": WHISPER_MODEL,
|
||||
"device": whisper_device_type,
|
||||
"compute_type": "int8",
|
||||
"download_root": WHISPER_MODEL_DIR,
|
||||
"local_files_only": not WHISPER_MODEL_AUTO_UPDATE,
|
||||
}
|
||||
|
||||
log.debug(f"whisper_kwargs: {whisper_kwargs}")
|
||||
|
||||
try:
|
||||
model = WhisperModel(**whisper_kwargs)
|
||||
except Exception:
|
||||
log.warning(
|
||||
"WhisperModel initialization failed, attempting download with local_files_only=False"
|
||||
)
|
||||
whisper_kwargs["local_files_only"] = False
|
||||
model = WhisperModel(**whisper_kwargs)
|
||||
|
||||
segments, info = model.transcribe(file_path, beam_size=5)
|
||||
log.info(
|
||||
"Detected language '%s' with probability %f"
|
||||
% (info.language, info.language_probability)
|
||||
)
|
||||
|
||||
transcript = "".join([segment.text for segment in list(segments)])
|
||||
|
||||
data = {"text": transcript.strip()}
|
||||
|
||||
# save the transcript to a json file
|
||||
transcript_file = f"{file_dir}/{id}.json"
|
||||
with open(transcript_file, "w") as f:
|
||||
json.dump(data, f)
|
||||
|
||||
print(data)
|
||||
return data
|
||||
elif app.state.config.STT_ENGINE == "openai":
|
||||
if is_mp4_audio(file_path):
|
||||
print("is_mp4_audio")
|
||||
os.rename(file_path, file_path.replace(".wav", ".mp4"))
|
||||
# Convert MP4 audio file to WAV format
|
||||
convert_mp4_to_wav(file_path.replace(".wav", ".mp4"), file_path)
|
||||
|
||||
headers = {"Authorization": f"Bearer {app.state.config.STT_OPENAI_API_KEY}"}
|
||||
|
||||
files = {"file": (filename, open(file_path, "rb"))}
|
||||
data = {"model": app.state.config.STT_MODEL}
|
||||
|
||||
print(files, data)
|
||||
|
||||
r = None
|
||||
try:
|
||||
r = requests.post(
|
||||
url=f"{app.state.config.STT_OPENAI_API_BASE_URL}/audio/transcriptions",
|
||||
headers=headers,
|
||||
files=files,
|
||||
data=data,
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
data = r.json()
|
||||
|
||||
# save the transcript to a json file
|
||||
transcript_file = f"{file_dir}/{id}.json"
|
||||
@@ -370,58 +438,82 @@ def transcribe(
|
||||
json.dump(data, f)
|
||||
|
||||
print(data)
|
||||
return data
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"External: {res['error']['message']}"
|
||||
except Exception:
|
||||
error_detail = f"External: {e}"
|
||||
|
||||
raise error_detail
|
||||
|
||||
|
||||
@app.post("/transcriptions")
|
||||
def transcription(
|
||||
file: UploadFile = File(...),
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
log.info(f"file.content_type: {file.content_type}")
|
||||
|
||||
if file.content_type not in ["audio/mpeg", "audio/wav", "audio/ogg", "audio/x-m4a"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.FILE_NOT_SUPPORTED,
|
||||
)
|
||||
|
||||
try:
|
||||
ext = file.filename.split(".")[-1]
|
||||
id = uuid.uuid4()
|
||||
|
||||
filename = f"{id}.{ext}"
|
||||
contents = file.file.read()
|
||||
|
||||
file_dir = f"{CACHE_DIR}/audio/transcriptions"
|
||||
os.makedirs(file_dir, exist_ok=True)
|
||||
file_path = f"{file_dir}/{filename}"
|
||||
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(contents)
|
||||
|
||||
try:
|
||||
if os.path.getsize(file_path) > MAX_FILE_SIZE: # file is bigger than 25MB
|
||||
log.debug(f"File size is larger than {MAX_FILE_SIZE_MB}MB")
|
||||
audio = AudioSegment.from_file(file_path)
|
||||
audio = audio.set_frame_rate(16000).set_channels(1) # Compress audio
|
||||
compressed_path = f"{file_dir}/{id}_compressed.opus"
|
||||
audio.export(compressed_path, format="opus", bitrate="32k")
|
||||
log.debug(f"Compressed audio to {compressed_path}")
|
||||
file_path = compressed_path
|
||||
|
||||
if (
|
||||
os.path.getsize(file_path) > MAX_FILE_SIZE
|
||||
): # Still larger than 25MB after compression
|
||||
log.debug(
|
||||
f"Compressed file size is still larger than {MAX_FILE_SIZE_MB}MB: {os.path.getsize(file_path)}"
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.FILE_TOO_LARGE(
|
||||
size=f"{MAX_FILE_SIZE_MB}MB"
|
||||
),
|
||||
)
|
||||
|
||||
data = transcribe(file_path)
|
||||
else:
|
||||
data = transcribe(file_path)
|
||||
|
||||
return data
|
||||
|
||||
elif app.state.config.STT_ENGINE == "openai":
|
||||
if is_mp4_audio(file_path):
|
||||
print("is_mp4_audio")
|
||||
os.rename(file_path, file_path.replace(".wav", ".mp4"))
|
||||
# Convert MP4 audio file to WAV format
|
||||
convert_mp4_to_wav(file_path.replace(".wav", ".mp4"), file_path)
|
||||
|
||||
headers = {"Authorization": f"Bearer {app.state.config.STT_OPENAI_API_KEY}"}
|
||||
|
||||
files = {"file": (filename, open(file_path, "rb"))}
|
||||
data = {"model": app.state.config.STT_MODEL}
|
||||
|
||||
print(files, data)
|
||||
|
||||
r = None
|
||||
try:
|
||||
r = requests.post(
|
||||
url=f"{app.state.config.STT_OPENAI_API_BASE_URL}/audio/transcriptions",
|
||||
headers=headers,
|
||||
files=files,
|
||||
data=data,
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
data = r.json()
|
||||
|
||||
# save the transcript to a json file
|
||||
transcript_file = f"{file_dir}/{id}.json"
|
||||
with open(transcript_file, "w") as f:
|
||||
json.dump(data, f)
|
||||
|
||||
print(data)
|
||||
return data
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"External: {res['error']['message']}"
|
||||
except Exception:
|
||||
error_detail = f"External: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r != None else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
@@ -443,7 +535,7 @@ def get_available_models() -> list[dict]:
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
"https://api.elevenlabs.io/v1/models", headers=headers
|
||||
"https://api.elevenlabs.io/v1/models", headers=headers, timeout=5
|
||||
)
|
||||
response.raise_for_status()
|
||||
models = response.json()
|
||||
@@ -478,6 +570,21 @@ def get_available_voices() -> dict:
|
||||
except Exception:
|
||||
# Avoided @lru_cache with exception
|
||||
pass
|
||||
elif app.state.config.TTS_ENGINE == "azure":
|
||||
try:
|
||||
region = app.state.config.TTS_AZURE_SPEECH_REGION
|
||||
url = f"https://{region}.tts.speech.microsoft.com/cognitiveservices/voices/list"
|
||||
headers = {"Ocp-Apim-Subscription-Key": app.state.config.TTS_API_KEY}
|
||||
|
||||
response = requests.get(url, headers=headers)
|
||||
response.raise_for_status()
|
||||
voices = response.json()
|
||||
for voice in voices:
|
||||
ret[voice["ShortName"]] = (
|
||||
f"{voice['DisplayName']} ({voice['ShortName']})"
|
||||
)
|
||||
except requests.RequestException as e:
|
||||
log.error(f"Error fetching voices: {str(e)}")
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
@@ -17,6 +17,9 @@ from open_webui.apps.images.utils.comfyui import (
|
||||
from open_webui.config import (
|
||||
AUTOMATIC1111_API_AUTH,
|
||||
AUTOMATIC1111_BASE_URL,
|
||||
AUTOMATIC1111_CFG_SCALE,
|
||||
AUTOMATIC1111_SAMPLER,
|
||||
AUTOMATIC1111_SCHEDULER,
|
||||
CACHE_DIR,
|
||||
COMFYUI_BASE_URL,
|
||||
COMFYUI_WORKFLOW,
|
||||
@@ -65,6 +68,9 @@ app.state.config.MODEL = IMAGE_GENERATION_MODEL
|
||||
|
||||
app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
|
||||
app.state.config.AUTOMATIC1111_API_AUTH = AUTOMATIC1111_API_AUTH
|
||||
app.state.config.AUTOMATIC1111_CFG_SCALE = AUTOMATIC1111_CFG_SCALE
|
||||
app.state.config.AUTOMATIC1111_SAMPLER = AUTOMATIC1111_SAMPLER
|
||||
app.state.config.AUTOMATIC1111_SCHEDULER = AUTOMATIC1111_SCHEDULER
|
||||
app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
|
||||
app.state.config.COMFYUI_WORKFLOW = COMFYUI_WORKFLOW
|
||||
app.state.config.COMFYUI_WORKFLOW_NODES = COMFYUI_WORKFLOW_NODES
|
||||
@@ -85,6 +91,9 @@ async def get_config(request: Request, user=Depends(get_admin_user)):
|
||||
"automatic1111": {
|
||||
"AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
|
||||
"AUTOMATIC1111_API_AUTH": app.state.config.AUTOMATIC1111_API_AUTH,
|
||||
"AUTOMATIC1111_CFG_SCALE": app.state.config.AUTOMATIC1111_CFG_SCALE,
|
||||
"AUTOMATIC1111_SAMPLER": app.state.config.AUTOMATIC1111_SAMPLER,
|
||||
"AUTOMATIC1111_SCHEDULER": app.state.config.AUTOMATIC1111_SCHEDULER,
|
||||
},
|
||||
"comfyui": {
|
||||
"COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
|
||||
@@ -102,6 +111,9 @@ class OpenAIConfigForm(BaseModel):
|
||||
class Automatic1111ConfigForm(BaseModel):
|
||||
AUTOMATIC1111_BASE_URL: str
|
||||
AUTOMATIC1111_API_AUTH: str
|
||||
AUTOMATIC1111_CFG_SCALE: Optional[str]
|
||||
AUTOMATIC1111_SAMPLER: Optional[str]
|
||||
AUTOMATIC1111_SCHEDULER: Optional[str]
|
||||
|
||||
|
||||
class ComfyUIConfigForm(BaseModel):
|
||||
@@ -133,6 +145,22 @@ async def update_config(form_data: ConfigForm, user=Depends(get_admin_user)):
|
||||
form_data.automatic1111.AUTOMATIC1111_API_AUTH
|
||||
)
|
||||
|
||||
app.state.config.AUTOMATIC1111_CFG_SCALE = (
|
||||
float(form_data.automatic1111.AUTOMATIC1111_CFG_SCALE)
|
||||
if form_data.automatic1111.AUTOMATIC1111_CFG_SCALE
|
||||
else None
|
||||
)
|
||||
app.state.config.AUTOMATIC1111_SAMPLER = (
|
||||
form_data.automatic1111.AUTOMATIC1111_SAMPLER
|
||||
if form_data.automatic1111.AUTOMATIC1111_SAMPLER
|
||||
else None
|
||||
)
|
||||
app.state.config.AUTOMATIC1111_SCHEDULER = (
|
||||
form_data.automatic1111.AUTOMATIC1111_SCHEDULER
|
||||
if form_data.automatic1111.AUTOMATIC1111_SCHEDULER
|
||||
else None
|
||||
)
|
||||
|
||||
app.state.config.COMFYUI_BASE_URL = form_data.comfyui.COMFYUI_BASE_URL.strip("/")
|
||||
app.state.config.COMFYUI_WORKFLOW = form_data.comfyui.COMFYUI_WORKFLOW
|
||||
app.state.config.COMFYUI_WORKFLOW_NODES = form_data.comfyui.COMFYUI_WORKFLOW_NODES
|
||||
@@ -147,6 +175,9 @@ async def update_config(form_data: ConfigForm, user=Depends(get_admin_user)):
|
||||
"automatic1111": {
|
||||
"AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
|
||||
"AUTOMATIC1111_API_AUTH": app.state.config.AUTOMATIC1111_API_AUTH,
|
||||
"AUTOMATIC1111_CFG_SCALE": app.state.config.AUTOMATIC1111_CFG_SCALE,
|
||||
"AUTOMATIC1111_SAMPLER": app.state.config.AUTOMATIC1111_SAMPLER,
|
||||
"AUTOMATIC1111_SCHEDULER": app.state.config.AUTOMATIC1111_SCHEDULER,
|
||||
},
|
||||
"comfyui": {
|
||||
"COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
|
||||
@@ -439,7 +470,9 @@ async def image_generations(
|
||||
"response_format": "b64_json",
|
||||
}
|
||||
|
||||
r = requests.post(
|
||||
# Use asyncio.to_thread for the requests.post call
|
||||
r = await asyncio.to_thread(
|
||||
requests.post,
|
||||
url=f"{app.state.config.OPENAI_API_BASE_URL}/images/generations",
|
||||
json=data,
|
||||
headers=headers,
|
||||
@@ -524,6 +557,15 @@ async def image_generations(
|
||||
if form_data.negative_prompt is not None:
|
||||
data["negative_prompt"] = form_data.negative_prompt
|
||||
|
||||
if app.state.config.AUTOMATIC1111_CFG_SCALE:
|
||||
data["cfg_scale"] = app.state.config.AUTOMATIC1111_CFG_SCALE
|
||||
|
||||
if app.state.config.AUTOMATIC1111_SAMPLER:
|
||||
data["sampler_name"] = app.state.config.AUTOMATIC1111_SAMPLER
|
||||
|
||||
if app.state.config.AUTOMATIC1111_SCHEDULER:
|
||||
data["scheduler"] = app.state.config.AUTOMATIC1111_SCHEDULER
|
||||
|
||||
# Use asyncio.to_thread for the requests.post call
|
||||
r = await asyncio.to_thread(
|
||||
requests.post,
|
||||
|
||||
@@ -12,7 +12,6 @@ import aiohttp
|
||||
import requests
|
||||
from open_webui.apps.webui.models.models import Models
|
||||
from open_webui.config import (
|
||||
AIOHTTP_CLIENT_TIMEOUT,
|
||||
CORS_ALLOW_ORIGIN,
|
||||
ENABLE_MODEL_FILTER,
|
||||
ENABLE_OLLAMA_API,
|
||||
@@ -21,6 +20,9 @@ from open_webui.config import (
|
||||
UPLOAD_DIR,
|
||||
AppConfig,
|
||||
)
|
||||
from open_webui.env import AIOHTTP_CLIENT_TIMEOUT
|
||||
|
||||
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
from fastapi import Depends, FastAPI, File, HTTPException, Request, UploadFile
|
||||
@@ -28,11 +30,15 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from starlette.background import BackgroundTask
|
||||
|
||||
|
||||
from open_webui.utils.misc import (
|
||||
calculate_sha256,
|
||||
)
|
||||
from open_webui.utils.payload import (
|
||||
apply_model_params_to_body_ollama,
|
||||
apply_model_params_to_body_openai,
|
||||
apply_model_system_prompt_to_body,
|
||||
calculate_sha256,
|
||||
)
|
||||
from open_webui.utils.utils import get_admin_user, get_verified_user
|
||||
|
||||
@@ -113,7 +119,7 @@ async def update_ollama_api_url(form_data: UrlUpdateForm, user=Depends(get_admin
|
||||
|
||||
|
||||
async def fetch_url(url):
|
||||
timeout = aiohttp.ClientTimeout(total=5)
|
||||
timeout = aiohttp.ClientTimeout(total=3)
|
||||
try:
|
||||
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
|
||||
async with session.get(url) as response:
|
||||
@@ -539,6 +545,65 @@ class GenerateEmbeddingsForm(BaseModel):
|
||||
keep_alive: Optional[Union[int, str]] = None
|
||||
|
||||
|
||||
class GenerateEmbedForm(BaseModel):
|
||||
model: str
|
||||
input: str
|
||||
truncate: Optional[bool]
|
||||
options: Optional[dict] = None
|
||||
keep_alive: Optional[Union[int, str]] = None
|
||||
|
||||
|
||||
@app.post("/api/embed")
|
||||
@app.post("/api/embed/{url_idx}")
|
||||
async def generate_embeddings(
|
||||
form_data: GenerateEmbedForm,
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
if url_idx is None:
|
||||
model = form_data.model
|
||||
|
||||
if ":" not in model:
|
||||
model = f"{model}:latest"
|
||||
|
||||
if model in app.state.MODELS:
|
||||
url_idx = random.choice(app.state.MODELS[model]["urls"])
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
|
||||
)
|
||||
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
log.info(f"url: {url}")
|
||||
|
||||
r = requests.request(
|
||||
method="POST",
|
||||
url=f"{url}/api/embed",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=form_data.model_dump_json(exclude_none=True).encode(),
|
||||
)
|
||||
try:
|
||||
r.raise_for_status()
|
||||
|
||||
return r.json()
|
||||
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 Exception:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
|
||||
|
||||
@app.post("/api/embeddings")
|
||||
@app.post("/api/embeddings/{url_idx}")
|
||||
async def generate_embeddings(
|
||||
@@ -724,6 +789,7 @@ async def generate_chat_completion(
|
||||
):
|
||||
payload = {**form_data.model_dump(exclude_none=True)}
|
||||
log.debug(f"{payload = }")
|
||||
|
||||
if "metadata" in payload:
|
||||
del payload["metadata"]
|
||||
|
||||
@@ -761,7 +827,10 @@ async def generate_chat_completion(
|
||||
log.debug(payload)
|
||||
|
||||
return await post_streaming_url(
|
||||
f"{url}/api/chat", json.dumps(payload), content_type="application/x-ndjson"
|
||||
f"{url}/api/chat",
|
||||
json.dumps(payload),
|
||||
stream=form_data.stream,
|
||||
content_type="application/x-ndjson",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import aiohttp
|
||||
import requests
|
||||
from open_webui.apps.webui.models.models import Models
|
||||
from open_webui.config import (
|
||||
AIOHTTP_CLIENT_TIMEOUT,
|
||||
CACHE_DIR,
|
||||
CORS_ALLOW_ORIGIN,
|
||||
ENABLE_MODEL_FILTER,
|
||||
@@ -19,6 +18,8 @@ from open_webui.config import (
|
||||
OPENAI_API_KEYS,
|
||||
AppConfig,
|
||||
)
|
||||
from open_webui.env import AIOHTTP_CLIENT_TIMEOUT
|
||||
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
from fastapi import Depends, FastAPI, HTTPException, Request
|
||||
@@ -26,10 +27,12 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse, StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
from starlette.background import BackgroundTask
|
||||
from open_webui.utils.misc import (
|
||||
|
||||
from open_webui.utils.payload import (
|
||||
apply_model_params_to_body_openai,
|
||||
apply_model_system_prompt_to_body,
|
||||
)
|
||||
|
||||
from open_webui.utils.utils import get_admin_user, get_verified_user
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -44,7 +47,6 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
app.state.config = AppConfig()
|
||||
|
||||
app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
|
||||
@@ -177,7 +179,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
||||
|
||||
|
||||
async def fetch_url(url, key):
|
||||
timeout = aiohttp.ClientTimeout(total=5)
|
||||
timeout = aiohttp.ClientTimeout(total=3)
|
||||
try:
|
||||
headers = {"Authorization": f"Bearer {key}"}
|
||||
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
|
||||
@@ -217,7 +219,17 @@ def merge_models_lists(model_lists):
|
||||
for model in models
|
||||
if "api.openai.com"
|
||||
not in app.state.config.OPENAI_API_BASE_URLS[idx]
|
||||
or "gpt" in model["id"]
|
||||
or not any(
|
||||
name in model["id"]
|
||||
for name in [
|
||||
"babbage",
|
||||
"dall-e",
|
||||
"davinci",
|
||||
"embedding",
|
||||
"tts",
|
||||
"whisper",
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
@@ -320,10 +332,24 @@ async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_us
|
||||
r.raise_for_status()
|
||||
|
||||
response_data = r.json()
|
||||
|
||||
if "api.openai.com" in url:
|
||||
response_data["data"] = list(
|
||||
filter(lambda model: "gpt" in model["id"], response_data["data"])
|
||||
)
|
||||
# Filter the response data
|
||||
response_data["data"] = [
|
||||
model
|
||||
for model in response_data["data"]
|
||||
if not any(
|
||||
name in model["id"]
|
||||
for name in [
|
||||
"babbage",
|
||||
"dall-e",
|
||||
"davinci",
|
||||
"embedding",
|
||||
"tts",
|
||||
"whisper",
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
return response_data
|
||||
except Exception as e:
|
||||
@@ -378,14 +404,32 @@ async def generate_chat_completion(
|
||||
"role": user.role,
|
||||
}
|
||||
|
||||
url = app.state.config.OPENAI_API_BASE_URLS[idx]
|
||||
key = app.state.config.OPENAI_API_KEYS[idx]
|
||||
is_o1 = payload["model"].lower().startswith("o1-")
|
||||
|
||||
# Change max_completion_tokens to max_tokens (Backward compatible)
|
||||
if "api.openai.com" not in url and not is_o1:
|
||||
if "max_completion_tokens" in payload:
|
||||
# Remove "max_completion_tokens" from the payload
|
||||
payload["max_tokens"] = payload["max_completion_tokens"]
|
||||
del payload["max_completion_tokens"]
|
||||
else:
|
||||
if is_o1 and "max_tokens" in payload:
|
||||
payload["max_completion_tokens"] = payload["max_tokens"]
|
||||
del payload["max_tokens"]
|
||||
if "max_tokens" in payload and "max_completion_tokens" in payload:
|
||||
del payload["max_tokens"]
|
||||
|
||||
# Fix: O1 does not support the "system" parameter, Modify "system" to "user"
|
||||
if is_o1 and payload["messages"][0]["role"] == "system":
|
||||
payload["messages"][0]["role"] = "user"
|
||||
|
||||
# Convert the modified body back to JSON
|
||||
payload = json.dumps(payload)
|
||||
|
||||
log.debug(payload)
|
||||
|
||||
url = app.state.config.OPENAI_API_BASE_URLS[idx]
|
||||
key = app.state.config.OPENAI_API_KEYS[idx]
|
||||
|
||||
headers = {}
|
||||
headers["Authorization"] = f"Bearer {key}"
|
||||
headers["Content-Type"] = "application/json"
|
||||
@@ -396,6 +440,7 @@ async def generate_chat_completion(
|
||||
r = None
|
||||
session = None
|
||||
streaming = False
|
||||
response = None
|
||||
|
||||
try:
|
||||
session = aiohttp.ClientSession(
|
||||
@@ -408,8 +453,6 @@ async def generate_chat_completion(
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
# Check if response is SSE
|
||||
if "text/event-stream" in r.headers.get("Content-Type", ""):
|
||||
streaming = True
|
||||
@@ -422,19 +465,23 @@ async def generate_chat_completion(
|
||||
),
|
||||
)
|
||||
else:
|
||||
response_data = await r.json()
|
||||
return response_data
|
||||
try:
|
||||
response = await r.json()
|
||||
except Exception as e:
|
||||
log.error(e)
|
||||
response = await r.text()
|
||||
|
||||
r.raise_for_status()
|
||||
return response
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = await r.json()
|
||||
print(res)
|
||||
if "error" in res:
|
||||
error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
|
||||
except Exception:
|
||||
error_detail = f"External: {e}"
|
||||
if isinstance(response, dict):
|
||||
if "error" in response:
|
||||
error_detail = f"{response['error']['message'] if 'message' in response['error'] else response['error']}"
|
||||
elif isinstance(response, str):
|
||||
error_detail = response
|
||||
|
||||
raise HTTPException(status_code=r.status if r else 500, detail=error_detail)
|
||||
finally:
|
||||
if not streaming and session:
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
import requests
|
||||
import logging
|
||||
import ftfy
|
||||
|
||||
from langchain_community.document_loaders import (
|
||||
BSHTMLLoader,
|
||||
CSVLoader,
|
||||
Docx2txtLoader,
|
||||
OutlookMessageLoader,
|
||||
PyPDFLoader,
|
||||
TextLoader,
|
||||
UnstructuredEPubLoader,
|
||||
UnstructuredExcelLoader,
|
||||
UnstructuredMarkdownLoader,
|
||||
UnstructuredPowerPointLoader,
|
||||
UnstructuredRSTLoader,
|
||||
UnstructuredXMLLoader,
|
||||
YoutubeLoader,
|
||||
)
|
||||
from langchain_core.documents import Document
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||
|
||||
known_source_ext = [
|
||||
"go",
|
||||
"py",
|
||||
"java",
|
||||
"sh",
|
||||
"bat",
|
||||
"ps1",
|
||||
"cmd",
|
||||
"js",
|
||||
"ts",
|
||||
"css",
|
||||
"cpp",
|
||||
"hpp",
|
||||
"h",
|
||||
"c",
|
||||
"cs",
|
||||
"sql",
|
||||
"log",
|
||||
"ini",
|
||||
"pl",
|
||||
"pm",
|
||||
"r",
|
||||
"dart",
|
||||
"dockerfile",
|
||||
"env",
|
||||
"php",
|
||||
"hs",
|
||||
"hsc",
|
||||
"lua",
|
||||
"nginxconf",
|
||||
"conf",
|
||||
"m",
|
||||
"mm",
|
||||
"plsql",
|
||||
"perl",
|
||||
"rb",
|
||||
"rs",
|
||||
"db2",
|
||||
"scala",
|
||||
"bash",
|
||||
"swift",
|
||||
"vue",
|
||||
"svelte",
|
||||
"msg",
|
||||
"ex",
|
||||
"exs",
|
||||
"erl",
|
||||
"tsx",
|
||||
"jsx",
|
||||
"hs",
|
||||
"lhs",
|
||||
]
|
||||
|
||||
|
||||
class TikaLoader:
|
||||
def __init__(self, url, file_path, mime_type=None):
|
||||
self.url = url
|
||||
self.file_path = file_path
|
||||
self.mime_type = mime_type
|
||||
|
||||
def load(self) -> list[Document]:
|
||||
with open(self.file_path, "rb") as f:
|
||||
data = f.read()
|
||||
|
||||
if self.mime_type is not None:
|
||||
headers = {"Content-Type": self.mime_type}
|
||||
else:
|
||||
headers = {}
|
||||
|
||||
endpoint = self.url
|
||||
if not endpoint.endswith("/"):
|
||||
endpoint += "/"
|
||||
endpoint += "tika/text"
|
||||
|
||||
r = requests.put(endpoint, data=data, headers=headers)
|
||||
|
||||
if r.ok:
|
||||
raw_metadata = r.json()
|
||||
text = raw_metadata.get("X-TIKA:content", "<No text content found>")
|
||||
|
||||
if "Content-Type" in raw_metadata:
|
||||
headers["Content-Type"] = raw_metadata["Content-Type"]
|
||||
|
||||
log.info("Tika extracted text: %s", text)
|
||||
|
||||
return [Document(page_content=text, metadata=headers)]
|
||||
else:
|
||||
raise Exception(f"Error calling Tika: {r.reason}")
|
||||
|
||||
|
||||
class Loader:
|
||||
def __init__(self, engine: str = "", **kwargs):
|
||||
self.engine = engine
|
||||
self.kwargs = kwargs
|
||||
|
||||
def load(
|
||||
self, filename: str, file_content_type: str, file_path: str
|
||||
) -> list[Document]:
|
||||
loader = self._get_loader(filename, file_content_type, file_path)
|
||||
docs = loader.load()
|
||||
|
||||
return [
|
||||
Document(
|
||||
page_content=ftfy.fix_text(doc.page_content), metadata=doc.metadata
|
||||
)
|
||||
for doc in docs
|
||||
]
|
||||
|
||||
def _get_loader(self, filename: str, file_content_type: str, file_path: str):
|
||||
file_ext = filename.split(".")[-1].lower()
|
||||
|
||||
if self.engine == "tika" and self.kwargs.get("TIKA_SERVER_URL"):
|
||||
if file_ext in known_source_ext or (
|
||||
file_content_type and file_content_type.find("text/") >= 0
|
||||
):
|
||||
loader = TextLoader(file_path, autodetect_encoding=True)
|
||||
else:
|
||||
loader = TikaLoader(
|
||||
url=self.kwargs.get("TIKA_SERVER_URL"),
|
||||
file_path=file_path,
|
||||
mime_type=file_content_type,
|
||||
)
|
||||
else:
|
||||
if file_ext == "pdf":
|
||||
loader = PyPDFLoader(
|
||||
file_path, extract_images=self.kwargs.get("PDF_EXTRACT_IMAGES")
|
||||
)
|
||||
elif file_ext == "csv":
|
||||
loader = CSVLoader(file_path)
|
||||
elif file_ext == "rst":
|
||||
loader = UnstructuredRSTLoader(file_path, mode="elements")
|
||||
elif file_ext == "xml":
|
||||
loader = UnstructuredXMLLoader(file_path)
|
||||
elif file_ext in ["htm", "html"]:
|
||||
loader = BSHTMLLoader(file_path, open_encoding="unicode_escape")
|
||||
elif file_ext == "md":
|
||||
loader = UnstructuredMarkdownLoader(file_path)
|
||||
elif file_content_type == "application/epub+zip":
|
||||
loader = UnstructuredEPubLoader(file_path)
|
||||
elif (
|
||||
file_content_type
|
||||
== "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||
or file_ext == "docx"
|
||||
):
|
||||
loader = Docx2txtLoader(file_path)
|
||||
elif file_content_type in [
|
||||
"application/vnd.ms-excel",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
] or file_ext in ["xls", "xlsx"]:
|
||||
loader = UnstructuredExcelLoader(file_path)
|
||||
elif file_content_type in [
|
||||
"application/vnd.ms-powerpoint",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
] or file_ext in ["ppt", "pptx"]:
|
||||
loader = UnstructuredPowerPointLoader(file_path)
|
||||
elif file_ext == "msg":
|
||||
loader = OutlookMessageLoader(file_path)
|
||||
elif file_ext in known_source_ext or (
|
||||
file_content_type and file_content_type.find("text/") >= 0
|
||||
):
|
||||
loader = TextLoader(file_path, autodetect_encoding=True)
|
||||
else:
|
||||
loader = TextLoader(file_path, autodetect_encoding=True)
|
||||
|
||||
return loader
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,81 @@
|
||||
import os
|
||||
import torch
|
||||
import numpy as np
|
||||
from colbert.infra import ColBERTConfig
|
||||
from colbert.modeling.checkpoint import Checkpoint
|
||||
|
||||
|
||||
class ColBERT:
|
||||
def __init__(self, name, **kwargs) -> None:
|
||||
print("ColBERT: Loading model", name)
|
||||
self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||
|
||||
DOCKER = kwargs.get("env") == "docker"
|
||||
if DOCKER:
|
||||
# This is a workaround for the issue with the docker container
|
||||
# where the torch extension is not loaded properly
|
||||
# and the following error is thrown:
|
||||
# /root/.cache/torch_extensions/py311_cpu/segmented_maxsim_cpp/segmented_maxsim_cpp.so: cannot open shared object file: No such file or directory
|
||||
|
||||
lock_file = (
|
||||
"/root/.cache/torch_extensions/py311_cpu/segmented_maxsim_cpp/lock"
|
||||
)
|
||||
if os.path.exists(lock_file):
|
||||
os.remove(lock_file)
|
||||
|
||||
self.ckpt = Checkpoint(
|
||||
name,
|
||||
colbert_config=ColBERTConfig(model_name=name),
|
||||
).to(self.device)
|
||||
pass
|
||||
|
||||
def calculate_similarity_scores(self, query_embeddings, document_embeddings):
|
||||
|
||||
query_embeddings = query_embeddings.to(self.device)
|
||||
document_embeddings = document_embeddings.to(self.device)
|
||||
|
||||
# Validate dimensions to ensure compatibility
|
||||
if query_embeddings.dim() != 3:
|
||||
raise ValueError(
|
||||
f"Expected query embeddings to have 3 dimensions, but got {query_embeddings.dim()}."
|
||||
)
|
||||
if document_embeddings.dim() != 3:
|
||||
raise ValueError(
|
||||
f"Expected document embeddings to have 3 dimensions, but got {document_embeddings.dim()}."
|
||||
)
|
||||
if query_embeddings.size(0) not in [1, document_embeddings.size(0)]:
|
||||
raise ValueError(
|
||||
"There should be either one query or queries equal to the number of documents."
|
||||
)
|
||||
|
||||
# Transpose the query embeddings to align for matrix multiplication
|
||||
transposed_query_embeddings = query_embeddings.permute(0, 2, 1)
|
||||
# Compute similarity scores using batch matrix multiplication
|
||||
computed_scores = torch.matmul(document_embeddings, transposed_query_embeddings)
|
||||
# Apply max pooling to extract the highest semantic similarity across each document's sequence
|
||||
maximum_scores = torch.max(computed_scores, dim=1).values
|
||||
|
||||
# Sum up the maximum scores across features to get the overall document relevance scores
|
||||
final_scores = maximum_scores.sum(dim=1)
|
||||
|
||||
normalized_scores = torch.softmax(final_scores, dim=0)
|
||||
|
||||
return normalized_scores.detach().cpu().numpy().astype(np.float32)
|
||||
|
||||
def predict(self, sentences):
|
||||
|
||||
query = sentences[0][0]
|
||||
docs = [i[1] for i in sentences]
|
||||
|
||||
# Embedding the documents
|
||||
embedded_docs = self.ckpt.docFromText(docs, bsize=32)[0]
|
||||
# Embedding the queries
|
||||
embedded_queries = self.ckpt.queryFromText([query], bsize=32)
|
||||
embedded_query = embedded_queries[0]
|
||||
|
||||
# Calculate retrieval scores for the query against all documents
|
||||
scores = self.calculate_similarity_scores(
|
||||
embedded_query.unsqueeze(0), embedded_docs
|
||||
)
|
||||
|
||||
return scores
|
||||
+167
-117
@@ -1,42 +1,84 @@
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from typing import Optional, Union
|
||||
|
||||
import requests
|
||||
from open_webui.apps.ollama.main import (
|
||||
GenerateEmbeddingsForm,
|
||||
generate_ollama_embeddings,
|
||||
)
|
||||
from open_webui.config import CHROMA_CLIENT
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
|
||||
from huggingface_hub import snapshot_download
|
||||
from langchain.retrievers import ContextualCompressionRetriever, EnsembleRetriever
|
||||
from langchain_community.retrievers import BM25Retriever
|
||||
from langchain_core.documents import Document
|
||||
|
||||
|
||||
from open_webui.apps.ollama.main import (
|
||||
GenerateEmbeddingsForm,
|
||||
generate_ollama_embeddings,
|
||||
)
|
||||
from open_webui.apps.retrieval.vector.connector import VECTOR_DB_CLIENT
|
||||
from open_webui.utils.misc import get_last_user_message
|
||||
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||
|
||||
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.callbacks import CallbackManagerForRetrieverRun
|
||||
from langchain_core.retrievers import BaseRetriever
|
||||
|
||||
|
||||
class VectorSearchRetriever(BaseRetriever):
|
||||
collection_name: Any
|
||||
embedding_function: Any
|
||||
top_k: int
|
||||
|
||||
def _get_relevant_documents(
|
||||
self,
|
||||
query: str,
|
||||
*,
|
||||
run_manager: CallbackManagerForRetrieverRun,
|
||||
) -> list[Document]:
|
||||
result = VECTOR_DB_CLIENT.search(
|
||||
collection_name=self.collection_name,
|
||||
vectors=[self.embedding_function(query)],
|
||||
limit=self.top_k,
|
||||
)
|
||||
|
||||
ids = result.ids[0]
|
||||
metadatas = result.metadatas[0]
|
||||
documents = result.documents[0]
|
||||
|
||||
results = []
|
||||
for idx in range(len(ids)):
|
||||
results.append(
|
||||
Document(
|
||||
metadata=metadatas[idx],
|
||||
page_content=documents[idx],
|
||||
)
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
def query_doc(
|
||||
collection_name: str,
|
||||
query: str,
|
||||
embedding_function,
|
||||
query_embedding: list[float],
|
||||
k: int,
|
||||
):
|
||||
try:
|
||||
collection = CHROMA_CLIENT.get_collection(name=collection_name)
|
||||
query_embeddings = embedding_function(query)
|
||||
|
||||
result = collection.query(
|
||||
query_embeddings=[query_embeddings],
|
||||
n_results=k,
|
||||
result = VECTOR_DB_CLIENT.search(
|
||||
collection_name=collection_name,
|
||||
vectors=[query_embedding],
|
||||
limit=k,
|
||||
)
|
||||
|
||||
log.info(f"query_doc:result {result}")
|
||||
return result
|
||||
except Exception as e:
|
||||
print(e)
|
||||
raise e
|
||||
|
||||
|
||||
@@ -47,27 +89,25 @@ def query_doc_with_hybrid_search(
|
||||
k: int,
|
||||
reranking_function,
|
||||
r: float,
|
||||
):
|
||||
) -> dict:
|
||||
try:
|
||||
collection = CHROMA_CLIENT.get_collection(name=collection_name)
|
||||
documents = collection.get() # get all documents
|
||||
result = VECTOR_DB_CLIENT.get(collection_name=collection_name)
|
||||
|
||||
bm25_retriever = BM25Retriever.from_texts(
|
||||
texts=documents.get("documents"),
|
||||
metadatas=documents.get("metadatas"),
|
||||
texts=result.documents[0],
|
||||
metadatas=result.metadatas[0],
|
||||
)
|
||||
bm25_retriever.k = k
|
||||
|
||||
chroma_retriever = ChromaRetriever(
|
||||
collection=collection,
|
||||
vector_search_retriever = VectorSearchRetriever(
|
||||
collection_name=collection_name,
|
||||
embedding_function=embedding_function,
|
||||
top_n=k,
|
||||
top_k=k,
|
||||
)
|
||||
|
||||
ensemble_retriever = EnsembleRetriever(
|
||||
retrievers=[bm25_retriever, chroma_retriever], weights=[0.5, 0.5]
|
||||
retrievers=[bm25_retriever, vector_search_retriever], weights=[0.5, 0.5]
|
||||
)
|
||||
|
||||
compressor = RerankCompressor(
|
||||
embedding_function=embedding_function,
|
||||
top_n=k,
|
||||
@@ -92,7 +132,9 @@ def query_doc_with_hybrid_search(
|
||||
raise e
|
||||
|
||||
|
||||
def merge_and_sort_query_results(query_results, k, reverse=False):
|
||||
def merge_and_sort_query_results(
|
||||
query_results: list[dict], k: int, reverse: bool = False
|
||||
) -> list[dict]:
|
||||
# Initialize lists to store combined data
|
||||
combined_distances = []
|
||||
combined_documents = []
|
||||
@@ -138,20 +180,22 @@ def query_collection(
|
||||
query: str,
|
||||
embedding_function,
|
||||
k: int,
|
||||
):
|
||||
) -> dict:
|
||||
|
||||
results = []
|
||||
query_embedding = embedding_function(query)
|
||||
|
||||
for collection_name in collection_names:
|
||||
if collection_name:
|
||||
try:
|
||||
result = query_doc(
|
||||
collection_name=collection_name,
|
||||
query=query,
|
||||
k=k,
|
||||
embedding_function=embedding_function,
|
||||
query_embedding=query_embedding,
|
||||
)
|
||||
results.append(result)
|
||||
except Exception:
|
||||
pass
|
||||
results.append(result.model_dump())
|
||||
except Exception as e:
|
||||
log.exception(f"Error when querying the collection: {e}")
|
||||
else:
|
||||
pass
|
||||
|
||||
@@ -165,8 +209,9 @@ def query_collection_with_hybrid_search(
|
||||
k: int,
|
||||
reranking_function,
|
||||
r: float,
|
||||
):
|
||||
) -> dict:
|
||||
results = []
|
||||
error = False
|
||||
for collection_name in collection_names:
|
||||
try:
|
||||
result = query_doc_with_hybrid_search(
|
||||
@@ -178,14 +223,39 @@ def query_collection_with_hybrid_search(
|
||||
r=r,
|
||||
)
|
||||
results.append(result)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
log.exception(
|
||||
"Error when querying the collection with " f"hybrid_search: {e}"
|
||||
)
|
||||
error = True
|
||||
|
||||
if error:
|
||||
raise Exception(
|
||||
"Hybrid search failed for all collections. Using Non hybrid search as fallback."
|
||||
)
|
||||
|
||||
return merge_and_sort_query_results(results, k=k, reverse=True)
|
||||
|
||||
|
||||
def rag_template(template: str, context: str, query: str):
|
||||
template = template.replace("[context]", context)
|
||||
template = template.replace("[query]", query)
|
||||
count = template.count("[context]")
|
||||
assert "[context]" in template, "RAG template does not contain '[context]'"
|
||||
|
||||
if "<context>" in context and "</context>" in context:
|
||||
log.debug(
|
||||
"WARNING: Potential prompt injection attack: the RAG "
|
||||
"context contains '<context>' and '</context>'. This might be "
|
||||
"nothing, or the user might be trying to hack something."
|
||||
)
|
||||
|
||||
if "[query]" in context:
|
||||
query_placeholder = f"[query-{str(uuid.uuid4())}]"
|
||||
template = template.replace("[query]", query_placeholder)
|
||||
template = template.replace("[context]", context)
|
||||
template = template.replace(query_placeholder, query)
|
||||
else:
|
||||
template = template.replace("[context]", context)
|
||||
template = template.replace("[query]", query)
|
||||
return template
|
||||
|
||||
|
||||
@@ -248,51 +318,71 @@ def get_rag_context(
|
||||
relevant_contexts = []
|
||||
|
||||
for file in files:
|
||||
context = None
|
||||
|
||||
collection_names = (
|
||||
file["collection_names"]
|
||||
if file["type"] == "collection"
|
||||
else [file["collection_name"]] if file["collection_name"] else []
|
||||
)
|
||||
|
||||
collection_names = set(collection_names).difference(extracted_collections)
|
||||
if not collection_names:
|
||||
log.debug(f"skipping {file} as it has already been extracted")
|
||||
continue
|
||||
|
||||
try:
|
||||
if file["type"] == "text":
|
||||
context = file["content"]
|
||||
else:
|
||||
if hybrid_search:
|
||||
context = query_collection_with_hybrid_search(
|
||||
collection_names=collection_names,
|
||||
query=query,
|
||||
embedding_function=embedding_function,
|
||||
k=k,
|
||||
reranking_function=reranking_function,
|
||||
r=r,
|
||||
)
|
||||
else:
|
||||
context = query_collection(
|
||||
collection_names=collection_names,
|
||||
query=query,
|
||||
embedding_function=embedding_function,
|
||||
k=k,
|
||||
)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
if file.get("context") == "full":
|
||||
context = {
|
||||
"documents": [[file.get("file").get("data", {}).get("content")]],
|
||||
"metadatas": [[{"file_id": file.get("id"), "name": file.get("name")}]],
|
||||
}
|
||||
else:
|
||||
context = None
|
||||
|
||||
if context:
|
||||
relevant_contexts.append({**context, "source": file})
|
||||
collection_names = []
|
||||
if file.get("type") == "collection":
|
||||
if file.get("legacy"):
|
||||
collection_names = file.get("collection_names", [])
|
||||
else:
|
||||
collection_names.append(file["id"])
|
||||
elif file.get("collection_name"):
|
||||
collection_names.append(file["collection_name"])
|
||||
elif file.get("id"):
|
||||
if file.get("legacy"):
|
||||
collection_names.append(f"{file['id']}")
|
||||
else:
|
||||
collection_names.append(f"file-{file['id']}")
|
||||
|
||||
extracted_collections.extend(collection_names)
|
||||
collection_names = set(collection_names).difference(extracted_collections)
|
||||
if not collection_names:
|
||||
log.debug(f"skipping {file} as it has already been extracted")
|
||||
continue
|
||||
|
||||
try:
|
||||
context = None
|
||||
if file.get("type") == "text":
|
||||
context = file["content"]
|
||||
else:
|
||||
if hybrid_search:
|
||||
try:
|
||||
context = query_collection_with_hybrid_search(
|
||||
collection_names=collection_names,
|
||||
query=query,
|
||||
embedding_function=embedding_function,
|
||||
k=k,
|
||||
reranking_function=reranking_function,
|
||||
r=r,
|
||||
)
|
||||
except Exception as e:
|
||||
log.debug(
|
||||
"Error when using hybrid search, using"
|
||||
" non hybrid search as fallback."
|
||||
)
|
||||
|
||||
if (not hybrid_search) or (context is None):
|
||||
context = query_collection(
|
||||
collection_names=collection_names,
|
||||
query=query,
|
||||
embedding_function=embedding_function,
|
||||
k=k,
|
||||
)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
||||
extracted_collections.extend(collection_names)
|
||||
|
||||
if context:
|
||||
relevant_contexts.append({**context, "file": file})
|
||||
|
||||
contexts = []
|
||||
citations = []
|
||||
|
||||
for context in relevant_contexts:
|
||||
try:
|
||||
if "documents" in context:
|
||||
@@ -305,7 +395,7 @@ def get_rag_context(
|
||||
if "metadatas" in context:
|
||||
citations.append(
|
||||
{
|
||||
"source": context["source"],
|
||||
"source": context["file"],
|
||||
"document": context["documents"][0],
|
||||
"metadata": context["metadatas"][0],
|
||||
}
|
||||
@@ -391,51 +481,11 @@ def generate_openai_batch_embeddings(
|
||||
return None
|
||||
|
||||
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.callbacks import CallbackManagerForRetrieverRun
|
||||
from langchain_core.retrievers import BaseRetriever
|
||||
|
||||
|
||||
class ChromaRetriever(BaseRetriever):
|
||||
collection: Any
|
||||
embedding_function: Any
|
||||
top_n: int
|
||||
|
||||
def _get_relevant_documents(
|
||||
self,
|
||||
query: str,
|
||||
*,
|
||||
run_manager: CallbackManagerForRetrieverRun,
|
||||
) -> list[Document]:
|
||||
query_embeddings = self.embedding_function(query)
|
||||
|
||||
results = self.collection.query(
|
||||
query_embeddings=[query_embeddings],
|
||||
n_results=self.top_n,
|
||||
)
|
||||
|
||||
ids = results["ids"][0]
|
||||
metadatas = results["metadatas"][0]
|
||||
documents = results["documents"][0]
|
||||
|
||||
results = []
|
||||
for idx in range(len(ids)):
|
||||
results.append(
|
||||
Document(
|
||||
metadata=metadatas[idx],
|
||||
page_content=documents[idx],
|
||||
)
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
import operator
|
||||
from typing import Optional, Sequence
|
||||
|
||||
from langchain_core.callbacks import Callbacks
|
||||
from langchain_core.documents import BaseDocumentCompressor, Document
|
||||
from langchain_core.pydantic_v1 import Extra
|
||||
|
||||
|
||||
class RerankCompressor(BaseDocumentCompressor):
|
||||
@@ -445,7 +495,7 @@ class RerankCompressor(BaseDocumentCompressor):
|
||||
r_score: float
|
||||
|
||||
class Config:
|
||||
extra = Extra.forbid
|
||||
extra = "forbid"
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
def compress_documents(
|
||||
@@ -0,0 +1,10 @@
|
||||
from open_webui.config import VECTOR_DB
|
||||
|
||||
if VECTOR_DB == "milvus":
|
||||
from open_webui.apps.retrieval.vector.dbs.milvus import MilvusClient
|
||||
|
||||
VECTOR_DB_CLIENT = MilvusClient()
|
||||
else:
|
||||
from open_webui.apps.retrieval.vector.dbs.chroma import ChromaClient
|
||||
|
||||
VECTOR_DB_CLIENT = ChromaClient()
|
||||
@@ -0,0 +1,157 @@
|
||||
import chromadb
|
||||
from chromadb import Settings
|
||||
from chromadb.utils.batch_utils import create_batches
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from open_webui.apps.retrieval.vector.main import VectorItem, SearchResult, GetResult
|
||||
from open_webui.config import (
|
||||
CHROMA_DATA_PATH,
|
||||
CHROMA_HTTP_HOST,
|
||||
CHROMA_HTTP_PORT,
|
||||
CHROMA_HTTP_HEADERS,
|
||||
CHROMA_HTTP_SSL,
|
||||
CHROMA_TENANT,
|
||||
CHROMA_DATABASE,
|
||||
)
|
||||
|
||||
|
||||
class ChromaClient:
|
||||
def __init__(self):
|
||||
if CHROMA_HTTP_HOST != "":
|
||||
self.client = chromadb.HttpClient(
|
||||
host=CHROMA_HTTP_HOST,
|
||||
port=CHROMA_HTTP_PORT,
|
||||
headers=CHROMA_HTTP_HEADERS,
|
||||
ssl=CHROMA_HTTP_SSL,
|
||||
tenant=CHROMA_TENANT,
|
||||
database=CHROMA_DATABASE,
|
||||
settings=Settings(allow_reset=True, anonymized_telemetry=False),
|
||||
)
|
||||
else:
|
||||
self.client = chromadb.PersistentClient(
|
||||
path=CHROMA_DATA_PATH,
|
||||
settings=Settings(allow_reset=True, anonymized_telemetry=False),
|
||||
tenant=CHROMA_TENANT,
|
||||
database=CHROMA_DATABASE,
|
||||
)
|
||||
|
||||
def has_collection(self, collection_name: str) -> bool:
|
||||
# Check if the collection exists based on the collection name.
|
||||
collections = self.client.list_collections()
|
||||
return collection_name in [collection.name for collection in collections]
|
||||
|
||||
def delete_collection(self, collection_name: str):
|
||||
# Delete the collection based on the collection name.
|
||||
return self.client.delete_collection(name=collection_name)
|
||||
|
||||
def search(
|
||||
self, collection_name: str, vectors: list[list[float | int]], limit: int
|
||||
) -> Optional[SearchResult]:
|
||||
# Search for the nearest neighbor items based on the vectors and return 'limit' number of results.
|
||||
try:
|
||||
collection = self.client.get_collection(name=collection_name)
|
||||
if collection:
|
||||
result = collection.query(
|
||||
query_embeddings=vectors,
|
||||
n_results=limit,
|
||||
)
|
||||
|
||||
return SearchResult(
|
||||
**{
|
||||
"ids": result["ids"],
|
||||
"distances": result["distances"],
|
||||
"documents": result["documents"],
|
||||
"metadatas": result["metadatas"],
|
||||
}
|
||||
)
|
||||
return None
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
def query(
|
||||
self, collection_name: str, filter: dict, limit: Optional[int] = None
|
||||
) -> Optional[GetResult]:
|
||||
# Query the items from the collection based on the filter.
|
||||
try:
|
||||
collection = self.client.get_collection(name=collection_name)
|
||||
if collection:
|
||||
result = collection.get(
|
||||
where=filter,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
return GetResult(
|
||||
**{
|
||||
"ids": [result["ids"]],
|
||||
"documents": [result["documents"]],
|
||||
"metadatas": [result["metadatas"]],
|
||||
}
|
||||
)
|
||||
return None
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return None
|
||||
|
||||
def get(self, collection_name: str) -> Optional[GetResult]:
|
||||
# Get all the items in the collection.
|
||||
collection = self.client.get_collection(name=collection_name)
|
||||
if collection:
|
||||
result = collection.get()
|
||||
return GetResult(
|
||||
**{
|
||||
"ids": [result["ids"]],
|
||||
"documents": [result["documents"]],
|
||||
"metadatas": [result["metadatas"]],
|
||||
}
|
||||
)
|
||||
return None
|
||||
|
||||
def insert(self, collection_name: str, items: list[VectorItem]):
|
||||
# Insert the items into the collection, if the collection does not exist, it will be created.
|
||||
collection = self.client.get_or_create_collection(name=collection_name)
|
||||
|
||||
ids = [item["id"] for item in items]
|
||||
documents = [item["text"] for item in items]
|
||||
embeddings = [item["vector"] for item in items]
|
||||
metadatas = [item["metadata"] for item in items]
|
||||
|
||||
for batch in create_batches(
|
||||
api=self.client,
|
||||
documents=documents,
|
||||
embeddings=embeddings,
|
||||
ids=ids,
|
||||
metadatas=metadatas,
|
||||
):
|
||||
collection.add(*batch)
|
||||
|
||||
def upsert(self, collection_name: str, items: list[VectorItem]):
|
||||
# Update the items in the collection, if the items are not present, insert them. If the collection does not exist, it will be created.
|
||||
collection = self.client.get_or_create_collection(name=collection_name)
|
||||
|
||||
ids = [item["id"] for item in items]
|
||||
documents = [item["text"] for item in items]
|
||||
embeddings = [item["vector"] for item in items]
|
||||
metadatas = [item["metadata"] for item in items]
|
||||
|
||||
collection.upsert(
|
||||
ids=ids, documents=documents, embeddings=embeddings, metadatas=metadatas
|
||||
)
|
||||
|
||||
def delete(
|
||||
self,
|
||||
collection_name: str,
|
||||
ids: Optional[list[str]] = None,
|
||||
filter: Optional[dict] = None,
|
||||
):
|
||||
# Delete the items from the collection based on the ids.
|
||||
collection = self.client.get_collection(name=collection_name)
|
||||
if collection:
|
||||
if ids:
|
||||
collection.delete(ids=ids)
|
||||
elif filter:
|
||||
collection.delete(where=filter)
|
||||
|
||||
def reset(self):
|
||||
# Resets the database. This will delete all collections and item entries.
|
||||
return self.client.reset()
|
||||
@@ -0,0 +1,286 @@
|
||||
from pymilvus import MilvusClient as Client
|
||||
from pymilvus import FieldSchema, DataType
|
||||
import json
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from open_webui.apps.retrieval.vector.main import VectorItem, SearchResult, GetResult
|
||||
from open_webui.config import (
|
||||
MILVUS_URI,
|
||||
)
|
||||
|
||||
|
||||
class MilvusClient:
|
||||
def __init__(self):
|
||||
self.collection_prefix = "open_webui"
|
||||
self.client = Client(uri=MILVUS_URI)
|
||||
|
||||
def _result_to_get_result(self, result) -> GetResult:
|
||||
ids = []
|
||||
documents = []
|
||||
metadatas = []
|
||||
|
||||
for match in result:
|
||||
_ids = []
|
||||
_documents = []
|
||||
_metadatas = []
|
||||
for item in match:
|
||||
_ids.append(item.get("id"))
|
||||
_documents.append(item.get("data", {}).get("text"))
|
||||
_metadatas.append(item.get("metadata"))
|
||||
|
||||
ids.append(_ids)
|
||||
documents.append(_documents)
|
||||
metadatas.append(_metadatas)
|
||||
|
||||
return GetResult(
|
||||
**{
|
||||
"ids": ids,
|
||||
"documents": documents,
|
||||
"metadatas": metadatas,
|
||||
}
|
||||
)
|
||||
|
||||
def _result_to_search_result(self, result) -> SearchResult:
|
||||
ids = []
|
||||
distances = []
|
||||
documents = []
|
||||
metadatas = []
|
||||
|
||||
for match in result:
|
||||
_ids = []
|
||||
_distances = []
|
||||
_documents = []
|
||||
_metadatas = []
|
||||
|
||||
for item in match:
|
||||
_ids.append(item.get("id"))
|
||||
_distances.append(item.get("distance"))
|
||||
_documents.append(item.get("entity", {}).get("data", {}).get("text"))
|
||||
_metadatas.append(item.get("entity", {}).get("metadata"))
|
||||
|
||||
ids.append(_ids)
|
||||
distances.append(_distances)
|
||||
documents.append(_documents)
|
||||
metadatas.append(_metadatas)
|
||||
|
||||
return SearchResult(
|
||||
**{
|
||||
"ids": ids,
|
||||
"distances": distances,
|
||||
"documents": documents,
|
||||
"metadatas": metadatas,
|
||||
}
|
||||
)
|
||||
|
||||
def _create_collection(self, collection_name: str, dimension: int):
|
||||
schema = self.client.create_schema(
|
||||
auto_id=False,
|
||||
enable_dynamic_field=True,
|
||||
)
|
||||
schema.add_field(
|
||||
field_name="id",
|
||||
datatype=DataType.VARCHAR,
|
||||
is_primary=True,
|
||||
max_length=65535,
|
||||
)
|
||||
schema.add_field(
|
||||
field_name="vector",
|
||||
datatype=DataType.FLOAT_VECTOR,
|
||||
dim=dimension,
|
||||
description="vector",
|
||||
)
|
||||
schema.add_field(field_name="data", datatype=DataType.JSON, description="data")
|
||||
schema.add_field(
|
||||
field_name="metadata", datatype=DataType.JSON, description="metadata"
|
||||
)
|
||||
|
||||
index_params = self.client.prepare_index_params()
|
||||
index_params.add_index(
|
||||
field_name="vector",
|
||||
index_type="HNSW",
|
||||
metric_type="COSINE",
|
||||
params={"M": 16, "efConstruction": 100},
|
||||
)
|
||||
|
||||
self.client.create_collection(
|
||||
collection_name=f"{self.collection_prefix}_{collection_name}",
|
||||
schema=schema,
|
||||
index_params=index_params,
|
||||
)
|
||||
|
||||
def has_collection(self, collection_name: str) -> bool:
|
||||
# Check if the collection exists based on the collection name.
|
||||
collection_name = collection_name.replace("-", "_")
|
||||
return self.client.has_collection(
|
||||
collection_name=f"{self.collection_prefix}_{collection_name}"
|
||||
)
|
||||
|
||||
def delete_collection(self, collection_name: str):
|
||||
# Delete the collection based on the collection name.
|
||||
collection_name = collection_name.replace("-", "_")
|
||||
return self.client.drop_collection(
|
||||
collection_name=f"{self.collection_prefix}_{collection_name}"
|
||||
)
|
||||
|
||||
def search(
|
||||
self, collection_name: str, vectors: list[list[float | int]], limit: int
|
||||
) -> Optional[SearchResult]:
|
||||
# Search for the nearest neighbor items based on the vectors and return 'limit' number of results.
|
||||
collection_name = collection_name.replace("-", "_")
|
||||
result = self.client.search(
|
||||
collection_name=f"{self.collection_prefix}_{collection_name}",
|
||||
data=vectors,
|
||||
limit=limit,
|
||||
output_fields=["data", "metadata"],
|
||||
)
|
||||
|
||||
return self._result_to_search_result(result)
|
||||
|
||||
def query(self, collection_name: str, filter: dict, limit: Optional[int] = None):
|
||||
# Construct the filter string for querying
|
||||
collection_name = collection_name.replace("-", "_")
|
||||
if not self.has_collection(collection_name):
|
||||
return None
|
||||
|
||||
filter_string = " && ".join(
|
||||
[
|
||||
f'metadata["{key}"] == {json.dumps(value)}'
|
||||
for key, value in filter.items()
|
||||
]
|
||||
)
|
||||
|
||||
max_limit = 16383 # The maximum number of records per request
|
||||
all_results = []
|
||||
|
||||
if limit is None:
|
||||
limit = float("inf") # Use infinity as a placeholder for no limit
|
||||
|
||||
# Initialize offset and remaining to handle pagination
|
||||
offset = 0
|
||||
remaining = limit
|
||||
|
||||
try:
|
||||
# Loop until there are no more items to fetch or the desired limit is reached
|
||||
while remaining > 0:
|
||||
print("remaining", remaining)
|
||||
current_fetch = min(
|
||||
max_limit, remaining
|
||||
) # Determine how many items to fetch in this iteration
|
||||
|
||||
results = self.client.query(
|
||||
collection_name=f"{self.collection_prefix}_{collection_name}",
|
||||
filter=filter_string,
|
||||
output_fields=["*"],
|
||||
limit=current_fetch,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
if not results:
|
||||
break
|
||||
|
||||
all_results.extend(results)
|
||||
results_count = len(results)
|
||||
remaining -= (
|
||||
results_count # Decrease remaining by the number of items fetched
|
||||
)
|
||||
offset += results_count
|
||||
|
||||
# Break the loop if the results returned are less than the requested fetch count
|
||||
if results_count < current_fetch:
|
||||
break
|
||||
|
||||
print(all_results)
|
||||
return self._result_to_get_result([all_results])
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return None
|
||||
|
||||
def get(self, collection_name: str) -> Optional[GetResult]:
|
||||
# Get all the items in the collection.
|
||||
collection_name = collection_name.replace("-", "_")
|
||||
result = self.client.query(
|
||||
collection_name=f"{self.collection_prefix}_{collection_name}",
|
||||
filter='id != ""',
|
||||
)
|
||||
return self._result_to_get_result([result])
|
||||
|
||||
def insert(self, collection_name: str, items: list[VectorItem]):
|
||||
# Insert the items into the collection, if the collection does not exist, it will be created.
|
||||
collection_name = collection_name.replace("-", "_")
|
||||
if not self.client.has_collection(
|
||||
collection_name=f"{self.collection_prefix}_{collection_name}"
|
||||
):
|
||||
self._create_collection(
|
||||
collection_name=collection_name, dimension=len(items[0]["vector"])
|
||||
)
|
||||
|
||||
return self.client.insert(
|
||||
collection_name=f"{self.collection_prefix}_{collection_name}",
|
||||
data=[
|
||||
{
|
||||
"id": item["id"],
|
||||
"vector": item["vector"],
|
||||
"data": {"text": item["text"]},
|
||||
"metadata": item["metadata"],
|
||||
}
|
||||
for item in items
|
||||
],
|
||||
)
|
||||
|
||||
def upsert(self, collection_name: str, items: list[VectorItem]):
|
||||
# Update the items in the collection, if the items are not present, insert them. If the collection does not exist, it will be created.
|
||||
collection_name = collection_name.replace("-", "_")
|
||||
if not self.client.has_collection(
|
||||
collection_name=f"{self.collection_prefix}_{collection_name}"
|
||||
):
|
||||
self._create_collection(
|
||||
collection_name=collection_name, dimension=len(items[0]["vector"])
|
||||
)
|
||||
|
||||
return self.client.upsert(
|
||||
collection_name=f"{self.collection_prefix}_{collection_name}",
|
||||
data=[
|
||||
{
|
||||
"id": item["id"],
|
||||
"vector": item["vector"],
|
||||
"data": {"text": item["text"]},
|
||||
"metadata": item["metadata"],
|
||||
}
|
||||
for item in items
|
||||
],
|
||||
)
|
||||
|
||||
def delete(
|
||||
self,
|
||||
collection_name: str,
|
||||
ids: Optional[list[str]] = None,
|
||||
filter: Optional[dict] = None,
|
||||
):
|
||||
# Delete the items from the collection based on the ids.
|
||||
collection_name = collection_name.replace("-", "_")
|
||||
if ids:
|
||||
return self.client.delete(
|
||||
collection_name=f"{self.collection_prefix}_{collection_name}",
|
||||
ids=ids,
|
||||
)
|
||||
elif filter:
|
||||
# Convert the filter dictionary to a string using JSON_CONTAINS.
|
||||
filter_string = " && ".join(
|
||||
[
|
||||
f'metadata["{key}"] == {json.dumps(value)}'
|
||||
for key, value in filter.items()
|
||||
]
|
||||
)
|
||||
|
||||
return self.client.delete(
|
||||
collection_name=f"{self.collection_prefix}_{collection_name}",
|
||||
filter=filter_string,
|
||||
)
|
||||
|
||||
def reset(self):
|
||||
# Resets the database. This will delete all collections and item entries.
|
||||
collection_names = self.client.list_collections()
|
||||
for collection_name in collection_names:
|
||||
if collection_name.startswith(self.collection_prefix):
|
||||
self.client.drop_collection(collection_name=collection_name)
|
||||
@@ -0,0 +1,19 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List, Any
|
||||
|
||||
|
||||
class VectorItem(BaseModel):
|
||||
id: str
|
||||
text: str
|
||||
vector: List[float | int]
|
||||
metadata: Any
|
||||
|
||||
|
||||
class GetResult(BaseModel):
|
||||
ids: Optional[List[List[str]]]
|
||||
documents: Optional[List[List[str]]]
|
||||
metadatas: Optional[List[List[Any]]]
|
||||
|
||||
|
||||
class SearchResult(GetResult):
|
||||
distances: Optional[List[List[float | int]]]
|
||||
+1
-1
@@ -2,7 +2,7 @@ import logging
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
from open_webui.apps.rag.search.main import SearchResult, get_filtered_results
|
||||
from open_webui.apps.retrieval.web.main import SearchResult, get_filtered_results
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from open_webui.apps.rag.search.main import SearchResult, get_filtered_results
|
||||
from open_webui.apps.retrieval.web.main import SearchResult, get_filtered_results
|
||||
from duckduckgo_search import DDGS
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import logging
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
from open_webui.apps.rag.search.main import SearchResult, get_filtered_results
|
||||
from open_webui.apps.retrieval.web.main import SearchResult, get_filtered_results
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
|
||||
import requests
|
||||
from open_webui.apps.rag.search.main import SearchResult
|
||||
from open_webui.apps.retrieval.web.main import SearchResult
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
from yarl import URL
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ from typing import Optional
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import requests
|
||||
from open_webui.apps.rag.search.main import SearchResult, get_filtered_results
|
||||
from open_webui.apps.retrieval.web.main import SearchResult, get_filtered_results
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
+1
-1
@@ -2,7 +2,7 @@ import logging
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
from open_webui.apps.rag.search.main import SearchResult, get_filtered_results
|
||||
from open_webui.apps.retrieval.web.main import SearchResult, get_filtered_results
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
+1
-1
@@ -3,7 +3,7 @@ import logging
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
from open_webui.apps.rag.search.main import SearchResult, get_filtered_results
|
||||
from open_webui.apps.retrieval.web.main import SearchResult, get_filtered_results
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
+1
-1
@@ -3,7 +3,7 @@ from typing import Optional
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import requests
|
||||
from open_webui.apps.rag.search.main import SearchResult, get_filtered_results
|
||||
from open_webui.apps.retrieval.web.main import SearchResult, get_filtered_results
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
+1
-1
@@ -2,7 +2,7 @@ import logging
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
from open_webui.apps.rag.search.main import SearchResult, get_filtered_results
|
||||
from open_webui.apps.retrieval.web.main import SearchResult, get_filtered_results
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
|
||||
import requests
|
||||
from open_webui.apps.rag.search.main import SearchResult
|
||||
from open_webui.apps.retrieval.web.main import SearchResult
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -0,0 +1,97 @@
|
||||
import socket
|
||||
import urllib.parse
|
||||
import validators
|
||||
from typing import Union, Sequence, Iterator
|
||||
|
||||
from langchain_community.document_loaders import (
|
||||
WebBaseLoader,
|
||||
)
|
||||
from langchain_core.documents import Document
|
||||
|
||||
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
from open_webui.config import ENABLE_RAG_LOCAL_WEB_FETCH
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||
|
||||
|
||||
def validate_url(url: Union[str, Sequence[str]]):
|
||||
if isinstance(url, str):
|
||||
if isinstance(validators.url(url), validators.ValidationError):
|
||||
raise ValueError(ERROR_MESSAGES.INVALID_URL)
|
||||
if not ENABLE_RAG_LOCAL_WEB_FETCH:
|
||||
# Local web fetch is disabled, filter out any URLs that resolve to private IP addresses
|
||||
parsed_url = urllib.parse.urlparse(url)
|
||||
# Get IPv4 and IPv6 addresses
|
||||
ipv4_addresses, ipv6_addresses = resolve_hostname(parsed_url.hostname)
|
||||
# Check if any of the resolved addresses are private
|
||||
# This is technically still vulnerable to DNS rebinding attacks, as we don't control WebBaseLoader
|
||||
for ip in ipv4_addresses:
|
||||
if validators.ipv4(ip, private=True):
|
||||
raise ValueError(ERROR_MESSAGES.INVALID_URL)
|
||||
for ip in ipv6_addresses:
|
||||
if validators.ipv6(ip, private=True):
|
||||
raise ValueError(ERROR_MESSAGES.INVALID_URL)
|
||||
return True
|
||||
elif isinstance(url, Sequence):
|
||||
return all(validate_url(u) for u in url)
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def resolve_hostname(hostname):
|
||||
# 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
|
||||
|
||||
|
||||
class SafeWebBaseLoader(WebBaseLoader):
|
||||
"""WebBaseLoader with enhanced error handling for URLs."""
|
||||
|
||||
def lazy_load(self) -> Iterator[Document]:
|
||||
"""Lazy load text from the url(s) in web_path with error handling."""
|
||||
for path in self.web_paths:
|
||||
try:
|
||||
soup = self._scrape(path, bs_kwargs=self.bs_kwargs)
|
||||
text = soup.get_text(**self.bs_get_text_kwargs)
|
||||
|
||||
# Build metadata
|
||||
metadata = {"source": path}
|
||||
if title := soup.find("title"):
|
||||
metadata["title"] = title.get_text()
|
||||
if description := soup.find("meta", attrs={"name": "description"}):
|
||||
metadata["description"] = description.get(
|
||||
"content", "No description found."
|
||||
)
|
||||
if html := soup.find("html"):
|
||||
metadata["language"] = html.get("lang", "No language found.")
|
||||
|
||||
yield Document(page_content=text, metadata=metadata)
|
||||
except Exception as e:
|
||||
# Log the error and continue with the next URL
|
||||
log.error(f"Error loading {path}: {e}")
|
||||
|
||||
|
||||
def get_web_loader(
|
||||
url: Union[str, Sequence[str]],
|
||||
verify_ssl: bool = True,
|
||||
requests_per_second: int = 2,
|
||||
):
|
||||
# Check if the URL is valid
|
||||
if not validate_url(url):
|
||||
raise ValueError(ERROR_MESSAGES.INVALID_URL)
|
||||
return SafeWebBaseLoader(
|
||||
url,
|
||||
verify_ssl=verify_ssl,
|
||||
requests_per_second=requests_per_second,
|
||||
continue_on_failure=True,
|
||||
)
|
||||
@@ -1,21 +1,123 @@
|
||||
import asyncio
|
||||
|
||||
import socketio
|
||||
from open_webui.apps.webui.models.users import Users
|
||||
from open_webui.utils.utils import decode_token
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
|
||||
from open_webui.apps.webui.models.users import Users
|
||||
from open_webui.env import (
|
||||
ENABLE_WEBSOCKET_SUPPORT,
|
||||
WEBSOCKET_MANAGER,
|
||||
WEBSOCKET_REDIS_URL,
|
||||
)
|
||||
from open_webui.utils.utils import decode_token
|
||||
from open_webui.apps.socket.utils import RedisDict
|
||||
|
||||
from open_webui.env import (
|
||||
GLOBAL_LOG_LEVEL,
|
||||
SRC_LOG_LEVELS,
|
||||
)
|
||||
|
||||
|
||||
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["SOCKET"])
|
||||
|
||||
|
||||
if WEBSOCKET_MANAGER == "redis":
|
||||
mgr = socketio.AsyncRedisManager(WEBSOCKET_REDIS_URL)
|
||||
sio = socketio.AsyncServer(
|
||||
cors_allowed_origins=[],
|
||||
async_mode="asgi",
|
||||
transports=(
|
||||
["polling", "websocket"] if ENABLE_WEBSOCKET_SUPPORT else ["polling"]
|
||||
),
|
||||
allow_upgrades=ENABLE_WEBSOCKET_SUPPORT,
|
||||
always_connect=True,
|
||||
client_manager=mgr,
|
||||
)
|
||||
else:
|
||||
sio = socketio.AsyncServer(
|
||||
cors_allowed_origins=[],
|
||||
async_mode="asgi",
|
||||
transports=(
|
||||
["polling", "websocket"] if ENABLE_WEBSOCKET_SUPPORT else ["polling"]
|
||||
),
|
||||
allow_upgrades=ENABLE_WEBSOCKET_SUPPORT,
|
||||
always_connect=True,
|
||||
)
|
||||
|
||||
sio = socketio.AsyncServer(cors_allowed_origins=[], async_mode="asgi")
|
||||
app = socketio.ASGIApp(sio, socketio_path="/ws/socket.io")
|
||||
|
||||
# Dictionary to maintain the user pool
|
||||
|
||||
SESSION_POOL = {}
|
||||
USER_POOL = {}
|
||||
USAGE_POOL = {}
|
||||
if WEBSOCKET_MANAGER == "redis":
|
||||
SESSION_POOL = RedisDict("open-webui:session_pool", redis_url=WEBSOCKET_REDIS_URL)
|
||||
USER_POOL = RedisDict("open-webui:user_pool", redis_url=WEBSOCKET_REDIS_URL)
|
||||
USAGE_POOL = RedisDict("open-webui:usage_pool", redis_url=WEBSOCKET_REDIS_URL)
|
||||
else:
|
||||
SESSION_POOL = {}
|
||||
USER_POOL = {}
|
||||
USAGE_POOL = {}
|
||||
|
||||
|
||||
# Timeout duration in seconds
|
||||
TIMEOUT_DURATION = 3
|
||||
|
||||
|
||||
async def periodic_usage_pool_cleanup():
|
||||
while True:
|
||||
now = int(time.time())
|
||||
for model_id, connections in list(USAGE_POOL.items()):
|
||||
# Creating a list of sids to remove if they have timed out
|
||||
expired_sids = [
|
||||
sid
|
||||
for sid, details in connections.items()
|
||||
if now - details["updated_at"] > TIMEOUT_DURATION
|
||||
]
|
||||
|
||||
for sid in expired_sids:
|
||||
del connections[sid]
|
||||
|
||||
if not connections:
|
||||
log.debug(f"Cleaning up model {model_id} from usage pool")
|
||||
del USAGE_POOL[model_id]
|
||||
else:
|
||||
USAGE_POOL[model_id] = connections
|
||||
|
||||
# Emit updated usage information after cleaning
|
||||
await sio.emit("usage", {"models": get_models_in_use()})
|
||||
|
||||
await asyncio.sleep(TIMEOUT_DURATION)
|
||||
|
||||
|
||||
app = socketio.ASGIApp(
|
||||
sio,
|
||||
socketio_path="/ws/socket.io",
|
||||
)
|
||||
|
||||
|
||||
def get_models_in_use():
|
||||
# List models that are currently in use
|
||||
models_in_use = list(USAGE_POOL.keys())
|
||||
return models_in_use
|
||||
|
||||
|
||||
@sio.on("usage")
|
||||
async def usage(sid, data):
|
||||
model_id = data["model"]
|
||||
# Record the timestamp for the last update
|
||||
current_time = int(time.time())
|
||||
|
||||
# Store the new usage data and task
|
||||
USAGE_POOL[model_id] = {
|
||||
**(USAGE_POOL[model_id] if model_id in USAGE_POOL else {}),
|
||||
sid: {"updated_at": current_time},
|
||||
}
|
||||
|
||||
# Broadcast the usage data to all clients
|
||||
await sio.emit("usage", {"models": get_models_in_use()})
|
||||
|
||||
|
||||
@sio.event
|
||||
async def connect(sid, environ, auth):
|
||||
user = None
|
||||
@@ -32,15 +134,14 @@ async def connect(sid, environ, auth):
|
||||
else:
|
||||
USER_POOL[user.id] = [sid]
|
||||
|
||||
print(f"user {user.name}({user.id}) connected with session ID {sid}")
|
||||
|
||||
await sio.emit("user-count", {"count": len(set(USER_POOL))})
|
||||
# print(f"user {user.name}({user.id}) connected with session ID {sid}")
|
||||
await sio.emit("user-count", {"count": len(USER_POOL.items())})
|
||||
await sio.emit("usage", {"models": get_models_in_use()})
|
||||
|
||||
|
||||
@sio.on("user-join")
|
||||
async def user_join(sid, data):
|
||||
print("user-join", sid, data)
|
||||
# print("user-join", sid, data)
|
||||
|
||||
auth = data["auth"] if "auth" in data else None
|
||||
if not auth or "token" not in auth:
|
||||
@@ -60,67 +161,14 @@ async def user_join(sid, data):
|
||||
else:
|
||||
USER_POOL[user.id] = [sid]
|
||||
|
||||
print(f"user {user.name}({user.id}) connected with session ID {sid}")
|
||||
# print(f"user {user.name}({user.id}) connected with session ID {sid}")
|
||||
|
||||
await sio.emit("user-count", {"count": len(set(USER_POOL))})
|
||||
await sio.emit("user-count", {"count": len(USER_POOL.items())})
|
||||
|
||||
|
||||
@sio.on("user-count")
|
||||
async def user_count(sid):
|
||||
await sio.emit("user-count", {"count": len(set(USER_POOL))})
|
||||
|
||||
|
||||
def get_models_in_use():
|
||||
# Aggregate all models in use
|
||||
models_in_use = []
|
||||
for model_id, data in USAGE_POOL.items():
|
||||
models_in_use.append(model_id)
|
||||
|
||||
return models_in_use
|
||||
|
||||
|
||||
@sio.on("usage")
|
||||
async def usage(sid, data):
|
||||
model_id = data["model"]
|
||||
|
||||
# Cancel previous callback if there is one
|
||||
if model_id in USAGE_POOL:
|
||||
USAGE_POOL[model_id]["callback"].cancel()
|
||||
|
||||
# Store the new usage data and task
|
||||
|
||||
if model_id in USAGE_POOL:
|
||||
USAGE_POOL[model_id]["sids"].append(sid)
|
||||
USAGE_POOL[model_id]["sids"] = list(set(USAGE_POOL[model_id]["sids"]))
|
||||
|
||||
else:
|
||||
USAGE_POOL[model_id] = {"sids": [sid]}
|
||||
|
||||
# Schedule a task to remove the usage data after TIMEOUT_DURATION
|
||||
USAGE_POOL[model_id]["callback"] = asyncio.create_task(
|
||||
remove_after_timeout(sid, model_id)
|
||||
)
|
||||
|
||||
# Broadcast the usage data to all clients
|
||||
await sio.emit("usage", {"models": get_models_in_use()})
|
||||
|
||||
|
||||
async def remove_after_timeout(sid, model_id):
|
||||
try:
|
||||
await asyncio.sleep(TIMEOUT_DURATION)
|
||||
if model_id in USAGE_POOL:
|
||||
print(USAGE_POOL[model_id]["sids"])
|
||||
USAGE_POOL[model_id]["sids"].remove(sid)
|
||||
USAGE_POOL[model_id]["sids"] = list(set(USAGE_POOL[model_id]["sids"]))
|
||||
|
||||
if len(USAGE_POOL[model_id]["sids"]) == 0:
|
||||
del USAGE_POOL[model_id]
|
||||
|
||||
# Broadcast the usage data to all clients
|
||||
await sio.emit("usage", {"models": get_models_in_use()})
|
||||
except asyncio.CancelledError:
|
||||
# Task was cancelled due to new 'usage' event
|
||||
pass
|
||||
await sio.emit("user-count", {"count": len(USER_POOL.items())})
|
||||
|
||||
|
||||
@sio.event
|
||||
@@ -129,14 +177,15 @@ async def disconnect(sid):
|
||||
user_id = SESSION_POOL[sid]
|
||||
del SESSION_POOL[sid]
|
||||
|
||||
USER_POOL[user_id].remove(sid)
|
||||
USER_POOL[user_id] = [_sid for _sid in USER_POOL[user_id] if _sid != sid]
|
||||
|
||||
if len(USER_POOL[user_id]) == 0:
|
||||
del USER_POOL[user_id]
|
||||
|
||||
await sio.emit("user-count", {"count": len(USER_POOL)})
|
||||
else:
|
||||
print(f"Unknown session ID {sid} disconnected")
|
||||
pass
|
||||
# print(f"Unknown session ID {sid} disconnected")
|
||||
|
||||
|
||||
def get_event_emitter(request_info):
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import json
|
||||
import redis
|
||||
|
||||
|
||||
class RedisDict:
|
||||
def __init__(self, name, redis_url):
|
||||
self.name = name
|
||||
self.redis = redis.Redis.from_url(redis_url, decode_responses=True)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
serialized_value = json.dumps(value)
|
||||
self.redis.hset(self.name, key, serialized_value)
|
||||
|
||||
def __getitem__(self, key):
|
||||
value = self.redis.hget(self.name, key)
|
||||
if value is None:
|
||||
raise KeyError(key)
|
||||
return json.loads(value)
|
||||
|
||||
def __delitem__(self, key):
|
||||
result = self.redis.hdel(self.name, key)
|
||||
if result == 0:
|
||||
raise KeyError(key)
|
||||
|
||||
def __contains__(self, key):
|
||||
return self.redis.hexists(self.name, key)
|
||||
|
||||
def __len__(self):
|
||||
return self.redis.hlen(self.name)
|
||||
|
||||
def keys(self):
|
||||
return self.redis.hkeys(self.name)
|
||||
|
||||
def values(self):
|
||||
return [json.loads(v) for v in self.redis.hvals(self.name)]
|
||||
|
||||
def items(self):
|
||||
return [(k, json.loads(v)) for k, v in self.redis.hgetall(self.name).items()]
|
||||
|
||||
def get(self, key, default=None):
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def clear(self):
|
||||
self.redis.delete(self.name)
|
||||
|
||||
def update(self, other=None, **kwargs):
|
||||
if other is not None:
|
||||
for k, v in other.items() if hasattr(other, "items") else other:
|
||||
self[k] = v
|
||||
for k, v in kwargs.items():
|
||||
self[k] = v
|
||||
|
||||
def setdefault(self, key, default=None):
|
||||
if key not in self:
|
||||
self[key] = default
|
||||
return self[key]
|
||||
@@ -4,11 +4,20 @@ from contextlib import contextmanager
|
||||
from typing import Any, Optional
|
||||
|
||||
from open_webui.apps.webui.internal.wrappers import register_connection
|
||||
from open_webui.env import OPEN_WEBUI_DIR, DATABASE_URL, SRC_LOG_LEVELS
|
||||
from open_webui.env import (
|
||||
OPEN_WEBUI_DIR,
|
||||
DATABASE_URL,
|
||||
SRC_LOG_LEVELS,
|
||||
DATABASE_POOL_MAX_OVERFLOW,
|
||||
DATABASE_POOL_RECYCLE,
|
||||
DATABASE_POOL_SIZE,
|
||||
DATABASE_POOL_TIMEOUT,
|
||||
)
|
||||
from peewee_migrate import Router
|
||||
from sqlalchemy import Dialect, create_engine, types
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||
from sqlalchemy.pool import QueuePool, NullPool
|
||||
from sqlalchemy.sql.type_api import _T
|
||||
from typing_extensions import Self
|
||||
|
||||
@@ -71,7 +80,20 @@ if "sqlite" in SQLALCHEMY_DATABASE_URL:
|
||||
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
|
||||
)
|
||||
else:
|
||||
engine = create_engine(SQLALCHEMY_DATABASE_URL, pool_pre_ping=True)
|
||||
if DATABASE_POOL_SIZE > 0:
|
||||
engine = create_engine(
|
||||
SQLALCHEMY_DATABASE_URL,
|
||||
pool_size=DATABASE_POOL_SIZE,
|
||||
max_overflow=DATABASE_POOL_MAX_OVERFLOW,
|
||||
pool_timeout=DATABASE_POOL_TIMEOUT,
|
||||
pool_recycle=DATABASE_POOL_RECYCLE,
|
||||
pool_pre_ping=True,
|
||||
poolclass=QueuePool,
|
||||
)
|
||||
else:
|
||||
engine = create_engine(
|
||||
SQLALCHEMY_DATABASE_URL, pool_pre_ping=True, poolclass=NullPool
|
||||
)
|
||||
|
||||
|
||||
SessionLocal = sessionmaker(
|
||||
|
||||
@@ -10,11 +10,11 @@ from open_webui.apps.webui.routers import (
|
||||
auths,
|
||||
chats,
|
||||
configs,
|
||||
documents,
|
||||
files,
|
||||
functions,
|
||||
memories,
|
||||
models,
|
||||
knowledge,
|
||||
prompts,
|
||||
tools,
|
||||
users,
|
||||
@@ -51,11 +51,15 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
from open_webui.utils.misc import (
|
||||
apply_model_params_to_body_openai,
|
||||
apply_model_system_prompt_to_body,
|
||||
openai_chat_chunk_message_template,
|
||||
openai_chat_completion_message_template,
|
||||
)
|
||||
from open_webui.utils.payload import (
|
||||
apply_model_params_to_body_openai,
|
||||
apply_model_system_prompt_to_body,
|
||||
)
|
||||
|
||||
|
||||
from open_webui.utils.tools import get_tools
|
||||
|
||||
app = FastAPI()
|
||||
@@ -107,15 +111,15 @@ app.include_router(auths.router, prefix="/auths", tags=["auths"])
|
||||
app.include_router(users.router, prefix="/users", tags=["users"])
|
||||
app.include_router(chats.router, prefix="/chats", tags=["chats"])
|
||||
|
||||
app.include_router(documents.router, prefix="/documents", tags=["documents"])
|
||||
app.include_router(models.router, prefix="/models", tags=["models"])
|
||||
app.include_router(knowledge.router, prefix="/knowledge", tags=["knowledge"])
|
||||
app.include_router(prompts.router, prefix="/prompts", tags=["prompts"])
|
||||
|
||||
app.include_router(memories.router, prefix="/memories", tags=["memories"])
|
||||
app.include_router(files.router, prefix="/files", tags=["files"])
|
||||
app.include_router(tools.router, prefix="/tools", tags=["tools"])
|
||||
app.include_router(functions.router, prefix="/functions", tags=["functions"])
|
||||
|
||||
app.include_router(memories.router, prefix="/memories", tags=["memories"])
|
||||
app.include_router(utils.router, prefix="/utils", tags=["utils"])
|
||||
|
||||
|
||||
@@ -152,29 +156,33 @@ async def get_pipe_models():
|
||||
|
||||
# Check if function is a manifold
|
||||
if hasattr(function_module, "pipes"):
|
||||
manifold_pipes = []
|
||||
sub_pipes = []
|
||||
|
||||
# Check if pipes is a function or a list
|
||||
if callable(function_module.pipes):
|
||||
manifold_pipes = function_module.pipes()
|
||||
else:
|
||||
manifold_pipes = function_module.pipes
|
||||
|
||||
for p in manifold_pipes:
|
||||
manifold_pipe_id = f'{pipe.id}.{p["id"]}'
|
||||
manifold_pipe_name = p["name"]
|
||||
try:
|
||||
if callable(function_module.pipes):
|
||||
sub_pipes = function_module.pipes()
|
||||
else:
|
||||
sub_pipes = function_module.pipes
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
sub_pipes = []
|
||||
|
||||
print(sub_pipes)
|
||||
|
||||
for p in sub_pipes:
|
||||
sub_pipe_id = f'{pipe.id}.{p["id"]}'
|
||||
sub_pipe_name = p["name"]
|
||||
|
||||
if hasattr(function_module, "name"):
|
||||
manifold_pipe_name = f"{function_module.name}{manifold_pipe_name}"
|
||||
sub_pipe_name = f"{function_module.name}{sub_pipe_name}"
|
||||
|
||||
pipe_flag = {"type": pipe.type}
|
||||
if hasattr(function_module, "ChatValves"):
|
||||
pipe_flag["valves_spec"] = function_module.ChatValves.schema()
|
||||
|
||||
pipe_models.append(
|
||||
{
|
||||
"id": manifold_pipe_id,
|
||||
"name": manifold_pipe_name,
|
||||
"id": sub_pipe_id,
|
||||
"name": sub_pipe_name,
|
||||
"object": "model",
|
||||
"created": pipe.created_at,
|
||||
"owned_by": "openai",
|
||||
@@ -183,8 +191,6 @@ async def get_pipe_models():
|
||||
)
|
||||
else:
|
||||
pipe_flag = {"type": "pipe"}
|
||||
if hasattr(function_module, "ChatValves"):
|
||||
pipe_flag["valves_spec"] = function_module.ChatValves.schema()
|
||||
|
||||
pipe_models.append(
|
||||
{
|
||||
@@ -281,17 +287,20 @@ async def generate_function_chat_completion(form_data, user):
|
||||
__event_emitter__ = None
|
||||
__event_call__ = None
|
||||
__task__ = None
|
||||
__task_body__ = None
|
||||
|
||||
if metadata:
|
||||
if all(k in metadata for k in ("session_id", "chat_id", "message_id")):
|
||||
__event_emitter__ = get_event_emitter(metadata)
|
||||
__event_call__ = get_event_call(metadata)
|
||||
__task__ = metadata.get("task", None)
|
||||
__task_body__ = metadata.get("task_body", None)
|
||||
|
||||
extra_params = {
|
||||
"__event_emitter__": __event_emitter__,
|
||||
"__event_call__": __event_call__,
|
||||
"__task__": __task__,
|
||||
"__task_body__": __task_body__,
|
||||
"__files__": files,
|
||||
"__user__": {
|
||||
"id": user.id,
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Optional
|
||||
from open_webui.apps.webui.internal.db import Base, JSONField, get_db
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from sqlalchemy import BigInteger, Column, String, Text
|
||||
from sqlalchemy import BigInteger, Column, String, Text, JSON
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||
@@ -20,19 +20,29 @@ class File(Base):
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
user_id = Column(String)
|
||||
hash = Column(Text, nullable=True)
|
||||
|
||||
filename = Column(Text)
|
||||
data = Column(JSON, nullable=True)
|
||||
meta = Column(JSONField)
|
||||
|
||||
created_at = Column(BigInteger)
|
||||
updated_at = Column(BigInteger)
|
||||
|
||||
|
||||
class FileModel(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: str
|
||||
user_id: str
|
||||
filename: str
|
||||
meta: dict
|
||||
created_at: int # timestamp in epoch
|
||||
hash: Optional[str] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
filename: str
|
||||
data: Optional[dict] = None
|
||||
meta: dict
|
||||
|
||||
created_at: int # timestamp in epoch
|
||||
updated_at: int # timestamp in epoch
|
||||
|
||||
|
||||
####################
|
||||
@@ -43,14 +53,21 @@ class FileModel(BaseModel):
|
||||
class FileModelResponse(BaseModel):
|
||||
id: str
|
||||
user_id: str
|
||||
hash: Optional[str] = None
|
||||
|
||||
filename: str
|
||||
data: Optional[dict] = None
|
||||
meta: dict
|
||||
|
||||
created_at: int # timestamp in epoch
|
||||
updated_at: int # timestamp in epoch
|
||||
|
||||
|
||||
class FileForm(BaseModel):
|
||||
id: str
|
||||
hash: Optional[str] = None
|
||||
filename: str
|
||||
data: dict = {}
|
||||
meta: dict = {}
|
||||
|
||||
|
||||
@@ -62,6 +79,7 @@ class FilesTable:
|
||||
**form_data.model_dump(),
|
||||
"user_id": user_id,
|
||||
"created_at": int(time.time()),
|
||||
"updated_at": int(time.time()),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -90,6 +108,16 @@ class FilesTable:
|
||||
with get_db() as db:
|
||||
return [FileModel.model_validate(file) for file in db.query(File).all()]
|
||||
|
||||
def get_files_by_ids(self, ids: list[str]) -> list[FileModel]:
|
||||
with get_db() as db:
|
||||
return [
|
||||
FileModel.model_validate(file)
|
||||
for file in db.query(File)
|
||||
.filter(File.id.in_(ids))
|
||||
.order_by(File.updated_at.desc())
|
||||
.all()
|
||||
]
|
||||
|
||||
def get_files_by_user_id(self, user_id: str) -> list[FileModel]:
|
||||
with get_db() as db:
|
||||
return [
|
||||
@@ -97,6 +125,38 @@ class FilesTable:
|
||||
for file in db.query(File).filter_by(user_id=user_id).all()
|
||||
]
|
||||
|
||||
def update_file_hash_by_id(self, id: str, hash: str) -> Optional[FileModel]:
|
||||
with get_db() as db:
|
||||
try:
|
||||
file = db.query(File).filter_by(id=id).first()
|
||||
file.hash = hash
|
||||
db.commit()
|
||||
|
||||
return FileModel.model_validate(file)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def update_file_data_by_id(self, id: str, data: dict) -> Optional[FileModel]:
|
||||
with get_db() as db:
|
||||
try:
|
||||
file = db.query(File).filter_by(id=id).first()
|
||||
file.data = {**(file.data if file.data else {}), **data}
|
||||
db.commit()
|
||||
return FileModel.model_validate(file)
|
||||
except Exception as e:
|
||||
|
||||
return None
|
||||
|
||||
def update_file_metadata_by_id(self, id: str, meta: dict) -> Optional[FileModel]:
|
||||
with get_db() as db:
|
||||
try:
|
||||
file = db.query(File).filter_by(id=id).first()
|
||||
file.meta = {**(file.meta if file.meta else {}), **meta}
|
||||
db.commit()
|
||||
return FileModel.model_validate(file)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def delete_file_by_id(self, id: str) -> bool:
|
||||
with get_db() as db:
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Optional
|
||||
import uuid
|
||||
|
||||
from open_webui.apps.webui.internal.db import Base, get_db
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from sqlalchemy import BigInteger, Column, String, Text, JSON
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||
|
||||
####################
|
||||
# Knowledge DB Schema
|
||||
####################
|
||||
|
||||
|
||||
class Knowledge(Base):
|
||||
__tablename__ = "knowledge"
|
||||
|
||||
id = Column(Text, unique=True, primary_key=True)
|
||||
user_id = Column(Text)
|
||||
|
||||
name = Column(Text)
|
||||
description = Column(Text)
|
||||
|
||||
data = Column(JSON, nullable=True)
|
||||
meta = Column(JSON, nullable=True)
|
||||
|
||||
created_at = Column(BigInteger)
|
||||
updated_at = Column(BigInteger)
|
||||
|
||||
|
||||
class KnowledgeModel(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: str
|
||||
user_id: str
|
||||
|
||||
name: str
|
||||
description: str
|
||||
|
||||
data: Optional[dict] = None
|
||||
meta: Optional[dict] = None
|
||||
|
||||
created_at: int # timestamp in epoch
|
||||
updated_at: int # timestamp in epoch
|
||||
|
||||
|
||||
####################
|
||||
# Forms
|
||||
####################
|
||||
|
||||
|
||||
class KnowledgeResponse(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
data: Optional[dict] = None
|
||||
meta: Optional[dict] = None
|
||||
created_at: int # timestamp in epoch
|
||||
updated_at: int # timestamp in epoch
|
||||
|
||||
|
||||
class KnowledgeForm(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
data: Optional[dict] = None
|
||||
|
||||
|
||||
class KnowledgeUpdateForm(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
data: Optional[dict] = None
|
||||
|
||||
|
||||
class KnowledgeTable:
|
||||
def insert_new_knowledge(
|
||||
self, user_id: str, form_data: KnowledgeForm
|
||||
) -> Optional[KnowledgeModel]:
|
||||
with get_db() as db:
|
||||
knowledge = KnowledgeModel(
|
||||
**{
|
||||
**form_data.model_dump(),
|
||||
"id": str(uuid.uuid4()),
|
||||
"user_id": user_id,
|
||||
"created_at": int(time.time()),
|
||||
"updated_at": int(time.time()),
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
result = Knowledge(**knowledge.model_dump())
|
||||
db.add(result)
|
||||
db.commit()
|
||||
db.refresh(result)
|
||||
if result:
|
||||
return KnowledgeModel.model_validate(result)
|
||||
else:
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_knowledge_items(self) -> list[KnowledgeModel]:
|
||||
with get_db() as db:
|
||||
return [
|
||||
KnowledgeModel.model_validate(knowledge)
|
||||
for knowledge in db.query(Knowledge)
|
||||
.order_by(Knowledge.updated_at.desc())
|
||||
.all()
|
||||
]
|
||||
|
||||
def get_knowledge_by_id(self, id: str) -> Optional[KnowledgeModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
knowledge = db.query(Knowledge).filter_by(id=id).first()
|
||||
return KnowledgeModel.model_validate(knowledge) if knowledge else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def update_knowledge_by_id(
|
||||
self, id: str, form_data: KnowledgeUpdateForm, overwrite: bool = False
|
||||
) -> Optional[KnowledgeModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
knowledge = self.get_knowledge_by_id(id=id)
|
||||
db.query(Knowledge).filter_by(id=id).update(
|
||||
{
|
||||
**form_data.model_dump(exclude_none=True),
|
||||
"updated_at": int(time.time()),
|
||||
}
|
||||
)
|
||||
db.commit()
|
||||
return self.get_knowledge_by_id(id=id)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
return None
|
||||
|
||||
def delete_knowledge_by_id(self, id: str) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
db.query(Knowledge).filter_by(id=id).delete()
|
||||
db.commit()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
Knowledges = KnowledgeTable()
|
||||
@@ -188,14 +188,19 @@ async def signin(request: Request, response: Response, form_data: SigninForm):
|
||||
|
||||
@router.post("/signup", response_model=SigninResponse)
|
||||
async def signup(request: Request, response: Response, form_data: SignupForm):
|
||||
if (
|
||||
not request.app.state.config.ENABLE_SIGNUP
|
||||
and request.app.state.config.ENABLE_LOGIN_FORM
|
||||
and WEBUI_AUTH
|
||||
):
|
||||
raise HTTPException(
|
||||
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
|
||||
)
|
||||
if WEBUI_AUTH:
|
||||
if (
|
||||
not request.app.state.config.ENABLE_SIGNUP
|
||||
or not request.app.state.config.ENABLE_LOGIN_FORM
|
||||
):
|
||||
raise HTTPException(
|
||||
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
|
||||
)
|
||||
else:
|
||||
if Users.get_num_users() != 0:
|
||||
raise HTTPException(
|
||||
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
|
||||
)
|
||||
|
||||
if not validate_email_format(form_data.email.lower()):
|
||||
raise HTTPException(
|
||||
|
||||
@@ -52,10 +52,9 @@ async def get_session_user_chat_list(
|
||||
|
||||
@router.delete("/", response_model=bool)
|
||||
async def delete_all_user_chats(request: Request, user=Depends(get_verified_user)):
|
||||
if (
|
||||
user.role == "user"
|
||||
and not request.app.state.config.USER_PERMISSIONS["chat"]["deletion"]
|
||||
):
|
||||
if user.role == "user" and not request.app.state.config.USER_PERMISSIONS.get(
|
||||
"chat", {}
|
||||
).get("deletion", {}):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
@@ -292,7 +291,9 @@ async def delete_chat_by_id(request: Request, id: str, user=Depends(get_verified
|
||||
result = Chats.delete_chat_by_id(id)
|
||||
return result
|
||||
else:
|
||||
if not request.app.state.config.USER_PERMISSIONS["chat"]["deletion"]:
|
||||
if not request.app.state.config.USER_PERMISSIONS.get("chat", {}).get(
|
||||
"deletion", {}
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
|
||||
@@ -4,13 +4,22 @@ import shutil
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
import mimetypes
|
||||
|
||||
|
||||
from open_webui.apps.webui.models.files import FileForm, FileModel, Files
|
||||
from open_webui.apps.retrieval.main import process_file, ProcessFileForm
|
||||
|
||||
from open_webui.config import UPLOAD_DIR
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
|
||||
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.responses import FileResponse, StreamingResponse
|
||||
|
||||
|
||||
from open_webui.utils.utils import get_admin_user, get_verified_user
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -58,6 +67,13 @@ def upload_file(file: UploadFile = File(...), user=Depends(get_verified_user)):
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
process_file(ProcessFileForm(file_id=id))
|
||||
file = Files.get_file_by_id(id=id)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
log.error(f"Error processing file: {file.id}")
|
||||
|
||||
if file:
|
||||
return file
|
||||
else:
|
||||
@@ -143,6 +159,55 @@ async def get_file_by_id(id: str, user=Depends(get_verified_user)):
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# Get File Data Content By Id
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/{id}/data/content")
|
||||
async def get_file_data_content_by_id(id: str, user=Depends(get_verified_user)):
|
||||
file = Files.get_file_by_id(id)
|
||||
|
||||
if file and (file.user_id == user.id or user.role == "admin"):
|
||||
return {"content": file.data.get("content", "")}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# Update File Data Content By Id
|
||||
############################
|
||||
|
||||
|
||||
class ContentForm(BaseModel):
|
||||
content: str
|
||||
|
||||
|
||||
@router.post("/{id}/data/content/update")
|
||||
async def update_file_data_content_by_id(
|
||||
id: str, form_data: ContentForm, user=Depends(get_verified_user)
|
||||
):
|
||||
file = Files.get_file_by_id(id)
|
||||
|
||||
if file and (file.user_id == user.id or user.role == "admin"):
|
||||
try:
|
||||
process_file(ProcessFileForm(file_id=id, content=form_data.content))
|
||||
file = Files.get_file_by_id(id=id)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
log.error(f"Error processing file: {file.id}")
|
||||
|
||||
return {"content": file.data.get("content", "")}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# Get File Content By Id
|
||||
############################
|
||||
@@ -176,16 +241,32 @@ async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
|
||||
file = Files.get_file_by_id(id)
|
||||
|
||||
if file and (file.user_id == user.id or user.role == "admin"):
|
||||
file_path = Path(file.meta["path"])
|
||||
file_path = file.meta.get("path")
|
||||
if file_path:
|
||||
file_path = Path(file_path)
|
||||
|
||||
# Check if the file already exists in the cache
|
||||
if file_path.is_file():
|
||||
print(f"file_path: {file_path}")
|
||||
return FileResponse(file_path)
|
||||
# Check if the file already exists in the cache
|
||||
if file_path.is_file():
|
||||
print(f"file_path: {file_path}")
|
||||
return FileResponse(file_path)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
# File path doesn’t exist, return the content as .txt if possible
|
||||
file_content = file.content.get("content", "")
|
||||
file_name = file.filename
|
||||
|
||||
# Create a generator that encodes the file content
|
||||
def generator():
|
||||
yield file_content.encode("utf-8")
|
||||
|
||||
return StreamingResponse(
|
||||
generator(),
|
||||
media_type="text/plain",
|
||||
headers={"Content-Disposition": f"attachment; filename={file_name}"},
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
|
||||
@@ -8,7 +8,7 @@ from open_webui.apps.webui.models.functions import (
|
||||
FunctionResponse,
|
||||
Functions,
|
||||
)
|
||||
from open_webui.apps.webui.utils import load_function_module_by_id
|
||||
from open_webui.apps.webui.utils import load_function_module_by_id, replace_imports
|
||||
from open_webui.config import CACHE_DIR, FUNCTIONS_DIR
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
@@ -55,13 +55,11 @@ async def create_new_function(
|
||||
|
||||
function = Functions.get_function_by_id(form_data.id)
|
||||
if function is None:
|
||||
function_path = os.path.join(FUNCTIONS_DIR, f"{form_data.id}.py")
|
||||
try:
|
||||
with open(function_path, "w") as function_file:
|
||||
function_file.write(form_data.content)
|
||||
|
||||
form_data.content = replace_imports(form_data.content)
|
||||
function_module, function_type, frontmatter = load_function_module_by_id(
|
||||
form_data.id
|
||||
form_data.id,
|
||||
content=form_data.content,
|
||||
)
|
||||
form_data.meta.manifest = frontmatter
|
||||
|
||||
@@ -174,13 +172,11 @@ async def toggle_global_by_id(id: str, user=Depends(get_admin_user)):
|
||||
async def update_function_by_id(
|
||||
request: Request, id: str, form_data: FunctionForm, user=Depends(get_admin_user)
|
||||
):
|
||||
function_path = os.path.join(FUNCTIONS_DIR, f"{id}.py")
|
||||
|
||||
try:
|
||||
with open(function_path, "w") as function_file:
|
||||
function_file.write(form_data.content)
|
||||
|
||||
function_module, function_type, frontmatter = load_function_module_by_id(id)
|
||||
form_data.content = replace_imports(form_data.content)
|
||||
function_module, function_type, frontmatter = load_function_module_by_id(
|
||||
id, content=form_data.content
|
||||
)
|
||||
form_data.meta.manifest = frontmatter
|
||||
|
||||
FUNCTIONS = request.app.state.FUNCTIONS
|
||||
@@ -222,13 +218,6 @@ async def delete_function_by_id(
|
||||
if id in FUNCTIONS:
|
||||
del FUNCTIONS[id]
|
||||
|
||||
# delete the function file
|
||||
function_path = os.path.join(FUNCTIONS_DIR, f"{id}.py")
|
||||
try:
|
||||
os.remove(function_path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
import json
|
||||
from typing import Optional, Union
|
||||
from pydantic import BaseModel
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
import logging
|
||||
|
||||
from open_webui.apps.webui.models.knowledge import (
|
||||
Knowledges,
|
||||
KnowledgeUpdateForm,
|
||||
KnowledgeForm,
|
||||
KnowledgeResponse,
|
||||
)
|
||||
from open_webui.apps.webui.models.files import Files, FileModel
|
||||
from open_webui.apps.retrieval.vector.connector import VECTOR_DB_CLIENT
|
||||
from open_webui.apps.retrieval.main import process_file, ProcessFileForm
|
||||
|
||||
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
from open_webui.utils.utils import get_admin_user, get_verified_user
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
############################
|
||||
# GetKnowledgeItems
|
||||
############################
|
||||
|
||||
|
||||
@router.get(
|
||||
"/", response_model=Optional[Union[list[KnowledgeResponse], KnowledgeResponse]]
|
||||
)
|
||||
async def get_knowledge_items(
|
||||
id: Optional[str] = None, user=Depends(get_verified_user)
|
||||
):
|
||||
if id:
|
||||
knowledge = Knowledges.get_knowledge_by_id(id=id)
|
||||
|
||||
if knowledge:
|
||||
return knowledge
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
else:
|
||||
return [
|
||||
KnowledgeResponse(**knowledge.model_dump())
|
||||
for knowledge in Knowledges.get_knowledge_items()
|
||||
]
|
||||
|
||||
|
||||
############################
|
||||
# CreateNewKnowledge
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/create", response_model=Optional[KnowledgeResponse])
|
||||
async def create_new_knowledge(form_data: KnowledgeForm, user=Depends(get_admin_user)):
|
||||
knowledge = Knowledges.insert_new_knowledge(user.id, form_data)
|
||||
|
||||
if knowledge:
|
||||
return knowledge
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.FILE_EXISTS,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# GetKnowledgeById
|
||||
############################
|
||||
|
||||
|
||||
class KnowledgeFilesResponse(KnowledgeResponse):
|
||||
files: list[FileModel]
|
||||
|
||||
|
||||
@router.get("/{id}", response_model=Optional[KnowledgeFilesResponse])
|
||||
async def get_knowledge_by_id(id: str, user=Depends(get_verified_user)):
|
||||
knowledge = Knowledges.get_knowledge_by_id(id=id)
|
||||
|
||||
if knowledge:
|
||||
file_ids = knowledge.data.get("file_ids", []) if knowledge.data else []
|
||||
files = Files.get_files_by_ids(file_ids)
|
||||
|
||||
return KnowledgeFilesResponse(
|
||||
**knowledge.model_dump(),
|
||||
files=files,
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# UpdateKnowledgeById
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/{id}/update", response_model=Optional[KnowledgeFilesResponse])
|
||||
async def update_knowledge_by_id(
|
||||
id: str,
|
||||
form_data: KnowledgeUpdateForm,
|
||||
user=Depends(get_admin_user),
|
||||
):
|
||||
knowledge = Knowledges.update_knowledge_by_id(id=id, form_data=form_data)
|
||||
|
||||
if knowledge:
|
||||
file_ids = knowledge.data.get("file_ids", []) if knowledge.data else []
|
||||
files = Files.get_files_by_ids(file_ids)
|
||||
|
||||
return KnowledgeFilesResponse(
|
||||
**knowledge.model_dump(),
|
||||
files=files,
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.ID_TAKEN,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# AddFileToKnowledge
|
||||
############################
|
||||
|
||||
|
||||
class KnowledgeFileIdForm(BaseModel):
|
||||
file_id: str
|
||||
|
||||
|
||||
@router.post("/{id}/file/add", response_model=Optional[KnowledgeFilesResponse])
|
||||
def add_file_to_knowledge_by_id(
|
||||
id: str,
|
||||
form_data: KnowledgeFileIdForm,
|
||||
user=Depends(get_admin_user),
|
||||
):
|
||||
knowledge = Knowledges.get_knowledge_by_id(id=id)
|
||||
file = Files.get_file_by_id(form_data.file_id)
|
||||
if not file:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
if not file.data:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.FILE_NOT_PROCESSED,
|
||||
)
|
||||
|
||||
# Add content to the vector database
|
||||
try:
|
||||
process_file(ProcessFileForm(file_id=form_data.file_id, collection_name=id))
|
||||
except Exception as e:
|
||||
log.debug(e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e),
|
||||
)
|
||||
|
||||
if knowledge:
|
||||
data = knowledge.data or {}
|
||||
file_ids = data.get("file_ids", [])
|
||||
|
||||
if form_data.file_id not in file_ids:
|
||||
file_ids.append(form_data.file_id)
|
||||
data["file_ids"] = file_ids
|
||||
|
||||
knowledge = Knowledges.update_knowledge_by_id(
|
||||
id=id, form_data=KnowledgeUpdateForm(data=data)
|
||||
)
|
||||
|
||||
if knowledge:
|
||||
files = Files.get_files_by_ids(file_ids)
|
||||
|
||||
return KnowledgeFilesResponse(
|
||||
**knowledge.model_dump(),
|
||||
files=files,
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT("knowledge"),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT("file_id"),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{id}/file/update", response_model=Optional[KnowledgeFilesResponse])
|
||||
def update_file_from_knowledge_by_id(
|
||||
id: str,
|
||||
form_data: KnowledgeFileIdForm,
|
||||
user=Depends(get_admin_user),
|
||||
):
|
||||
knowledge = Knowledges.get_knowledge_by_id(id=id)
|
||||
file = Files.get_file_by_id(form_data.file_id)
|
||||
if not file:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
# Remove content from the vector database
|
||||
VECTOR_DB_CLIENT.delete(
|
||||
collection_name=knowledge.id, filter={"file_id": form_data.file_id}
|
||||
)
|
||||
|
||||
# Add content to the vector database
|
||||
try:
|
||||
process_file(ProcessFileForm(file_id=form_data.file_id, collection_name=id))
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e),
|
||||
)
|
||||
|
||||
if knowledge:
|
||||
data = knowledge.data or {}
|
||||
file_ids = data.get("file_ids", [])
|
||||
|
||||
files = Files.get_files_by_ids(file_ids)
|
||||
|
||||
return KnowledgeFilesResponse(
|
||||
**knowledge.model_dump(),
|
||||
files=files,
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# RemoveFileFromKnowledge
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/{id}/file/remove", response_model=Optional[KnowledgeFilesResponse])
|
||||
def remove_file_from_knowledge_by_id(
|
||||
id: str,
|
||||
form_data: KnowledgeFileIdForm,
|
||||
user=Depends(get_admin_user),
|
||||
):
|
||||
knowledge = Knowledges.get_knowledge_by_id(id=id)
|
||||
file = Files.get_file_by_id(form_data.file_id)
|
||||
if not file:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
# Remove content from the vector database
|
||||
VECTOR_DB_CLIENT.delete(
|
||||
collection_name=knowledge.id, filter={"file_id": form_data.file_id}
|
||||
)
|
||||
|
||||
result = VECTOR_DB_CLIENT.query(
|
||||
collection_name=knowledge.id,
|
||||
filter={"file_id": form_data.file_id},
|
||||
)
|
||||
|
||||
Files.delete_file_by_id(form_data.file_id)
|
||||
|
||||
if knowledge:
|
||||
data = knowledge.data or {}
|
||||
file_ids = data.get("file_ids", [])
|
||||
|
||||
if form_data.file_id in file_ids:
|
||||
file_ids.remove(form_data.file_id)
|
||||
data["file_ids"] = file_ids
|
||||
|
||||
knowledge = Knowledges.update_knowledge_by_id(
|
||||
id=id, form_data=KnowledgeUpdateForm(data=data)
|
||||
)
|
||||
|
||||
if knowledge:
|
||||
files = Files.get_files_by_ids(file_ids)
|
||||
|
||||
return KnowledgeFilesResponse(
|
||||
**knowledge.model_dump(),
|
||||
files=files,
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT("knowledge"),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT("file_id"),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# ResetKnowledgeById
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/{id}/reset", response_model=Optional[KnowledgeResponse])
|
||||
async def reset_knowledge_by_id(id: str, user=Depends(get_admin_user)):
|
||||
try:
|
||||
VECTOR_DB_CLIENT.delete_collection(collection_name=id)
|
||||
except Exception as e:
|
||||
log.debug(e)
|
||||
pass
|
||||
|
||||
knowledge = Knowledges.update_knowledge_by_id(
|
||||
id=id, form_data=KnowledgeUpdateForm(data={"file_ids": []})
|
||||
)
|
||||
return knowledge
|
||||
|
||||
|
||||
############################
|
||||
# DeleteKnowledgeById
|
||||
############################
|
||||
|
||||
|
||||
@router.delete("/{id}/delete", response_model=bool)
|
||||
async def delete_knowledge_by_id(id: str, user=Depends(get_admin_user)):
|
||||
try:
|
||||
VECTOR_DB_CLIENT.delete_collection(collection_name=id)
|
||||
except Exception as e:
|
||||
log.debug(e)
|
||||
pass
|
||||
result = Knowledges.delete_knowledge_by_id(id=id)
|
||||
return result
|
||||
@@ -1,12 +1,13 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from pydantic import BaseModel
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from open_webui.apps.webui.models.memories import Memories, MemoryModel
|
||||
from open_webui.config import CHROMA_CLIENT
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from pydantic import BaseModel
|
||||
from open_webui.apps.retrieval.vector.connector import VECTOR_DB_CLIENT
|
||||
from open_webui.utils.utils import get_verified_user
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||
@@ -49,14 +50,17 @@ async def add_memory(
|
||||
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}],
|
||||
VECTOR_DB_CLIENT.upsert(
|
||||
collection_name=f"user-memory-{user.id}",
|
||||
items=[
|
||||
{
|
||||
"id": memory.id,
|
||||
"text": memory.content,
|
||||
"vector": request.app.state.EMBEDDING_FUNCTION(memory.content),
|
||||
"metadata": {"created_at": memory.created_at},
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
return memory
|
||||
@@ -76,12 +80,10 @@ class QueryMemoryForm(BaseModel):
|
||||
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=form_data.k, # how many results to return
|
||||
results = VECTOR_DB_CLIENT.search(
|
||||
collection_name=f"user-memory-{user.id}",
|
||||
vectors=[request.app.state.EMBEDDING_FUNCTION(form_data.content)],
|
||||
limit=form_data.k,
|
||||
)
|
||||
|
||||
return results
|
||||
@@ -94,17 +96,25 @@ async def query_memory(
|
||||
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}")
|
||||
VECTOR_DB_CLIENT.delete_collection(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],
|
||||
)
|
||||
VECTOR_DB_CLIENT.upsert(
|
||||
collection_name=f"user-memory-{user.id}",
|
||||
items=[
|
||||
{
|
||||
"id": memory.id,
|
||||
"text": memory.content,
|
||||
"vector": request.app.state.EMBEDDING_FUNCTION(memory.content),
|
||||
"metadata": {
|
||||
"created_at": memory.created_at,
|
||||
"updated_at": memory.updated_at,
|
||||
},
|
||||
}
|
||||
for memory in memories
|
||||
],
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -119,7 +129,7 @@ async def delete_memory_by_user_id(user=Depends(get_verified_user)):
|
||||
|
||||
if result:
|
||||
try:
|
||||
CHROMA_CLIENT.delete_collection(f"user-memory-{user.id}")
|
||||
VECTOR_DB_CLIENT.delete_collection(f"user-memory-{user.id}")
|
||||
except Exception as e:
|
||||
log.error(e)
|
||||
return True
|
||||
@@ -144,16 +154,18 @@ async def update_memory_by_id(
|
||||
raise HTTPException(status_code=404, detail="Memory not found")
|
||||
|
||||
if form_data.content is not None:
|
||||
memory_embedding = request.app.state.EMBEDDING_FUNCTION(form_data.content)
|
||||
collection = CHROMA_CLIENT.get_or_create_collection(
|
||||
name=f"user-memory-{user.id}"
|
||||
)
|
||||
collection.upsert(
|
||||
documents=[form_data.content],
|
||||
ids=[memory.id],
|
||||
embeddings=[memory_embedding],
|
||||
metadatas=[
|
||||
{"created_at": memory.created_at, "updated_at": memory.updated_at}
|
||||
VECTOR_DB_CLIENT.upsert(
|
||||
collection_name=f"user-memory-{user.id}",
|
||||
items=[
|
||||
{
|
||||
"id": memory.id,
|
||||
"text": memory.content,
|
||||
"vector": request.app.state.EMBEDDING_FUNCTION(memory.content),
|
||||
"metadata": {
|
||||
"created_at": memory.created_at,
|
||||
"updated_at": memory.updated_at,
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
@@ -170,10 +182,9 @@ 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}"
|
||||
VECTOR_DB_CLIENT.delete(
|
||||
collection_name=f"user-memory-{user.id}", ids=[memory_id]
|
||||
)
|
||||
collection.delete(ids=[memory_id])
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -18,8 +18,18 @@ router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=list[ModelResponse])
|
||||
async def get_models(user=Depends(get_verified_user)):
|
||||
return Models.get_all_models()
|
||||
async def get_models(id: Optional[str] = None, user=Depends(get_verified_user)):
|
||||
if id:
|
||||
model = Models.get_model_by_id(id)
|
||||
if model:
|
||||
return [model]
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
else:
|
||||
return Models.get_all_models()
|
||||
|
||||
|
||||
############################
|
||||
@@ -50,24 +60,6 @@ async def add_new_model(
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# GetModelById
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/", response_model=Optional[ModelModel])
|
||||
async def get_model_by_id(id: str, user=Depends(get_verified_user)):
|
||||
model = Models.get_model_by_id(id)
|
||||
|
||||
if model:
|
||||
return model
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# UpdateModelById
|
||||
############################
|
||||
|
||||
@@ -3,7 +3,7 @@ from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from open_webui.apps.webui.models.tools import ToolForm, ToolModel, ToolResponse, Tools
|
||||
from open_webui.apps.webui.utils import load_toolkit_module_by_id
|
||||
from open_webui.apps.webui.utils import load_toolkit_module_by_id, replace_imports
|
||||
from open_webui.config import CACHE_DIR, DATA_DIR
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
@@ -59,12 +59,11 @@ async def create_new_toolkit(
|
||||
|
||||
toolkit = Tools.get_tool_by_id(form_data.id)
|
||||
if toolkit is None:
|
||||
toolkit_path = os.path.join(TOOLS_DIR, f"{form_data.id}.py")
|
||||
try:
|
||||
with open(toolkit_path, "w") as tool_file:
|
||||
tool_file.write(form_data.content)
|
||||
|
||||
toolkit_module, frontmatter = load_toolkit_module_by_id(form_data.id)
|
||||
form_data.content = replace_imports(form_data.content)
|
||||
toolkit_module, frontmatter = load_toolkit_module_by_id(
|
||||
form_data.id, content=form_data.content
|
||||
)
|
||||
form_data.meta.manifest = frontmatter
|
||||
|
||||
TOOLS = request.app.state.TOOLS
|
||||
@@ -126,13 +125,11 @@ async def update_toolkit_by_id(
|
||||
form_data: ToolForm,
|
||||
user=Depends(get_admin_user),
|
||||
):
|
||||
toolkit_path = os.path.join(TOOLS_DIR, f"{id}.py")
|
||||
|
||||
try:
|
||||
with open(toolkit_path, "w") as tool_file:
|
||||
tool_file.write(form_data.content)
|
||||
|
||||
toolkit_module, frontmatter = load_toolkit_module_by_id(id)
|
||||
form_data.content = replace_imports(form_data.content)
|
||||
toolkit_module, frontmatter = load_toolkit_module_by_id(
|
||||
id, content=form_data.content
|
||||
)
|
||||
form_data.meta.manifest = frontmatter
|
||||
|
||||
TOOLS = request.app.state.TOOLS
|
||||
@@ -177,10 +174,6 @@ async def delete_toolkit_by_id(request: Request, id: str, user=Depends(get_admin
|
||||
if id in TOOLS:
|
||||
del TOOLS[id]
|
||||
|
||||
# delete the toolkit file
|
||||
toolkit_path = os.path.join(TOOLS_DIR, f"{id}.py")
|
||||
os.remove(toolkit_path)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from pathlib import Path
|
||||
import black
|
||||
import markdown
|
||||
from open_webui.config import DATA_DIR, ENABLE_ADMIN_EXPORT
|
||||
from open_webui.env import FONTS_DIR
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
||||
from fpdf import FPDF
|
||||
@@ -57,14 +58,11 @@ class ChatForm(BaseModel):
|
||||
async def download_chat_as_pdf(
|
||||
form_data: ChatForm,
|
||||
):
|
||||
global FONTS_DIR
|
||||
|
||||
pdf = FPDF()
|
||||
pdf.add_page()
|
||||
|
||||
# When running in docker, workdir is /app/backend, so fonts is in /app/backend/static/fonts
|
||||
FONTS_DIR = Path("./static/fonts")
|
||||
|
||||
# Non Docker Installation
|
||||
|
||||
# When running using `pip install` the static directory is in the site packages.
|
||||
if not FONTS_DIR.exists():
|
||||
FONTS_DIR = Path(site.getsitepackages()[0]) / "static/fonts"
|
||||
|
||||
@@ -3,15 +3,17 @@ import re
|
||||
import subprocess
|
||||
import sys
|
||||
from importlib import util
|
||||
import types
|
||||
import tempfile
|
||||
|
||||
from open_webui.apps.webui.models.functions import Functions
|
||||
from open_webui.apps.webui.models.tools import Tools
|
||||
from open_webui.config import FUNCTIONS_DIR, TOOLS_DIR
|
||||
|
||||
|
||||
def extract_frontmatter(file_path):
|
||||
def extract_frontmatter(content):
|
||||
"""
|
||||
Extract frontmatter as a dictionary from the specified file path.
|
||||
Extract frontmatter as a dictionary from the provided content string.
|
||||
"""
|
||||
frontmatter = {}
|
||||
frontmatter_started = False
|
||||
@@ -19,29 +21,25 @@ def extract_frontmatter(file_path):
|
||||
frontmatter_pattern = re.compile(r"^\s*([a-z_]+):\s*(.*)\s*$", re.IGNORECASE)
|
||||
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
first_line = file.readline()
|
||||
if first_line.strip() != '"""':
|
||||
# The file doesn't start with triple quotes
|
||||
return {}
|
||||
lines = content.splitlines()
|
||||
if len(lines) < 1 or lines[0].strip() != '"""':
|
||||
# The content doesn't start with triple quotes
|
||||
return {}
|
||||
|
||||
frontmatter_started = True
|
||||
frontmatter_started = True
|
||||
|
||||
for line in file:
|
||||
if '"""' in line:
|
||||
if frontmatter_started:
|
||||
frontmatter_ended = True
|
||||
break
|
||||
for line in lines[1:]:
|
||||
if '"""' in line:
|
||||
if frontmatter_started:
|
||||
frontmatter_ended = True
|
||||
break
|
||||
|
||||
if frontmatter_started and not frontmatter_ended:
|
||||
match = frontmatter_pattern.match(line)
|
||||
if match:
|
||||
key, value = match.groups()
|
||||
frontmatter[key.strip()] = value.strip()
|
||||
if frontmatter_started and not frontmatter_ended:
|
||||
match = frontmatter_pattern.match(line)
|
||||
if match:
|
||||
key, value = match.groups()
|
||||
frontmatter[key.strip()] = value.strip()
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"Error: The file {file_path} does not exist.")
|
||||
return {}
|
||||
except Exception as e:
|
||||
print(f"An error occurred: {e}")
|
||||
return {}
|
||||
@@ -49,55 +47,102 @@ def extract_frontmatter(file_path):
|
||||
return frontmatter
|
||||
|
||||
|
||||
def load_toolkit_module_by_id(toolkit_id):
|
||||
toolkit_path = os.path.join(TOOLS_DIR, f"{toolkit_id}.py")
|
||||
def replace_imports(content):
|
||||
"""
|
||||
Replace the import paths in the content.
|
||||
"""
|
||||
replacements = {
|
||||
"from utils": "from open_webui.utils",
|
||||
"from apps": "from open_webui.apps",
|
||||
"from main": "from open_webui.main",
|
||||
"from config": "from open_webui.config",
|
||||
}
|
||||
|
||||
if not os.path.exists(toolkit_path):
|
||||
for old, new in replacements.items():
|
||||
content = content.replace(old, new)
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def load_toolkit_module_by_id(toolkit_id, content=None):
|
||||
|
||||
if content is None:
|
||||
tool = Tools.get_tool_by_id(toolkit_id)
|
||||
if tool:
|
||||
with open(toolkit_path, "w") as file:
|
||||
file.write(tool.content)
|
||||
else:
|
||||
if not tool:
|
||||
raise Exception(f"Toolkit not found: {toolkit_id}")
|
||||
|
||||
spec = util.spec_from_file_location(toolkit_id, toolkit_path)
|
||||
module = util.module_from_spec(spec)
|
||||
frontmatter = extract_frontmatter(toolkit_path)
|
||||
content = tool.content
|
||||
|
||||
try:
|
||||
content = replace_imports(content)
|
||||
Tools.update_tool_by_id(toolkit_id, {"content": content})
|
||||
else:
|
||||
frontmatter = extract_frontmatter(content)
|
||||
# Install required packages found within the frontmatter
|
||||
install_frontmatter_requirements(frontmatter.get("requirements", ""))
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
module_name = f"tool_{toolkit_id}"
|
||||
module = types.ModuleType(module_name)
|
||||
sys.modules[module_name] = module
|
||||
|
||||
# Create a temporary file and use it to define `__file__` so
|
||||
# that it works as expected from the module's perspective.
|
||||
temp_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
temp_file.close()
|
||||
try:
|
||||
with open(temp_file.name, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
module.__dict__["__file__"] = temp_file.name
|
||||
|
||||
# Executing the modified content in the created module's namespace
|
||||
exec(content, module.__dict__)
|
||||
frontmatter = extract_frontmatter(content)
|
||||
print(f"Loaded module: {module.__name__}")
|
||||
|
||||
# Create and return the object if the class 'Tools' is found in the module
|
||||
if hasattr(module, "Tools"):
|
||||
return module.Tools(), frontmatter
|
||||
else:
|
||||
raise Exception("No Tools class found")
|
||||
raise Exception("No Tools class found in the module")
|
||||
except Exception as e:
|
||||
print(f"Error loading module: {toolkit_id}")
|
||||
# Move the file to the error folder
|
||||
os.rename(toolkit_path, f"{toolkit_path}.error")
|
||||
print(f"Error loading module: {toolkit_id}: {e}")
|
||||
del sys.modules[module_name] # Clean up
|
||||
raise e
|
||||
finally:
|
||||
os.unlink(temp_file.name)
|
||||
|
||||
|
||||
def load_function_module_by_id(function_id):
|
||||
function_path = os.path.join(FUNCTIONS_DIR, f"{function_id}.py")
|
||||
|
||||
if not os.path.exists(function_path):
|
||||
def load_function_module_by_id(function_id, content=None):
|
||||
if content is None:
|
||||
function = Functions.get_function_by_id(function_id)
|
||||
if function:
|
||||
with open(function_path, "w") as file:
|
||||
file.write(function.content)
|
||||
else:
|
||||
if not function:
|
||||
raise Exception(f"Function not found: {function_id}")
|
||||
content = function.content
|
||||
|
||||
spec = util.spec_from_file_location(function_id, function_path)
|
||||
module = util.module_from_spec(spec)
|
||||
frontmatter = extract_frontmatter(function_path)
|
||||
|
||||
try:
|
||||
content = replace_imports(content)
|
||||
Functions.update_function_by_id(function_id, {"content": content})
|
||||
else:
|
||||
frontmatter = extract_frontmatter(content)
|
||||
install_frontmatter_requirements(frontmatter.get("requirements", ""))
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
module_name = f"function_{function_id}"
|
||||
module = types.ModuleType(module_name)
|
||||
sys.modules[module_name] = module
|
||||
|
||||
# Create a temporary file and use it to define `__file__` so
|
||||
# that it works as expected from the module's perspective.
|
||||
temp_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
temp_file.close()
|
||||
try:
|
||||
with open(temp_file.name, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
module.__dict__["__file__"] = temp_file.name
|
||||
|
||||
# Execute the modified content in the created module's namespace
|
||||
exec(content, module.__dict__)
|
||||
frontmatter = extract_frontmatter(content)
|
||||
print(f"Loaded module: {module.__name__}")
|
||||
|
||||
# Create appropriate object based on available class type in the module
|
||||
if hasattr(module, "Pipe"):
|
||||
return module.Pipe(), "pipe", frontmatter
|
||||
elif hasattr(module, "Filter"):
|
||||
@@ -105,12 +150,15 @@ def load_function_module_by_id(function_id):
|
||||
elif hasattr(module, "Action"):
|
||||
return module.Action(), "action", frontmatter
|
||||
else:
|
||||
raise Exception("No Function class found")
|
||||
raise Exception("No Function class found in the module")
|
||||
except Exception as e:
|
||||
print(f"Error loading module: {function_id}")
|
||||
# Move the file to the error folder
|
||||
os.rename(function_path, f"{function_path}.error")
|
||||
print(f"Error loading module: {function_id}: {e}")
|
||||
del sys.modules[module_name] # Cleanup by removing the module in case of error
|
||||
|
||||
Functions.update_function_by_id(function_id, {"is_active": False})
|
||||
raise e
|
||||
finally:
|
||||
os.unlink(temp_file.name)
|
||||
|
||||
|
||||
def install_frontmatter_requirements(requirements):
|
||||
|
||||
+100
-143
@@ -11,7 +11,6 @@ import chromadb
|
||||
import requests
|
||||
import yaml
|
||||
from open_webui.apps.webui.internal.db import Base, get_db
|
||||
from chromadb import Settings
|
||||
from open_webui.env import (
|
||||
OPEN_WEBUI_DIR,
|
||||
DATA_DIR,
|
||||
@@ -88,6 +87,12 @@ def save_to_db(data):
|
||||
db.commit()
|
||||
|
||||
|
||||
def reset_config():
|
||||
with get_db() as db:
|
||||
db.query(Config).delete()
|
||||
db.commit()
|
||||
|
||||
|
||||
# When initializing, check if config.json exists and migrate it to the database
|
||||
if os.path.exists(f"{DATA_DIR}/config.json"):
|
||||
data = load_json_config()
|
||||
@@ -516,15 +521,6 @@ Path(UPLOAD_DIR).mkdir(parents=True, exist_ok=True)
|
||||
CACHE_DIR = f"{DATA_DIR}/cache"
|
||||
Path(CACHE_DIR).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
####################################
|
||||
# Docs DIR
|
||||
####################################
|
||||
|
||||
DOCS_DIR = os.getenv("DOCS_DIR", f"{DATA_DIR}/docs")
|
||||
Path(DOCS_DIR).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
####################################
|
||||
# Tools DIR
|
||||
####################################
|
||||
@@ -540,40 +536,6 @@ Path(TOOLS_DIR).mkdir(parents=True, exist_ok=True)
|
||||
FUNCTIONS_DIR = os.getenv("FUNCTIONS_DIR", f"{DATA_DIR}/functions")
|
||||
Path(FUNCTIONS_DIR).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
####################################
|
||||
# LITELLM_CONFIG
|
||||
####################################
|
||||
|
||||
|
||||
def create_config_file(file_path):
|
||||
directory = os.path.dirname(file_path)
|
||||
|
||||
# Check if directory exists, if not, create it
|
||||
if not os.path.exists(directory):
|
||||
os.makedirs(directory)
|
||||
|
||||
# Data to write into the YAML file
|
||||
config_data = {
|
||||
"general_settings": {},
|
||||
"litellm_settings": {},
|
||||
"model_list": [],
|
||||
"router_settings": {},
|
||||
}
|
||||
|
||||
# Write data to YAML file
|
||||
with open(file_path, "w") as file:
|
||||
yaml.dump(config_data, file)
|
||||
|
||||
|
||||
LITELLM_CONFIG_PATH = f"{DATA_DIR}/litellm/config.yaml"
|
||||
|
||||
# if not os.path.exists(LITELLM_CONFIG_PATH):
|
||||
# log.info("Config file doesn't exist. Creating...")
|
||||
# create_config_file(LITELLM_CONFIG_PATH)
|
||||
# log.info("Config file created successfully.")
|
||||
|
||||
|
||||
####################################
|
||||
# OLLAMA_BASE_URL
|
||||
####################################
|
||||
@@ -590,16 +552,6 @@ OLLAMA_API_BASE_URL = os.environ.get(
|
||||
)
|
||||
|
||||
OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "")
|
||||
AIOHTTP_CLIENT_TIMEOUT = os.environ.get("AIOHTTP_CLIENT_TIMEOUT", "")
|
||||
|
||||
if AIOHTTP_CLIENT_TIMEOUT == "":
|
||||
AIOHTTP_CLIENT_TIMEOUT = None
|
||||
else:
|
||||
try:
|
||||
AIOHTTP_CLIENT_TIMEOUT = int(AIOHTTP_CLIENT_TIMEOUT)
|
||||
except Exception:
|
||||
AIOHTTP_CLIENT_TIMEOUT = 300
|
||||
|
||||
|
||||
K8S_FLAG = os.environ.get("K8S_FLAG", "")
|
||||
USE_OLLAMA_DOCKER = os.environ.get("USE_OLLAMA_DOCKER", "false")
|
||||
@@ -898,76 +850,37 @@ TASK_MODEL_EXTERNAL = PersistentConfig(
|
||||
TITLE_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
|
||||
"TITLE_GENERATION_PROMPT_TEMPLATE",
|
||||
"task.title.prompt_template",
|
||||
os.environ.get(
|
||||
"TITLE_GENERATION_PROMPT_TEMPLATE",
|
||||
"""Create a concise, 3-5 word title with an emoji as a title for the prompt in the given language. Suitable Emojis for the summary can be used to enhance understanding but avoid quotation marks or special formatting. RESPOND ONLY WITH THE TITLE TEXT.
|
||||
os.environ.get("TITLE_GENERATION_PROMPT_TEMPLATE", ""),
|
||||
)
|
||||
|
||||
Examples of titles:
|
||||
📉 Stock Market Trends
|
||||
🍪 Perfect Chocolate Chip Recipe
|
||||
Evolution of Music Streaming
|
||||
Remote Work Productivity Tips
|
||||
Artificial Intelligence in Healthcare
|
||||
🎮 Video Game Development Insights
|
||||
|
||||
Prompt: {{prompt:middletruncate:8000}}""",
|
||||
),
|
||||
ENABLE_SEARCH_QUERY = PersistentConfig(
|
||||
"ENABLE_SEARCH_QUERY",
|
||||
"task.search.enable",
|
||||
os.environ.get("ENABLE_SEARCH_QUERY", "True").lower() == "true",
|
||||
)
|
||||
|
||||
|
||||
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
|
||||
"SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE",
|
||||
"task.search.prompt_template",
|
||||
os.environ.get(
|
||||
"SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE",
|
||||
"""You are tasked with generating web search queries. Give me an appropriate query to answer my question for google search. Answer with only the query. Today is {{CURRENT_DATE}}.
|
||||
|
||||
Question:
|
||||
{{prompt:end:4000}}""",
|
||||
),
|
||||
os.environ.get("SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE", ""),
|
||||
)
|
||||
|
||||
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD = PersistentConfig(
|
||||
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD",
|
||||
"task.search.prompt_length_threshold",
|
||||
int(
|
||||
os.environ.get(
|
||||
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD",
|
||||
100,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = PersistentConfig(
|
||||
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE",
|
||||
"task.tools.prompt_template",
|
||||
os.environ.get(
|
||||
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE",
|
||||
"""Available Tools: {{TOOLS}}\nReturn an empty string if no tools match the query. If a function tool matches, construct and return a JSON object in the format {\"name\": \"functionName\", \"parameters\": {\"requiredFunctionParamKey\": \"requiredFunctionParamValue\"}} using the appropriate tool and its parameters. Only return the object and limit the response to the JSON object without additional text.""",
|
||||
),
|
||||
os.environ.get("TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE", ""),
|
||||
)
|
||||
|
||||
|
||||
####################################
|
||||
# RAG document content extraction
|
||||
# Vector Database
|
||||
####################################
|
||||
|
||||
CONTENT_EXTRACTION_ENGINE = PersistentConfig(
|
||||
"CONTENT_EXTRACTION_ENGINE",
|
||||
"rag.CONTENT_EXTRACTION_ENGINE",
|
||||
os.environ.get("CONTENT_EXTRACTION_ENGINE", "").lower(),
|
||||
)
|
||||
|
||||
TIKA_SERVER_URL = PersistentConfig(
|
||||
"TIKA_SERVER_URL",
|
||||
"rag.tika_server_url",
|
||||
os.getenv("TIKA_SERVER_URL", "http://tika:9998"), # Default for sidecar deployment
|
||||
)
|
||||
|
||||
####################################
|
||||
# RAG
|
||||
####################################
|
||||
VECTOR_DB = os.environ.get("VECTOR_DB", "chroma")
|
||||
|
||||
# Chroma
|
||||
CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db"
|
||||
CHROMA_TENANT = os.environ.get("CHROMA_TENANT", chromadb.DEFAULT_TENANT)
|
||||
CHROMA_DATABASE = os.environ.get("CHROMA_DATABASE", chromadb.DEFAULT_DATABASE)
|
||||
@@ -984,8 +897,29 @@ 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)
|
||||
|
||||
# Milvus
|
||||
|
||||
MILVUS_URI = os.environ.get("MILVUS_URI", f"{DATA_DIR}/vector_db/milvus.db")
|
||||
|
||||
####################################
|
||||
# Information Retrieval (RAG)
|
||||
####################################
|
||||
|
||||
# RAG Content Extraction
|
||||
CONTENT_EXTRACTION_ENGINE = PersistentConfig(
|
||||
"CONTENT_EXTRACTION_ENGINE",
|
||||
"rag.CONTENT_EXTRACTION_ENGINE",
|
||||
os.environ.get("CONTENT_EXTRACTION_ENGINE", "").lower(),
|
||||
)
|
||||
|
||||
TIKA_SERVER_URL = PersistentConfig(
|
||||
"TIKA_SERVER_URL",
|
||||
"rag.tika_server_url",
|
||||
os.getenv("TIKA_SERVER_URL", "http://tika:9998"), # Default for sidecar deployment
|
||||
)
|
||||
|
||||
RAG_TOP_K = PersistentConfig(
|
||||
"RAG_TOP_K", "rag.top_k", int(os.environ.get("RAG_TOP_K", "5"))
|
||||
"RAG_TOP_K", "rag.top_k", int(os.environ.get("RAG_TOP_K", "3"))
|
||||
)
|
||||
RAG_RELEVANCE_THRESHOLD = PersistentConfig(
|
||||
"RAG_RELEVANCE_THRESHOLD",
|
||||
@@ -1074,36 +1008,8 @@ RAG_RERANKING_MODEL_TRUST_REMOTE_CODE = (
|
||||
os.environ.get("RAG_RERANKING_MODEL_TRUST_REMOTE_CODE", "").lower() == "true"
|
||||
)
|
||||
|
||||
|
||||
if CHROMA_HTTP_HOST != "":
|
||||
CHROMA_CLIENT = chromadb.HttpClient(
|
||||
host=CHROMA_HTTP_HOST,
|
||||
port=CHROMA_HTTP_PORT,
|
||||
headers=CHROMA_HTTP_HEADERS,
|
||||
ssl=CHROMA_HTTP_SSL,
|
||||
tenant=CHROMA_TENANT,
|
||||
database=CHROMA_DATABASE,
|
||||
settings=Settings(allow_reset=True, anonymized_telemetry=False),
|
||||
)
|
||||
else:
|
||||
CHROMA_CLIENT = chromadb.PersistentClient(
|
||||
path=CHROMA_DATA_PATH,
|
||||
settings=Settings(allow_reset=True, anonymized_telemetry=False),
|
||||
tenant=CHROMA_TENANT,
|
||||
database=CHROMA_DATABASE,
|
||||
)
|
||||
|
||||
|
||||
# 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_SIZE", "rag.chunk_size", int(os.environ.get("CHUNK_SIZE", "1000"))
|
||||
)
|
||||
CHUNK_OVERLAP = PersistentConfig(
|
||||
"CHUNK_OVERLAP",
|
||||
@@ -1111,19 +1017,25 @@ CHUNK_OVERLAP = PersistentConfig(
|
||||
int(os.environ.get("CHUNK_OVERLAP", "100")),
|
||||
)
|
||||
|
||||
DEFAULT_RAG_TEMPLATE = """Use the following context as your learned knowledge, inside <context></context> XML tags.
|
||||
DEFAULT_RAG_TEMPLATE = """You are given a user query, some textual context and rules, all inside xml tags. You have to answer the query based on the context while respecting the rules.
|
||||
|
||||
<context>
|
||||
[context]
|
||||
[context]
|
||||
</context>
|
||||
|
||||
When answer to user:
|
||||
- If you don't know, just say that you don't know.
|
||||
- If you don't know when you are not sure, ask for clarification.
|
||||
Avoid mentioning that you obtained the information from the context.
|
||||
And answer according to the language of the user's question.
|
||||
<rules>
|
||||
- If you don't know, just say so.
|
||||
- If you are not sure, ask for clarification.
|
||||
- Answer in the same language as the user query.
|
||||
- If the context appears unreadable or of poor quality, tell the user then answer as best as you can.
|
||||
- If the answer is not in the context but you think you know the answer, explain that to the user then answer with your own knowledge.
|
||||
- Answer directly and without using xml tags.
|
||||
</rules>
|
||||
|
||||
Given the context information, answer the query.
|
||||
Query: [query]"""
|
||||
<user_query>
|
||||
[query]
|
||||
</user_query>
|
||||
"""
|
||||
|
||||
RAG_TEMPLATE = PersistentConfig(
|
||||
"RAG_TEMPLATE",
|
||||
@@ -1293,6 +1205,37 @@ AUTOMATIC1111_API_AUTH = PersistentConfig(
|
||||
os.getenv("AUTOMATIC1111_API_AUTH", ""),
|
||||
)
|
||||
|
||||
AUTOMATIC1111_CFG_SCALE = PersistentConfig(
|
||||
"AUTOMATIC1111_CFG_SCALE",
|
||||
"image_generation.automatic1111.cfg_scale",
|
||||
(
|
||||
float(os.environ.get("AUTOMATIC1111_CFG_SCALE"))
|
||||
if os.environ.get("AUTOMATIC1111_CFG_SCALE")
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
AUTOMATIC1111_SAMPLER = PersistentConfig(
|
||||
"AUTOMATIC1111_SAMPLERE",
|
||||
"image_generation.automatic1111.sampler",
|
||||
(
|
||||
os.environ.get("AUTOMATIC1111_SAMPLER")
|
||||
if os.environ.get("AUTOMATIC1111_SAMPLER")
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
AUTOMATIC1111_SCHEDULER = PersistentConfig(
|
||||
"AUTOMATIC1111_SCHEDULER",
|
||||
"image_generation.automatic1111.scheduler",
|
||||
(
|
||||
os.environ.get("AUTOMATIC1111_SCHEDULER")
|
||||
if os.environ.get("AUTOMATIC1111_SCHEDULER")
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
COMFYUI_BASE_URL = PersistentConfig(
|
||||
"COMFYUI_BASE_URL",
|
||||
"image_generation.comfyui.base_url",
|
||||
@@ -1516,3 +1459,17 @@ AUDIO_TTS_SPLIT_ON = PersistentConfig(
|
||||
"audio.tts.split_on",
|
||||
os.getenv("AUDIO_TTS_SPLIT_ON", "punctuation"),
|
||||
)
|
||||
|
||||
AUDIO_TTS_AZURE_SPEECH_REGION = PersistentConfig(
|
||||
"AUDIO_TTS_AZURE_SPEECH_REGION",
|
||||
"audio.tts.azure.speech_region",
|
||||
os.getenv("AUDIO_TTS_AZURE_SPEECH_REGION", "eastus"),
|
||||
)
|
||||
|
||||
AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT = PersistentConfig(
|
||||
"AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT",
|
||||
"audio.tts.azure.speech_output_format",
|
||||
os.getenv(
|
||||
"AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT", "audio-24khz-160kbitrate-mono-mp3"
|
||||
),
|
||||
)
|
||||
|
||||
@@ -34,8 +34,8 @@ class ERROR_MESSAGES(str, Enum):
|
||||
|
||||
ID_TAKEN = "Uh-oh! This id is already registered. Please choose another id string."
|
||||
MODEL_ID_TAKEN = "Uh-oh! This model id is already registered. Please choose another model id string."
|
||||
|
||||
NAME_TAG_TAKEN = "Uh-oh! This name tag is already registered. Please choose another name tag string."
|
||||
|
||||
INVALID_TOKEN = (
|
||||
"Your session has expired or the token is invalid. Please sign in again."
|
||||
)
|
||||
@@ -90,6 +90,15 @@ class ERROR_MESSAGES(str, Enum):
|
||||
"The Ollama API is disabled. Please enable it to use this feature."
|
||||
)
|
||||
|
||||
FILE_TOO_LARGE = (
|
||||
lambda size="": f"Oops! The file you're trying to upload is too large. Please upload a file that is less than {size}."
|
||||
)
|
||||
|
||||
DUPLICATE_CONTENT = (
|
||||
"Duplicate content detected. Please provide unique content to proceed."
|
||||
)
|
||||
FILE_NOT_PROCESSED = "Extracted content is not available for this file. Please ensure that the file is processed before proceeding."
|
||||
|
||||
|
||||
class TASKS(str, Enum):
|
||||
def __str__(self) -> str:
|
||||
|
||||
+103
-24
@@ -31,6 +31,28 @@ try:
|
||||
except ImportError:
|
||||
print("dotenv not installed, skipping...")
|
||||
|
||||
DOCKER = os.environ.get("DOCKER", "False").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":
|
||||
try:
|
||||
import torch
|
||||
|
||||
assert torch.cuda.is_available(), "CUDA not available"
|
||||
DEVICE_TYPE = "cuda"
|
||||
except Exception as e:
|
||||
cuda_error = (
|
||||
"Error when testing CUDA but USE_CUDA_DOCKER is true. "
|
||||
f"Resetting USE_CUDA_DOCKER to false: {e}"
|
||||
)
|
||||
os.environ["USE_CUDA_DOCKER"] = "false"
|
||||
USE_CUDA = "false"
|
||||
DEVICE_TYPE = "cpu"
|
||||
else:
|
||||
DEVICE_TYPE = "cpu"
|
||||
|
||||
|
||||
####################################
|
||||
# LOGGING
|
||||
@@ -47,6 +69,9 @@ else:
|
||||
log = logging.getLogger(__name__)
|
||||
log.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}")
|
||||
|
||||
if "cuda_error" in locals():
|
||||
log.exception(cuda_error)
|
||||
|
||||
log_sources = [
|
||||
"AUDIO",
|
||||
"COMFYUI",
|
||||
@@ -59,6 +84,7 @@ log_sources = [
|
||||
"OPENAI",
|
||||
"RAG",
|
||||
"WEBHOOK",
|
||||
"SOCKET",
|
||||
]
|
||||
|
||||
SRC_LOG_LEVELS = {}
|
||||
@@ -88,15 +114,9 @@ WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png"
|
||||
|
||||
ENV = os.environ.get("ENV", "dev")
|
||||
|
||||
PIP_INSTALL = False
|
||||
try:
|
||||
importlib.metadata.version("open-webui")
|
||||
PIP_INSTALL = True
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
pass
|
||||
FROM_INIT_PY = os.environ.get("FROM_INIT_PY", "False").lower() == "true"
|
||||
|
||||
|
||||
if PIP_INSTALL:
|
||||
if FROM_INIT_PY:
|
||||
PACKAGE_DATA = {"version": importlib.metadata.version("open-webui")}
|
||||
else:
|
||||
try:
|
||||
@@ -187,12 +207,12 @@ WEBUI_BUILD_HASH = os.environ.get("WEBUI_BUILD_HASH", "dev-build")
|
||||
|
||||
DATA_DIR = Path(os.getenv("DATA_DIR", BACKEND_DIR / "data")).resolve()
|
||||
|
||||
if PIP_INSTALL:
|
||||
if FROM_INIT_PY:
|
||||
NEW_DATA_DIR = Path(os.getenv("DATA_DIR", OPEN_WEBUI_DIR / "data")).resolve()
|
||||
NEW_DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Check if the data directory exists in the package directory
|
||||
if DATA_DIR.exists():
|
||||
if DATA_DIR.exists() and DATA_DIR != NEW_DATA_DIR:
|
||||
log.info(f"Moving {DATA_DIR} to {NEW_DATA_DIR}")
|
||||
for item in DATA_DIR.iterdir():
|
||||
dest = NEW_DATA_DIR / item.name
|
||||
@@ -201,28 +221,25 @@ if PIP_INSTALL:
|
||||
else:
|
||||
shutil.copy2(item, dest)
|
||||
|
||||
DATA_DIR = OPEN_WEBUI_DIR / "data"
|
||||
# Zip the data directory
|
||||
shutil.make_archive(DATA_DIR.parent / "open_webui_data", "zip", DATA_DIR)
|
||||
|
||||
# Remove the old data directory
|
||||
shutil.rmtree(DATA_DIR)
|
||||
|
||||
DATA_DIR = Path(os.getenv("DATA_DIR", OPEN_WEBUI_DIR / "data"))
|
||||
|
||||
|
||||
FONTS_DIR = Path(os.getenv("FONTS_DIR", OPEN_WEBUI_DIR / "static" / "fonts"))
|
||||
|
||||
FRONTEND_BUILD_DIR = Path(os.getenv("FRONTEND_BUILD_DIR", BASE_DIR / "build")).resolve()
|
||||
if PIP_INSTALL:
|
||||
|
||||
if FROM_INIT_PY:
|
||||
FRONTEND_BUILD_DIR = Path(
|
||||
os.getenv("FRONTEND_BUILD_DIR", OPEN_WEBUI_DIR / "frontend")
|
||||
).resolve()
|
||||
|
||||
|
||||
RESET_CONFIG_ON_START = (
|
||||
os.environ.get("RESET_CONFIG_ON_START", "False").lower() == "true"
|
||||
)
|
||||
|
||||
if RESET_CONFIG_ON_START:
|
||||
try:
|
||||
os.remove(f"{DATA_DIR}/config.json")
|
||||
with open(f"{DATA_DIR}/config.json", "w") as f:
|
||||
f.write("{}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
####################################
|
||||
# Database
|
||||
####################################
|
||||
@@ -241,6 +258,49 @@ DATABASE_URL = os.environ.get("DATABASE_URL", f"sqlite:///{DATA_DIR}/webui.db")
|
||||
if "postgres://" in DATABASE_URL:
|
||||
DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://")
|
||||
|
||||
DATABASE_POOL_SIZE = os.environ.get("DATABASE_POOL_SIZE", 0)
|
||||
|
||||
if DATABASE_POOL_SIZE == "":
|
||||
DATABASE_POOL_SIZE = 0
|
||||
else:
|
||||
try:
|
||||
DATABASE_POOL_SIZE = int(DATABASE_POOL_SIZE)
|
||||
except Exception:
|
||||
DATABASE_POOL_SIZE = 0
|
||||
|
||||
DATABASE_POOL_MAX_OVERFLOW = os.environ.get("DATABASE_POOL_MAX_OVERFLOW", 0)
|
||||
|
||||
if DATABASE_POOL_MAX_OVERFLOW == "":
|
||||
DATABASE_POOL_MAX_OVERFLOW = 0
|
||||
else:
|
||||
try:
|
||||
DATABASE_POOL_MAX_OVERFLOW = int(DATABASE_POOL_MAX_OVERFLOW)
|
||||
except Exception:
|
||||
DATABASE_POOL_MAX_OVERFLOW = 0
|
||||
|
||||
DATABASE_POOL_TIMEOUT = os.environ.get("DATABASE_POOL_TIMEOUT", 30)
|
||||
|
||||
if DATABASE_POOL_TIMEOUT == "":
|
||||
DATABASE_POOL_TIMEOUT = 30
|
||||
else:
|
||||
try:
|
||||
DATABASE_POOL_TIMEOUT = int(DATABASE_POOL_TIMEOUT)
|
||||
except Exception:
|
||||
DATABASE_POOL_TIMEOUT = 30
|
||||
|
||||
DATABASE_POOL_RECYCLE = os.environ.get("DATABASE_POOL_RECYCLE", 3600)
|
||||
|
||||
if DATABASE_POOL_RECYCLE == "":
|
||||
DATABASE_POOL_RECYCLE = 3600
|
||||
else:
|
||||
try:
|
||||
DATABASE_POOL_RECYCLE = int(DATABASE_POOL_RECYCLE)
|
||||
except Exception:
|
||||
DATABASE_POOL_RECYCLE = 3600
|
||||
|
||||
RESET_CONFIG_ON_START = (
|
||||
os.environ.get("RESET_CONFIG_ON_START", "False").lower() == "true"
|
||||
)
|
||||
|
||||
####################################
|
||||
# WEBUI_AUTH (Required for security)
|
||||
@@ -276,3 +336,22 @@ WEBUI_SESSION_COOKIE_SECURE = os.environ.get(
|
||||
|
||||
if WEBUI_AUTH and WEBUI_SECRET_KEY == "":
|
||||
raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND)
|
||||
|
||||
ENABLE_WEBSOCKET_SUPPORT = (
|
||||
os.environ.get("ENABLE_WEBSOCKET_SUPPORT", "True").lower() == "true"
|
||||
)
|
||||
|
||||
WEBSOCKET_MANAGER = os.environ.get("WEBSOCKET_MANAGER", "")
|
||||
|
||||
WEBSOCKET_REDIS_URL = os.environ.get("WEBSOCKET_REDIS_URL", "redis://localhost:6379/0")
|
||||
|
||||
|
||||
AIOHTTP_CLIENT_TIMEOUT = os.environ.get("AIOHTTP_CLIENT_TIMEOUT", "")
|
||||
|
||||
if AIOHTTP_CLIENT_TIMEOUT == "":
|
||||
AIOHTTP_CLIENT_TIMEOUT = None
|
||||
else:
|
||||
try:
|
||||
AIOHTTP_CLIENT_TIMEOUT = int(AIOHTTP_CLIENT_TIMEOUT)
|
||||
except Exception:
|
||||
AIOHTTP_CLIENT_TIMEOUT = 300
|
||||
|
||||
+245
-116
@@ -8,41 +8,53 @@ import shutil
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
import asyncio
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Optional
|
||||
|
||||
import aiohttp
|
||||
import requests
|
||||
|
||||
|
||||
from open_webui.apps.audio.main import app as audio_app
|
||||
from open_webui.apps.images.main import app as images_app
|
||||
from open_webui.apps.ollama.main import app as ollama_app
|
||||
from open_webui.apps.ollama.main import (
|
||||
generate_openai_chat_completion as generate_ollama_chat_completion,
|
||||
app as ollama_app,
|
||||
get_all_models as get_ollama_models,
|
||||
generate_chat_completion as generate_ollama_chat_completion,
|
||||
generate_openai_chat_completion as generate_ollama_openai_chat_completion,
|
||||
GenerateChatCompletionForm,
|
||||
)
|
||||
from open_webui.apps.ollama.main import get_all_models as get_ollama_models
|
||||
from open_webui.apps.openai.main import app as openai_app
|
||||
from open_webui.apps.openai.main import (
|
||||
app as openai_app,
|
||||
generate_chat_completion as generate_openai_chat_completion,
|
||||
get_all_models as get_openai_models,
|
||||
)
|
||||
from open_webui.apps.openai.main import get_all_models as get_openai_models
|
||||
from open_webui.apps.rag.main import app as rag_app
|
||||
from open_webui.apps.rag.utils import get_rag_context, rag_template
|
||||
from open_webui.apps.socket.main import app as socket_app
|
||||
from open_webui.apps.socket.main import get_event_call, get_event_emitter
|
||||
from open_webui.apps.webui.internal.db import Session
|
||||
from open_webui.apps.webui.main import app as webui_app
|
||||
|
||||
from open_webui.apps.retrieval.main import app as retrieval_app
|
||||
from open_webui.apps.retrieval.utils import get_rag_context, rag_template
|
||||
|
||||
from open_webui.apps.socket.main import (
|
||||
app as socket_app,
|
||||
periodic_usage_pool_cleanup,
|
||||
get_event_call,
|
||||
get_event_emitter,
|
||||
)
|
||||
|
||||
from open_webui.apps.webui.main import (
|
||||
app as webui_app,
|
||||
generate_function_chat_completion,
|
||||
get_pipe_models,
|
||||
)
|
||||
from open_webui.apps.webui.internal.db import Session
|
||||
|
||||
from open_webui.apps.webui.models.auths import Auths
|
||||
from open_webui.apps.webui.models.functions import Functions
|
||||
from open_webui.apps.webui.models.models import Models
|
||||
from open_webui.apps.webui.models.users import UserModel, Users
|
||||
|
||||
from open_webui.apps.webui.utils import load_function_module_by_id
|
||||
|
||||
from open_webui.apps.audio.main import app as audio_app
|
||||
from open_webui.apps.images.main import app as images_app
|
||||
|
||||
from authlib.integrations.starlette_client import OAuth
|
||||
from authlib.oidc.core import UserInfo
|
||||
@@ -63,8 +75,8 @@ from open_webui.config import (
|
||||
MODEL_FILTER_LIST,
|
||||
OAUTH_MERGE_ACCOUNTS_BY_EMAIL,
|
||||
OAUTH_PROVIDERS,
|
||||
ENABLE_SEARCH_QUERY,
|
||||
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
|
||||
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD,
|
||||
STATIC_DIR,
|
||||
TASK_MODEL,
|
||||
TASK_MODEL_EXTERNAL,
|
||||
@@ -75,6 +87,7 @@ from open_webui.config import (
|
||||
WEBUI_NAME,
|
||||
AppConfig,
|
||||
run_migrations,
|
||||
reset_config,
|
||||
)
|
||||
from open_webui.constants import ERROR_MESSAGES, TASKS, WEBHOOK_MESSAGES
|
||||
from open_webui.env import (
|
||||
@@ -88,6 +101,7 @@ from open_webui.env import (
|
||||
WEBUI_SESSION_COOKIE_SAME_SITE,
|
||||
WEBUI_SESSION_COOKIE_SECURE,
|
||||
WEBUI_URL,
|
||||
RESET_CONFIG_ON_START,
|
||||
)
|
||||
from fastapi import (
|
||||
Depends,
|
||||
@@ -109,6 +123,7 @@ from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
from starlette.responses import RedirectResponse, Response, StreamingResponse
|
||||
|
||||
from open_webui.utils.security_headers import SecurityHeadersMiddleware
|
||||
|
||||
from open_webui.utils.misc import (
|
||||
add_or_update_system_message,
|
||||
@@ -134,6 +149,12 @@ from open_webui.utils.utils import (
|
||||
)
|
||||
from open_webui.utils.webhook import post_webhook
|
||||
|
||||
from open_webui.utils.payload import convert_payload_openai_to_ollama
|
||||
from open_webui.utils.response import (
|
||||
convert_response_ollama_to_openai,
|
||||
convert_streaming_response_ollama_to_openai,
|
||||
)
|
||||
|
||||
if SAFE_MODE:
|
||||
print("SAFE MODE ENABLED")
|
||||
Functions.deactivate_all_functions()
|
||||
@@ -174,7 +195,10 @@ https://github.com/open-webui/open-webui
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
run_migrations()
|
||||
if RESET_CONFIG_ON_START:
|
||||
reset_config()
|
||||
|
||||
asyncio.create_task(periodic_usage_pool_cleanup())
|
||||
yield
|
||||
|
||||
|
||||
@@ -199,9 +223,7 @@ app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = TITLE_GENERATION_PROMPT_TEMP
|
||||
app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE = (
|
||||
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
|
||||
)
|
||||
app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD = (
|
||||
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD
|
||||
)
|
||||
app.state.config.ENABLE_SEARCH_QUERY = ENABLE_SEARCH_QUERY
|
||||
app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = (
|
||||
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
|
||||
)
|
||||
@@ -397,8 +419,13 @@ async def chat_completion_tools_handler(
|
||||
specs = [tool["spec"] for tool in tools.values()]
|
||||
tools_specs = json.dumps(specs)
|
||||
|
||||
if app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE != "":
|
||||
template = app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
|
||||
else:
|
||||
template = """Available Tools: {{TOOLS}}\nReturn an empty string if no tools match the query. If a function tool matches, construct and return a JSON object in the format {\"name\": \"functionName\", \"parameters\": {\"requiredFunctionParamKey\": \"requiredFunctionParamValue\"}} using the appropriate tool and its parameters. Only return the object and limit the response to the JSON object without additional text."""
|
||||
|
||||
tools_function_calling_prompt = tools_function_calling_generation_template(
|
||||
app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE, tools_specs
|
||||
template, tools_specs
|
||||
)
|
||||
log.info(f"{tools_function_calling_prompt=}")
|
||||
payload = get_tools_function_calling_payload(
|
||||
@@ -419,37 +446,44 @@ async def chat_completion_tools_handler(
|
||||
if not content:
|
||||
return body, {}
|
||||
|
||||
result = json.loads(content)
|
||||
|
||||
tool_function_name = result.get("name", None)
|
||||
if tool_function_name not in tools:
|
||||
return body, {}
|
||||
|
||||
tool_function_params = result.get("parameters", {})
|
||||
|
||||
try:
|
||||
tool_output = await tools[tool_function_name]["callable"](
|
||||
**tool_function_params
|
||||
)
|
||||
content = content[content.find("{") : content.rfind("}") + 1]
|
||||
if not content:
|
||||
raise Exception("No JSON object found in the response")
|
||||
|
||||
result = json.loads(content)
|
||||
|
||||
tool_function_name = result.get("name", None)
|
||||
if tool_function_name not in tools:
|
||||
return body, {}
|
||||
|
||||
tool_function_params = result.get("parameters", {})
|
||||
|
||||
try:
|
||||
tool_output = await tools[tool_function_name]["callable"](
|
||||
**tool_function_params
|
||||
)
|
||||
except Exception as e:
|
||||
tool_output = str(e)
|
||||
|
||||
if tools[tool_function_name]["citation"]:
|
||||
citations.append(
|
||||
{
|
||||
"source": {
|
||||
"name": f"TOOL:{tools[tool_function_name]['toolkit_id']}/{tool_function_name}"
|
||||
},
|
||||
"document": [tool_output],
|
||||
"metadata": [{"source": tool_function_name}],
|
||||
}
|
||||
)
|
||||
if tools[tool_function_name]["file_handler"]:
|
||||
skip_files = True
|
||||
|
||||
if isinstance(tool_output, str):
|
||||
contexts.append(tool_output)
|
||||
except Exception as e:
|
||||
tool_output = str(e)
|
||||
|
||||
if tools[tool_function_name]["citation"]:
|
||||
citations.append(
|
||||
{
|
||||
"source": {
|
||||
"name": f"TOOL:{tools[tool_function_name]['toolkit_id']}/{tool_function_name}"
|
||||
},
|
||||
"document": [tool_output],
|
||||
"metadata": [{"source": tool_function_name}],
|
||||
}
|
||||
)
|
||||
if tools[tool_function_name]["file_handler"]:
|
||||
skip_files = True
|
||||
|
||||
if isinstance(tool_output, str):
|
||||
contexts.append(tool_output)
|
||||
|
||||
log.exception(f"Error: {e}")
|
||||
content = None
|
||||
except Exception as e:
|
||||
log.exception(f"Error: {e}")
|
||||
content = None
|
||||
@@ -470,11 +504,11 @@ async def chat_completion_files_handler(body) -> tuple[dict, dict[str, list]]:
|
||||
contexts, citations = get_rag_context(
|
||||
files=files,
|
||||
messages=body["messages"],
|
||||
embedding_function=rag_app.state.EMBEDDING_FUNCTION,
|
||||
k=rag_app.state.config.TOP_K,
|
||||
reranking_function=rag_app.state.sentence_transformer_rf,
|
||||
r=rag_app.state.config.RELEVANCE_THRESHOLD,
|
||||
hybrid_search=rag_app.state.config.ENABLE_RAG_HYBRID_SEARCH,
|
||||
embedding_function=retrieval_app.state.EMBEDDING_FUNCTION,
|
||||
k=retrieval_app.state.config.TOP_K,
|
||||
reranking_function=retrieval_app.state.sentence_transformer_rf,
|
||||
r=retrieval_app.state.config.RELEVANCE_THRESHOLD,
|
||||
hybrid_search=retrieval_app.state.config.ENABLE_RAG_HYBRID_SEARCH,
|
||||
)
|
||||
|
||||
log.debug(f"rag_contexts: {contexts}, citations: {citations}")
|
||||
@@ -583,21 +617,30 @@ class ChatCompletionMiddleware(BaseHTTPMiddleware):
|
||||
if len(contexts) > 0:
|
||||
context_string = "/n".join(contexts).strip()
|
||||
prompt = get_last_user_message(body["messages"])
|
||||
|
||||
if prompt is None:
|
||||
raise Exception("No user message found")
|
||||
if (
|
||||
retrieval_app.state.config.RELEVANCE_THRESHOLD == 0
|
||||
and context_string.strip() == ""
|
||||
):
|
||||
log.debug(
|
||||
f"With a 0 relevancy threshold for RAG, the context cannot be empty"
|
||||
)
|
||||
|
||||
# Workaround for Ollama 2.0+ system prompt issue
|
||||
# TODO: replace with add_or_update_system_message
|
||||
if model["owned_by"] == "ollama":
|
||||
body["messages"] = prepend_to_first_user_message_content(
|
||||
rag_template(
|
||||
rag_app.state.config.RAG_TEMPLATE, context_string, prompt
|
||||
retrieval_app.state.config.RAG_TEMPLATE, context_string, prompt
|
||||
),
|
||||
body["messages"],
|
||||
)
|
||||
else:
|
||||
body["messages"] = add_or_update_system_message(
|
||||
rag_template(
|
||||
rag_app.state.config.RAG_TEMPLATE, context_string, prompt
|
||||
retrieval_app.state.config.RAG_TEMPLATE, context_string, prompt
|
||||
),
|
||||
body["messages"],
|
||||
)
|
||||
@@ -731,10 +774,22 @@ class PipelineMiddleware(BaseHTTPMiddleware):
|
||||
# Parse string to JSON
|
||||
data = json.loads(body_str) if body_str else {}
|
||||
|
||||
user = get_current_user(
|
||||
request,
|
||||
get_http_authorization_cred(request.headers["Authorization"]),
|
||||
)
|
||||
try:
|
||||
user = get_current_user(
|
||||
request,
|
||||
get_http_authorization_cred(request.headers["Authorization"]),
|
||||
)
|
||||
except KeyError as e:
|
||||
if len(e.args) > 1:
|
||||
return JSONResponse(
|
||||
status_code=e.args[0],
|
||||
content={"detail": e.args[1]},
|
||||
)
|
||||
else:
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
content={"detail": "Not authenticated"},
|
||||
)
|
||||
|
||||
try:
|
||||
data = filter_pipeline(data, user)
|
||||
@@ -777,6 +832,8 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.add_middleware(SecurityHeadersMiddleware)
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def commit_session_after_request(request: Request, call_next):
|
||||
@@ -805,22 +862,40 @@ async def check_url(request: Request, call_next):
|
||||
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
|
||||
webui_app.state.EMBEDDING_FUNCTION = retrieval_app.state.EMBEDDING_FUNCTION
|
||||
return response
|
||||
|
||||
|
||||
app.mount("/ws", socket_app)
|
||||
@app.middleware("http")
|
||||
async def inspect_websocket(request: Request, call_next):
|
||||
if (
|
||||
"/ws/socket.io" in request.url.path
|
||||
and request.query_params.get("transport") == "websocket"
|
||||
):
|
||||
upgrade = (request.headers.get("Upgrade") or "").lower()
|
||||
connection = (request.headers.get("Connection") or "").lower().split(",")
|
||||
# Check that there's the correct headers for an upgrade, else reject the connection
|
||||
# This is to work around this upstream issue: https://github.com/miguelgrinberg/python-engineio/issues/367
|
||||
if upgrade != "websocket" or "upgrade" not in connection:
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
content={"detail": "Invalid WebSocket upgrade request"},
|
||||
)
|
||||
return await call_next(request)
|
||||
|
||||
|
||||
app.mount("/ws", socket_app)
|
||||
app.mount("/ollama", ollama_app)
|
||||
app.mount("/openai", openai_app)
|
||||
|
||||
app.mount("/images/api/v1", images_app)
|
||||
app.mount("/audio/api/v1", audio_app)
|
||||
app.mount("/rag/api/v1", rag_app)
|
||||
app.mount("/retrieval/api/v1", retrieval_app)
|
||||
|
||||
app.mount("/api/v1", webui_app)
|
||||
|
||||
webui_app.state.EMBEDDING_FUNCTION = rag_app.state.EMBEDDING_FUNCTION
|
||||
|
||||
webui_app.state.EMBEDDING_FUNCTION = retrieval_app.state.EMBEDDING_FUNCTION
|
||||
|
||||
|
||||
async def get_all_models():
|
||||
@@ -1015,7 +1090,18 @@ async def generate_chat_completions(form_data: dict, user=Depends(get_verified_u
|
||||
if model.get("pipe"):
|
||||
return await generate_function_chat_completion(form_data, user=user)
|
||||
if model["owned_by"] == "ollama":
|
||||
return await generate_ollama_chat_completion(form_data, user=user)
|
||||
# Using /ollama/api/chat endpoint
|
||||
form_data = convert_payload_openai_to_ollama(form_data)
|
||||
form_data = GenerateChatCompletionForm(**form_data)
|
||||
response = await generate_ollama_chat_completion(form_data=form_data, user=user)
|
||||
if form_data.stream:
|
||||
response.headers["content-type"] = "text/event-stream"
|
||||
return StreamingResponse(
|
||||
convert_streaming_response_ollama_to_openai(response),
|
||||
headers=dict(response.headers),
|
||||
)
|
||||
else:
|
||||
return convert_response_ollama_to_openai(response)
|
||||
else:
|
||||
return await generate_openai_chat_completion(form_data, user=user)
|
||||
|
||||
@@ -1312,8 +1398,8 @@ async def get_task_config(user=Depends(get_verified_user)):
|
||||
"TASK_MODEL": app.state.config.TASK_MODEL,
|
||||
"TASK_MODEL_EXTERNAL": app.state.config.TASK_MODEL_EXTERNAL,
|
||||
"TITLE_GENERATION_PROMPT_TEMPLATE": app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
|
||||
"ENABLE_SEARCH_QUERY": app.state.config.ENABLE_SEARCH_QUERY,
|
||||
"SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE": app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
|
||||
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD": app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD,
|
||||
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE": app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
|
||||
}
|
||||
|
||||
@@ -1323,7 +1409,7 @@ class TaskConfigForm(BaseModel):
|
||||
TASK_MODEL_EXTERNAL: Optional[str]
|
||||
TITLE_GENERATION_PROMPT_TEMPLATE: str
|
||||
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE: str
|
||||
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD: int
|
||||
ENABLE_SEARCH_QUERY: bool
|
||||
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE: str
|
||||
|
||||
|
||||
@@ -1337,9 +1423,7 @@ async def update_task_config(form_data: TaskConfigForm, user=Depends(get_admin_u
|
||||
app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE = (
|
||||
form_data.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
|
||||
)
|
||||
app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD = (
|
||||
form_data.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD
|
||||
)
|
||||
app.state.config.ENABLE_SEARCH_QUERY = form_data.ENABLE_SEARCH_QUERY
|
||||
app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = (
|
||||
form_data.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
|
||||
)
|
||||
@@ -1349,7 +1433,7 @@ async def update_task_config(form_data: TaskConfigForm, user=Depends(get_admin_u
|
||||
"TASK_MODEL_EXTERNAL": app.state.config.TASK_MODEL_EXTERNAL,
|
||||
"TITLE_GENERATION_PROMPT_TEMPLATE": app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
|
||||
"SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE": app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
|
||||
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD": app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD,
|
||||
"ENABLE_SEARCH_QUERY": app.state.config.ENABLE_SEARCH_QUERY,
|
||||
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE": app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
|
||||
}
|
||||
|
||||
@@ -1367,11 +1451,25 @@ async def generate_title(form_data: dict, user=Depends(get_verified_user)):
|
||||
|
||||
# Check if the user has a custom task model
|
||||
# If the user has a custom task model, use that model
|
||||
model_id = get_task_model_id(model_id)
|
||||
task_model_id = get_task_model_id(model_id)
|
||||
print(task_model_id)
|
||||
|
||||
print(model_id)
|
||||
model = app.state.MODELS[task_model_id]
|
||||
|
||||
template = app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE
|
||||
if app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE != "":
|
||||
template = app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE
|
||||
else:
|
||||
template = """Create a concise, 3-5 word title with an emoji as a title for the prompt in the given language. Suitable Emojis for the summary can be used to enhance understanding but avoid quotation marks or special formatting. RESPOND ONLY WITH THE TITLE TEXT.
|
||||
|
||||
Examples of titles:
|
||||
📉 Stock Market Trends
|
||||
🍪 Perfect Chocolate Chip Recipe
|
||||
Evolution of Music Streaming
|
||||
Remote Work Productivity Tips
|
||||
Artificial Intelligence in Healthcare
|
||||
🎮 Video Game Development Insights
|
||||
|
||||
Prompt: {{prompt:middletruncate:8000}}"""
|
||||
|
||||
content = title_generation_template(
|
||||
template,
|
||||
@@ -1383,16 +1481,22 @@ async def generate_title(form_data: dict, user=Depends(get_verified_user)):
|
||||
)
|
||||
|
||||
payload = {
|
||||
"model": model_id,
|
||||
"model": task_model_id,
|
||||
"messages": [{"role": "user", "content": content}],
|
||||
"stream": False,
|
||||
"max_tokens": 50,
|
||||
**(
|
||||
{"max_tokens": 50}
|
||||
if app.state.MODELS[task_model_id]["owned_by"] == "ollama"
|
||||
else {
|
||||
"max_completion_tokens": 50,
|
||||
}
|
||||
),
|
||||
"chat_id": form_data.get("chat_id", None),
|
||||
"metadata": {"task": str(TASKS.TITLE_GENERATION)},
|
||||
"metadata": {"task": str(TASKS.TITLE_GENERATION), "task_body": form_data},
|
||||
}
|
||||
|
||||
log.debug(payload)
|
||||
|
||||
# Handle pipeline filters
|
||||
try:
|
||||
payload = filter_pipeline(payload, user)
|
||||
except Exception as e:
|
||||
@@ -1406,7 +1510,6 @@ async def generate_title(form_data: dict, user=Depends(get_verified_user)):
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
content={"detail": str(e)},
|
||||
)
|
||||
|
||||
if "chat_id" in payload:
|
||||
del payload["chat_id"]
|
||||
|
||||
@@ -1416,11 +1519,10 @@ async def generate_title(form_data: dict, user=Depends(get_verified_user)):
|
||||
@app.post("/api/task/query/completions")
|
||||
async def generate_search_query(form_data: dict, user=Depends(get_verified_user)):
|
||||
print("generate_search_query")
|
||||
|
||||
if len(form_data["prompt"]) < app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD:
|
||||
if not app.state.config.ENABLE_SEARCH_QUERY:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Skip search query generation for short prompts (< {app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD} characters)",
|
||||
detail=f"Search query generation is disabled",
|
||||
)
|
||||
|
||||
model_id = form_data["model"]
|
||||
@@ -1432,26 +1534,46 @@ async def generate_search_query(form_data: dict, user=Depends(get_verified_user)
|
||||
|
||||
# Check if the user has a custom task model
|
||||
# If the user has a custom task model, use that model
|
||||
model_id = get_task_model_id(model_id)
|
||||
task_model_id = get_task_model_id(model_id)
|
||||
print(task_model_id)
|
||||
|
||||
print(model_id)
|
||||
model = app.state.MODELS[task_model_id]
|
||||
|
||||
template = app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
|
||||
if app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE != "":
|
||||
template = app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
|
||||
else:
|
||||
template = """Given the user's message and interaction history, decide if a web search is necessary. You must be concise and exclusively provide a search query if one is necessary. Refrain from verbose responses or any additional commentary. Prefer suggesting a search if uncertain to provide comprehensive or updated information. If a search isn't needed at all, respond with an empty string. Default to a search query when in doubt. Today's date is {{CURRENT_DATE}}.
|
||||
|
||||
User Message:
|
||||
{{prompt:end:4000}}
|
||||
|
||||
Interaction History:
|
||||
{{MESSAGES:END:6}}
|
||||
|
||||
Search Query:"""
|
||||
|
||||
content = search_query_generation_template(
|
||||
template, form_data["prompt"], {"name": user.name}
|
||||
template, form_data["messages"], {"name": user.name}
|
||||
)
|
||||
|
||||
print("content", content)
|
||||
|
||||
payload = {
|
||||
"model": model_id,
|
||||
"model": task_model_id,
|
||||
"messages": [{"role": "user", "content": content}],
|
||||
"stream": False,
|
||||
"max_tokens": 30,
|
||||
"metadata": {"task": str(TASKS.QUERY_GENERATION)},
|
||||
**(
|
||||
{"max_tokens": 30}
|
||||
if app.state.MODELS[task_model_id]["owned_by"] == "ollama"
|
||||
else {
|
||||
"max_completion_tokens": 30,
|
||||
}
|
||||
),
|
||||
"metadata": {"task": str(TASKS.QUERY_GENERATION), "task_body": form_data},
|
||||
}
|
||||
log.debug(payload)
|
||||
|
||||
print(payload)
|
||||
|
||||
# Handle pipeline filters
|
||||
try:
|
||||
payload = filter_pipeline(payload, user)
|
||||
except Exception as e:
|
||||
@@ -1465,7 +1587,6 @@ async def generate_search_query(form_data: dict, user=Depends(get_verified_user)
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
content={"detail": str(e)},
|
||||
)
|
||||
|
||||
if "chat_id" in payload:
|
||||
del payload["chat_id"]
|
||||
|
||||
@@ -1485,16 +1606,16 @@ async def generate_emoji(form_data: dict, user=Depends(get_verified_user)):
|
||||
|
||||
# Check if the user has a custom task model
|
||||
# If the user has a custom task model, use that model
|
||||
model_id = get_task_model_id(model_id)
|
||||
task_model_id = get_task_model_id(model_id)
|
||||
print(task_model_id)
|
||||
|
||||
print(model_id)
|
||||
model = app.state.MODELS[task_model_id]
|
||||
|
||||
template = '''
|
||||
Your task is to reflect the speaker's likely facial expression through a fitting emoji. Interpret emotions from the message and reflect their facial expression using fitting, diverse emojis (e.g., 😊, 😢, 😡, 😱).
|
||||
|
||||
Message: """{{prompt}}"""
|
||||
'''
|
||||
|
||||
content = title_generation_template(
|
||||
template,
|
||||
form_data["prompt"],
|
||||
@@ -1505,16 +1626,22 @@ Message: """{{prompt}}"""
|
||||
)
|
||||
|
||||
payload = {
|
||||
"model": model_id,
|
||||
"model": task_model_id,
|
||||
"messages": [{"role": "user", "content": content}],
|
||||
"stream": False,
|
||||
"max_tokens": 4,
|
||||
**(
|
||||
{"max_tokens": 4}
|
||||
if app.state.MODELS[task_model_id]["owned_by"] == "ollama"
|
||||
else {
|
||||
"max_completion_tokens": 4,
|
||||
}
|
||||
),
|
||||
"chat_id": form_data.get("chat_id", None),
|
||||
"metadata": {"task": str(TASKS.EMOJI_GENERATION)},
|
||||
"metadata": {"task": str(TASKS.EMOJI_GENERATION), "task_body": form_data},
|
||||
}
|
||||
|
||||
log.debug(payload)
|
||||
|
||||
# Handle pipeline filters
|
||||
try:
|
||||
payload = filter_pipeline(payload, user)
|
||||
except Exception as e:
|
||||
@@ -1528,7 +1655,6 @@ Message: """{{prompt}}"""
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
content={"detail": str(e)},
|
||||
)
|
||||
|
||||
if "chat_id" in payload:
|
||||
del payload["chat_id"]
|
||||
|
||||
@@ -1548,8 +1674,10 @@ async def generate_moa_response(form_data: dict, user=Depends(get_verified_user)
|
||||
|
||||
# Check if the user has a custom task model
|
||||
# If the user has a custom task model, use that model
|
||||
model_id = get_task_model_id(model_id)
|
||||
print(model_id)
|
||||
task_model_id = get_task_model_id(model_id)
|
||||
print(task_model_id)
|
||||
|
||||
model = app.state.MODELS[task_model_id]
|
||||
|
||||
template = """You have been provided with a set of responses from various models to the latest user query: "{{prompt}}"
|
||||
|
||||
@@ -1564,13 +1692,15 @@ Responses from models: {{responses}}"""
|
||||
)
|
||||
|
||||
payload = {
|
||||
"model": model_id,
|
||||
"model": task_model_id,
|
||||
"messages": [{"role": "user", "content": content}],
|
||||
"stream": form_data.get("stream", False),
|
||||
"chat_id": form_data.get("chat_id", None),
|
||||
"metadata": {"task": str(TASKS.MOA_RESPONSE_GENERATION)},
|
||||
"metadata": {
|
||||
"task": str(TASKS.MOA_RESPONSE_GENERATION),
|
||||
"task_body": form_data,
|
||||
},
|
||||
}
|
||||
|
||||
log.debug(payload)
|
||||
|
||||
try:
|
||||
@@ -1586,7 +1716,6 @@ Responses from models: {{responses}}"""
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
content={"detail": str(e)},
|
||||
)
|
||||
|
||||
if "chat_id" in payload:
|
||||
del payload["chat_id"]
|
||||
|
||||
@@ -1954,7 +2083,7 @@ async def get_app_config(request: Request):
|
||||
"enable_login_form": webui_app.state.config.ENABLE_LOGIN_FORM,
|
||||
**(
|
||||
{
|
||||
"enable_web_search": rag_app.state.config.ENABLE_RAG_WEB_SEARCH,
|
||||
"enable_web_search": retrieval_app.state.config.ENABLE_RAG_WEB_SEARCH,
|
||||
"enable_image_generation": images_app.state.config.ENABLED,
|
||||
"enable_community_sharing": webui_app.state.config.ENABLE_COMMUNITY_SHARING,
|
||||
"enable_message_rating": webui_app.state.config.ENABLE_MESSAGE_RATING,
|
||||
@@ -1980,8 +2109,8 @@ async def get_app_config(request: Request):
|
||||
},
|
||||
},
|
||||
"file": {
|
||||
"max_size": rag_app.state.config.FILE_MAX_SIZE,
|
||||
"max_count": rag_app.state.config.FILE_MAX_COUNT,
|
||||
"max_size": retrieval_app.state.config.FILE_MAX_SIZE,
|
||||
"max_count": retrieval_app.state.config.FILE_MAX_COUNT,
|
||||
},
|
||||
"permissions": {**webui_app.state.config.USER_PERMISSIONS},
|
||||
}
|
||||
@@ -2053,7 +2182,8 @@ async def get_app_changelog():
|
||||
@app.get("/api/version/updates")
|
||||
async def get_app_latest_release_version():
|
||||
try:
|
||||
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||
timeout = aiohttp.ClientTimeout(total=1)
|
||||
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
|
||||
async with session.get(
|
||||
"https://api.github.com/repos/open-webui/open-webui/releases/latest"
|
||||
) as response:
|
||||
@@ -2062,11 +2192,9 @@ async def get_app_latest_release_version():
|
||||
latest_version = data["tag_name"]
|
||||
|
||||
return {"current": VERSION, "latest": latest_version[1:]}
|
||||
except aiohttp.ClientError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=ERROR_MESSAGES.RATE_LIMIT_EXCEEDED,
|
||||
)
|
||||
except Exception as e:
|
||||
log.debug(e)
|
||||
return {"current": VERSION, "latest": VERSION}
|
||||
|
||||
|
||||
############################
|
||||
@@ -2237,10 +2365,11 @@ async def get_manifest_json():
|
||||
return {
|
||||
"name": WEBUI_NAME,
|
||||
"short_name": WEBUI_NAME,
|
||||
"description": "Open WebUI is an open, extensible, user-friendly interface for AI that adapts to your workflow.",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#343541",
|
||||
"orientation": "portrait-primary",
|
||||
"orientation": "any",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/logo.png",
|
||||
|
||||
@@ -7,3 +7,9 @@ def get_existing_tables():
|
||||
inspector = Inspector.from_engine(con)
|
||||
tables = set(inspector.get_table_names())
|
||||
return tables
|
||||
|
||||
|
||||
def get_revision_id():
|
||||
import uuid
|
||||
|
||||
return str(uuid.uuid4()).replace("-", "")[:12]
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Add knowledge table
|
||||
|
||||
Revision ID: 6a39f3d8e55c
|
||||
Revises: c0fbf31ca0db
|
||||
Create Date: 2024-10-01 14:02:35.241684
|
||||
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.sql import table, column, select
|
||||
import json
|
||||
|
||||
|
||||
revision = "6a39f3d8e55c"
|
||||
down_revision = "c0fbf31ca0db"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# Creating the 'knowledge' table
|
||||
print("Creating knowledge table")
|
||||
knowledge_table = op.create_table(
|
||||
"knowledge",
|
||||
sa.Column("id", sa.Text(), primary_key=True),
|
||||
sa.Column("user_id", sa.Text(), nullable=False),
|
||||
sa.Column("name", sa.Text(), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("data", sa.JSON(), nullable=True),
|
||||
sa.Column("meta", sa.JSON(), nullable=True),
|
||||
sa.Column("created_at", sa.BigInteger(), nullable=False),
|
||||
sa.Column("updated_at", sa.BigInteger(), nullable=True),
|
||||
)
|
||||
|
||||
print("Migrating data from document table to knowledge table")
|
||||
# Representation of the existing 'document' table
|
||||
document_table = table(
|
||||
"document",
|
||||
column("collection_name", sa.String()),
|
||||
column("user_id", sa.String()),
|
||||
column("name", sa.String()),
|
||||
column("title", sa.Text()),
|
||||
column("content", sa.Text()),
|
||||
column("timestamp", sa.BigInteger()),
|
||||
)
|
||||
|
||||
# Select all from existing document table
|
||||
documents = op.get_bind().execute(
|
||||
select(
|
||||
document_table.c.collection_name,
|
||||
document_table.c.user_id,
|
||||
document_table.c.name,
|
||||
document_table.c.title,
|
||||
document_table.c.content,
|
||||
document_table.c.timestamp,
|
||||
)
|
||||
)
|
||||
|
||||
# Insert data into knowledge table from document table
|
||||
for doc in documents:
|
||||
op.get_bind().execute(
|
||||
knowledge_table.insert().values(
|
||||
id=doc.collection_name,
|
||||
user_id=doc.user_id,
|
||||
description=doc.name,
|
||||
meta={
|
||||
"legacy": True,
|
||||
"document": True,
|
||||
"tags": json.loads(doc.content or "{}").get("tags", []),
|
||||
},
|
||||
name=doc.title,
|
||||
created_at=doc.timestamp,
|
||||
updated_at=doc.timestamp, # using created_at for both created_at and updated_at in project
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table("knowledge")
|
||||
@@ -11,8 +11,8 @@ from typing import Sequence, Union
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
|
||||
import open_webui.apps.webui.internal.db
|
||||
from open_webui.apps.webui.internal.db import JSONField
|
||||
from open_webui.migrations.util import get_existing_tables
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
@@ -82,7 +82,7 @@ def upgrade() -> None:
|
||||
sa.Column("id", sa.String(), nullable=False),
|
||||
sa.Column("user_id", sa.String(), nullable=True),
|
||||
sa.Column("filename", sa.Text(), nullable=True),
|
||||
sa.Column("meta", apps.webui.internal.db.JSONField(), nullable=True),
|
||||
sa.Column("meta", JSONField(), nullable=True),
|
||||
sa.Column("created_at", sa.BigInteger(), nullable=True),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
@@ -95,8 +95,8 @@ def upgrade() -> None:
|
||||
sa.Column("name", sa.Text(), nullable=True),
|
||||
sa.Column("type", sa.Text(), nullable=True),
|
||||
sa.Column("content", sa.Text(), nullable=True),
|
||||
sa.Column("meta", apps.webui.internal.db.JSONField(), nullable=True),
|
||||
sa.Column("valves", apps.webui.internal.db.JSONField(), nullable=True),
|
||||
sa.Column("meta", JSONField(), nullable=True),
|
||||
sa.Column("valves", JSONField(), nullable=True),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=True),
|
||||
sa.Column("is_global", sa.Boolean(), nullable=True),
|
||||
sa.Column("updated_at", sa.BigInteger(), nullable=True),
|
||||
@@ -122,8 +122,8 @@ def upgrade() -> None:
|
||||
sa.Column("user_id", sa.Text(), nullable=True),
|
||||
sa.Column("base_model_id", sa.Text(), nullable=True),
|
||||
sa.Column("name", sa.Text(), nullable=True),
|
||||
sa.Column("params", apps.webui.internal.db.JSONField(), nullable=True),
|
||||
sa.Column("meta", apps.webui.internal.db.JSONField(), nullable=True),
|
||||
sa.Column("params", JSONField(), nullable=True),
|
||||
sa.Column("meta", JSONField(), nullable=True),
|
||||
sa.Column("updated_at", sa.BigInteger(), nullable=True),
|
||||
sa.Column("created_at", sa.BigInteger(), nullable=True),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
@@ -157,9 +157,9 @@ def upgrade() -> None:
|
||||
sa.Column("user_id", sa.String(), nullable=True),
|
||||
sa.Column("name", sa.Text(), nullable=True),
|
||||
sa.Column("content", sa.Text(), nullable=True),
|
||||
sa.Column("specs", apps.webui.internal.db.JSONField(), nullable=True),
|
||||
sa.Column("meta", apps.webui.internal.db.JSONField(), nullable=True),
|
||||
sa.Column("valves", apps.webui.internal.db.JSONField(), nullable=True),
|
||||
sa.Column("specs", JSONField(), nullable=True),
|
||||
sa.Column("meta", JSONField(), nullable=True),
|
||||
sa.Column("valves", JSONField(), nullable=True),
|
||||
sa.Column("updated_at", sa.BigInteger(), nullable=True),
|
||||
sa.Column("created_at", sa.BigInteger(), nullable=True),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
@@ -177,8 +177,8 @@ def upgrade() -> None:
|
||||
sa.Column("updated_at", sa.BigInteger(), nullable=True),
|
||||
sa.Column("created_at", sa.BigInteger(), nullable=True),
|
||||
sa.Column("api_key", sa.String(), nullable=True),
|
||||
sa.Column("settings", apps.webui.internal.db.JSONField(), nullable=True),
|
||||
sa.Column("info", apps.webui.internal.db.JSONField(), nullable=True),
|
||||
sa.Column("settings", JSONField(), nullable=True),
|
||||
sa.Column("info", JSONField(), nullable=True),
|
||||
sa.Column("oauth_sub", sa.Text(), nullable=True),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("api_key"),
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Update file table
|
||||
|
||||
Revision ID: c0fbf31ca0db
|
||||
Revises: ca81bd47c050
|
||||
Create Date: 2024-09-20 15:26:35.241684
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "c0fbf31ca0db"
|
||||
down_revision: Union[str, None] = "ca81bd47c050"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column("file", sa.Column("hash", sa.Text(), nullable=True))
|
||||
op.add_column("file", sa.Column("data", sa.JSON(), nullable=True))
|
||||
op.add_column("file", sa.Column("updated_at", sa.BigInteger(), nullable=True))
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column("file", "updated_at")
|
||||
op.drop_column("file", "data")
|
||||
op.drop_column("file", "hash")
|
||||
@@ -6,7 +6,14 @@ from datetime import timedelta
|
||||
from pathlib import Path
|
||||
from typing import Callable, Optional
|
||||
|
||||
from open_webui.utils.task import prompt_template
|
||||
|
||||
def get_messages_content(messages: list[dict]) -> str:
|
||||
return "\n".join(
|
||||
[
|
||||
f"{message['role'].upper()}: {get_content_from_message(message)}"
|
||||
for message in messages
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def get_last_user_message_item(messages: list[dict]) -> Optional[dict]:
|
||||
@@ -30,7 +37,6 @@ def get_last_user_message(messages: list[dict]) -> Optional[str]:
|
||||
message = get_last_user_message_item(messages)
|
||||
if message is None:
|
||||
return None
|
||||
|
||||
return get_content_from_message(message)
|
||||
|
||||
|
||||
@@ -99,103 +105,29 @@ def openai_chat_message_template(model: str):
|
||||
}
|
||||
|
||||
|
||||
def openai_chat_chunk_message_template(model: str, message: str) -> dict:
|
||||
def openai_chat_chunk_message_template(
|
||||
model: str, message: Optional[str] = None
|
||||
) -> dict:
|
||||
template = openai_chat_message_template(model)
|
||||
template["object"] = "chat.completion.chunk"
|
||||
template["choices"][0]["delta"] = {"content": message}
|
||||
if message:
|
||||
template["choices"][0]["delta"] = {"content": message}
|
||||
else:
|
||||
template["choices"][0]["finish_reason"] = "stop"
|
||||
return template
|
||||
|
||||
|
||||
def openai_chat_completion_message_template(model: str, message: str) -> dict:
|
||||
def openai_chat_completion_message_template(
|
||||
model: str, message: Optional[str] = None
|
||||
) -> dict:
|
||||
template = openai_chat_message_template(model)
|
||||
template["object"] = "chat.completion"
|
||||
template["choices"][0]["message"] = {"content": message, "role": "assistant"}
|
||||
if message is not None:
|
||||
template["choices"][0]["message"] = {"content": message, "role": "assistant"}
|
||||
template["choices"][0]["finish_reason"] = "stop"
|
||||
return template
|
||||
|
||||
|
||||
# inplace function: form_data is modified
|
||||
def apply_model_system_prompt_to_body(params: dict, form_data: dict, user) -> dict:
|
||||
system = params.get("system", None)
|
||||
if not system:
|
||||
return form_data
|
||||
|
||||
if user:
|
||||
template_params = {
|
||||
"user_name": user.name,
|
||||
"user_location": user.info.get("location") if user.info else None,
|
||||
}
|
||||
else:
|
||||
template_params = {}
|
||||
system = prompt_template(system, **template_params)
|
||||
form_data["messages"] = add_or_update_system_message(
|
||||
system, form_data.get("messages", [])
|
||||
)
|
||||
return form_data
|
||||
|
||||
|
||||
# inplace function: form_data is modified
|
||||
def apply_model_params_to_body(
|
||||
params: dict, form_data: dict, mappings: dict[str, Callable]
|
||||
) -> dict:
|
||||
if not params:
|
||||
return form_data
|
||||
|
||||
for key, cast_func in mappings.items():
|
||||
if (value := params.get(key)) is not None:
|
||||
form_data[key] = cast_func(value)
|
||||
|
||||
return form_data
|
||||
|
||||
|
||||
# inplace function: form_data is modified
|
||||
def apply_model_params_to_body_openai(params: dict, form_data: dict) -> dict:
|
||||
mappings = {
|
||||
"temperature": float,
|
||||
"top_p": int,
|
||||
"max_tokens": int,
|
||||
"frequency_penalty": int,
|
||||
"seed": lambda x: x,
|
||||
"stop": lambda x: [bytes(s, "utf-8").decode("unicode_escape") for s in x],
|
||||
}
|
||||
return apply_model_params_to_body(params, form_data, mappings)
|
||||
|
||||
|
||||
def apply_model_params_to_body_ollama(params: dict, form_data: dict) -> dict:
|
||||
opts = [
|
||||
"temperature",
|
||||
"top_p",
|
||||
"seed",
|
||||
"mirostat",
|
||||
"mirostat_eta",
|
||||
"mirostat_tau",
|
||||
"num_ctx",
|
||||
"num_batch",
|
||||
"num_keep",
|
||||
"repeat_last_n",
|
||||
"tfs_z",
|
||||
"top_k",
|
||||
"min_p",
|
||||
"use_mmap",
|
||||
"use_mlock",
|
||||
"num_thread",
|
||||
"num_gpu",
|
||||
]
|
||||
mappings = {i: lambda x: x for i in opts}
|
||||
form_data = apply_model_params_to_body(params, form_data, mappings)
|
||||
|
||||
name_differences = {
|
||||
"max_tokens": "num_predict",
|
||||
"frequency_penalty": "repeat_penalty",
|
||||
}
|
||||
|
||||
for key, value in name_differences.items():
|
||||
if (param := params.get(key, None)) is not None:
|
||||
form_data[value] = param
|
||||
|
||||
return form_data
|
||||
|
||||
|
||||
def get_gravatar_url(email):
|
||||
# Trim leading and trailing whitespace from
|
||||
# an email address and force all characters
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
from open_webui.utils.task import prompt_template
|
||||
from open_webui.utils.misc import (
|
||||
add_or_update_system_message,
|
||||
)
|
||||
|
||||
from typing import Callable, Optional
|
||||
|
||||
|
||||
# inplace function: form_data is modified
|
||||
def apply_model_system_prompt_to_body(params: dict, form_data: dict, user) -> dict:
|
||||
system = params.get("system", None)
|
||||
if not system:
|
||||
return form_data
|
||||
|
||||
if user:
|
||||
template_params = {
|
||||
"user_name": user.name,
|
||||
"user_location": user.info.get("location") if user.info else None,
|
||||
}
|
||||
else:
|
||||
template_params = {}
|
||||
system = prompt_template(system, **template_params)
|
||||
form_data["messages"] = add_or_update_system_message(
|
||||
system, form_data.get("messages", [])
|
||||
)
|
||||
return form_data
|
||||
|
||||
|
||||
# inplace function: form_data is modified
|
||||
def apply_model_params_to_body(
|
||||
params: dict, form_data: dict, mappings: dict[str, Callable]
|
||||
) -> dict:
|
||||
if not params:
|
||||
return form_data
|
||||
|
||||
for key, cast_func in mappings.items():
|
||||
if (value := params.get(key)) is not None:
|
||||
form_data[key] = cast_func(value)
|
||||
|
||||
return form_data
|
||||
|
||||
|
||||
# inplace function: form_data is modified
|
||||
def apply_model_params_to_body_openai(params: dict, form_data: dict) -> dict:
|
||||
mappings = {
|
||||
"temperature": float,
|
||||
"top_p": float,
|
||||
"max_tokens": int,
|
||||
"frequency_penalty": float,
|
||||
"seed": lambda x: x,
|
||||
"stop": lambda x: [bytes(s, "utf-8").decode("unicode_escape") for s in x],
|
||||
}
|
||||
return apply_model_params_to_body(params, form_data, mappings)
|
||||
|
||||
|
||||
def apply_model_params_to_body_ollama(params: dict, form_data: dict) -> dict:
|
||||
opts = [
|
||||
"temperature",
|
||||
"top_p",
|
||||
"seed",
|
||||
"mirostat",
|
||||
"mirostat_eta",
|
||||
"mirostat_tau",
|
||||
"num_ctx",
|
||||
"num_batch",
|
||||
"num_keep",
|
||||
"repeat_last_n",
|
||||
"tfs_z",
|
||||
"top_k",
|
||||
"min_p",
|
||||
"use_mmap",
|
||||
"use_mlock",
|
||||
"num_thread",
|
||||
"num_gpu",
|
||||
]
|
||||
mappings = {i: lambda x: x for i in opts}
|
||||
form_data = apply_model_params_to_body(params, form_data, mappings)
|
||||
|
||||
name_differences = {
|
||||
"max_tokens": "num_predict",
|
||||
"frequency_penalty": "repeat_penalty",
|
||||
}
|
||||
|
||||
for key, value in name_differences.items():
|
||||
if (param := params.get(key, None)) is not None:
|
||||
form_data[value] = param
|
||||
|
||||
return form_data
|
||||
|
||||
|
||||
def convert_payload_openai_to_ollama(openai_payload: dict) -> dict:
|
||||
"""
|
||||
Converts a payload formatted for OpenAI's API to be compatible with Ollama's API endpoint for chat completions.
|
||||
|
||||
Args:
|
||||
openai_payload (dict): The payload originally designed for OpenAI API usage.
|
||||
|
||||
Returns:
|
||||
dict: A modified payload compatible with the Ollama API.
|
||||
"""
|
||||
ollama_payload = {}
|
||||
|
||||
# Mapping basic model and message details
|
||||
ollama_payload["model"] = openai_payload.get("model")
|
||||
ollama_payload["messages"] = openai_payload.get("messages")
|
||||
ollama_payload["stream"] = openai_payload.get("stream", False)
|
||||
|
||||
# If there are advanced parameters in the payload, format them in Ollama's options field
|
||||
ollama_options = {}
|
||||
|
||||
# Handle parameters which map directly
|
||||
for param in ["temperature", "top_p", "seed"]:
|
||||
if param in openai_payload:
|
||||
ollama_options[param] = openai_payload[param]
|
||||
|
||||
# Mapping OpenAI's `max_tokens` -> Ollama's `num_predict`
|
||||
if "max_completion_tokens" in openai_payload:
|
||||
ollama_options["num_predict"] = openai_payload["max_completion_tokens"]
|
||||
elif "max_tokens" in openai_payload:
|
||||
ollama_options["num_predict"] = openai_payload["max_tokens"]
|
||||
|
||||
# Handle frequency / presence_penalty, which needs renaming and checking
|
||||
if "frequency_penalty" in openai_payload:
|
||||
ollama_options["repeat_penalty"] = openai_payload["frequency_penalty"]
|
||||
|
||||
if "presence_penalty" in openai_payload and "penalty" not in ollama_options:
|
||||
# We are assuming presence penalty uses a similar concept in Ollama, which needs custom handling if exists.
|
||||
ollama_options["new_topic_penalty"] = openai_payload["presence_penalty"]
|
||||
|
||||
# Add options to payload if any have been set
|
||||
if ollama_options:
|
||||
ollama_payload["options"] = ollama_options
|
||||
|
||||
return ollama_payload
|
||||
@@ -0,0 +1,32 @@
|
||||
import json
|
||||
from open_webui.utils.misc import (
|
||||
openai_chat_chunk_message_template,
|
||||
openai_chat_completion_message_template,
|
||||
)
|
||||
|
||||
|
||||
def convert_response_ollama_to_openai(ollama_response: dict) -> dict:
|
||||
model = ollama_response.get("model", "ollama")
|
||||
message_content = ollama_response.get("message", {}).get("content", "")
|
||||
|
||||
response = openai_chat_completion_message_template(model, message_content)
|
||||
return response
|
||||
|
||||
|
||||
async def convert_streaming_response_ollama_to_openai(ollama_streaming_response):
|
||||
async for data in ollama_streaming_response.body_iterator:
|
||||
data = json.loads(data)
|
||||
|
||||
model = data.get("model", "ollama")
|
||||
message_content = data.get("message", {}).get("content", "")
|
||||
done = data.get("done", False)
|
||||
|
||||
data = openai_chat_chunk_message_template(
|
||||
model, message_content if not done else None
|
||||
)
|
||||
|
||||
line = f"data: {json.dumps(data)}\n\n"
|
||||
if done:
|
||||
line += "data: [DONE]\n\n"
|
||||
|
||||
yield line
|
||||
@@ -104,5 +104,9 @@ def json_schema_to_pydantic_type(json_schema: dict[str, Any]) -> Any:
|
||||
return Optional[Any] # Use Optional[Any] for nullable fields
|
||||
elif type_ == "literal":
|
||||
return Literal[literal_eval(json_schema.get("enum"))]
|
||||
elif type_ == "optional":
|
||||
inner_schema = json_schema.get("items", {"type": "string"})
|
||||
inner_type = json_schema_to_pydantic_type(inner_schema)
|
||||
return Optional[inner_type]
|
||||
else:
|
||||
raise ValueError(f"Unsupported JSON schema type: {type_}")
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import re
|
||||
import os
|
||||
|
||||
from fastapi import Request
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from typing import Dict
|
||||
|
||||
|
||||
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
response = await call_next(request)
|
||||
response.headers.update(set_security_headers())
|
||||
return response
|
||||
|
||||
|
||||
def set_security_headers() -> Dict[str, str]:
|
||||
"""
|
||||
Sets security headers based on environment variables.
|
||||
|
||||
This function reads specific environment variables and uses their values
|
||||
to set corresponding security headers. The headers that can be set are:
|
||||
- cache-control
|
||||
- strict-transport-security
|
||||
- referrer-policy
|
||||
- x-content-type-options
|
||||
- x-download-options
|
||||
- x-frame-options
|
||||
- x-permitted-cross-domain-policies
|
||||
|
||||
Each environment variable is associated with a specific setter function
|
||||
that constructs the header. If the environment variable is set, the
|
||||
corresponding header is added to the options dictionary.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the security headers and their values.
|
||||
"""
|
||||
options = {}
|
||||
header_setters = {
|
||||
"CACHE_CONTROL": set_cache_control,
|
||||
"HSTS": set_hsts,
|
||||
"REFERRER_POLICY": set_referrer,
|
||||
"XCONTENT_TYPE": set_xcontent_type,
|
||||
"XDOWNLOAD_OPTIONS": set_xdownload_options,
|
||||
"XFRAME_OPTIONS": set_xframe,
|
||||
"XPERMITTED_CROSS_DOMAIN_POLICIES": set_xpermitted_cross_domain_policies,
|
||||
}
|
||||
|
||||
for env_var, setter in header_setters.items():
|
||||
value = os.environ.get(env_var, None)
|
||||
if value:
|
||||
header = setter(value)
|
||||
if header:
|
||||
options.update(header)
|
||||
|
||||
return options
|
||||
|
||||
|
||||
# Set HTTP Strict Transport Security(HSTS) response header
|
||||
def set_hsts(value: str):
|
||||
pattern = r"^max-age=(\d+)(;includeSubDomains)?(;preload)?$"
|
||||
match = re.match(pattern, value, re.IGNORECASE)
|
||||
if not match:
|
||||
return "max-age=31536000;includeSubDomains"
|
||||
return {"Strict-Transport-Security": value}
|
||||
|
||||
|
||||
# Set X-Frame-Options response header
|
||||
def set_xframe(value: str):
|
||||
pattern = r"^(DENY|SAMEORIGIN)$"
|
||||
match = re.match(pattern, value, re.IGNORECASE)
|
||||
if not match:
|
||||
value = "DENY"
|
||||
return {"X-Frame-Options": value}
|
||||
|
||||
|
||||
# Set Referrer-Policy response header
|
||||
def set_referrer(value: str):
|
||||
pattern = r"^(no-referrer|no-referrer-when-downgrade|origin|origin-when-cross-origin|same-origin|strict-origin|strict-origin-when-cross-origin|unsafe-url)$"
|
||||
match = re.match(pattern, value, re.IGNORECASE)
|
||||
if not match:
|
||||
value = "no-referrer"
|
||||
return {"Referrer-Policy": value}
|
||||
|
||||
|
||||
# Set Cache-Control response header
|
||||
def set_cache_control(value: str):
|
||||
pattern = r"^(public|private|no-cache|no-store|must-revalidate|proxy-revalidate|max-age=\d+|s-maxage=\d+|no-transform|immutable)(,\s*(public|private|no-cache|no-store|must-revalidate|proxy-revalidate|max-age=\d+|s-maxage=\d+|no-transform|immutable))*$"
|
||||
match = re.match(pattern, value, re.IGNORECASE)
|
||||
if not match:
|
||||
value = "no-store, max-age=0"
|
||||
|
||||
return {"Cache-Control": value}
|
||||
|
||||
|
||||
# Set X-Download-Options response header
|
||||
def set_xdownload_options(value: str):
|
||||
if value != "noopen":
|
||||
value = "noopen"
|
||||
return {"X-Download-Options": value}
|
||||
|
||||
|
||||
# Set X-Content-Type-Options response header
|
||||
def set_xcontent_type(value: str):
|
||||
if value != "nosniff":
|
||||
value = "nosniff"
|
||||
return {"X-Content-Type-Options": value}
|
||||
|
||||
|
||||
# Set X-Permitted-Cross-Domain-Policies response header
|
||||
def set_xpermitted_cross_domain_policies(value: str):
|
||||
pattern = r"^(none|master-only|by-content-type|by-ftp-filename)$"
|
||||
match = re.match(pattern, value, re.IGNORECASE)
|
||||
if not match:
|
||||
value = "none"
|
||||
return {"X-Permitted-Cross-Domain-Policies": value}
|
||||
@@ -4,6 +4,9 @@ from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
|
||||
from open_webui.utils.misc import get_last_user_message, get_messages_content
|
||||
|
||||
|
||||
def prompt_template(
|
||||
template: str, user_name: Optional[str] = None, user_location: Optional[str] = None
|
||||
) -> str:
|
||||
@@ -37,9 +40,7 @@ def prompt_template(
|
||||
return template
|
||||
|
||||
|
||||
def title_generation_template(
|
||||
template: str, prompt: str, user: Optional[dict] = None
|
||||
) -> str:
|
||||
def replace_prompt_variable(template: str, prompt: str) -> str:
|
||||
def replacement_function(match):
|
||||
full_match = match.group(0)
|
||||
start_length = match.group(1)
|
||||
@@ -66,7 +67,13 @@ def title_generation_template(
|
||||
replacement_function,
|
||||
template,
|
||||
)
|
||||
return template
|
||||
|
||||
|
||||
def title_generation_template(
|
||||
template: str, prompt: str, user: Optional[dict] = None
|
||||
) -> str:
|
||||
template = replace_prompt_variable(template, prompt)
|
||||
template = prompt_template(
|
||||
template,
|
||||
**(
|
||||
@@ -79,36 +86,50 @@ def title_generation_template(
|
||||
return template
|
||||
|
||||
|
||||
def search_query_generation_template(
|
||||
template: str, prompt: str, user: Optional[dict] = None
|
||||
) -> str:
|
||||
def replace_messages_variable(template: str, messages: list[str]) -> str:
|
||||
def replacement_function(match):
|
||||
full_match = match.group(0)
|
||||
start_length = match.group(1)
|
||||
end_length = match.group(2)
|
||||
middle_length = match.group(3)
|
||||
|
||||
if full_match == "{{prompt}}":
|
||||
return prompt
|
||||
# Process messages based on the number of messages required
|
||||
if full_match == "{{MESSAGES}}":
|
||||
return get_messages_content(messages)
|
||||
elif start_length is not None:
|
||||
return prompt[: int(start_length)]
|
||||
return get_messages_content(messages[: int(start_length)])
|
||||
elif end_length is not None:
|
||||
return prompt[-int(end_length) :]
|
||||
return get_messages_content(messages[-int(end_length) :])
|
||||
elif middle_length is not None:
|
||||
middle_length = int(middle_length)
|
||||
if len(prompt) <= middle_length:
|
||||
return prompt
|
||||
start = prompt[: math.ceil(middle_length / 2)]
|
||||
end = prompt[-math.floor(middle_length / 2) :]
|
||||
return f"{start}...{end}"
|
||||
mid = int(middle_length)
|
||||
|
||||
if len(messages) <= mid:
|
||||
return get_messages_content(messages)
|
||||
# Handle middle truncation: split to get start and end portions of the messages list
|
||||
half = mid // 2
|
||||
start_msgs = messages[:half]
|
||||
end_msgs = messages[-half:] if mid % 2 == 0 else messages[-(half + 1) :]
|
||||
formatted_start = get_messages_content(start_msgs)
|
||||
formatted_end = get_messages_content(end_msgs)
|
||||
return f"{formatted_start}\n{formatted_end}"
|
||||
return ""
|
||||
|
||||
template = re.sub(
|
||||
r"{{prompt}}|{{prompt:start:(\d+)}}|{{prompt:end:(\d+)}}|{{prompt:middletruncate:(\d+)}}",
|
||||
r"{{MESSAGES}}|{{MESSAGES:START:(\d+)}}|{{MESSAGES:END:(\d+)}}|{{MESSAGES:MIDDLETRUNCATE:(\d+)}}",
|
||||
replacement_function,
|
||||
template,
|
||||
)
|
||||
|
||||
return template
|
||||
|
||||
|
||||
def search_query_generation_template(
|
||||
template: str, messages: list[dict], user: Optional[dict] = None
|
||||
) -> str:
|
||||
prompt = get_last_user_message(messages)
|
||||
template = replace_prompt_variable(template, prompt)
|
||||
template = replace_messages_variable(template, messages)
|
||||
|
||||
template = prompt_template(
|
||||
template,
|
||||
**(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
fastapi==0.111.0
|
||||
uvicorn[standard]==0.30.6
|
||||
pydantic==2.8.2
|
||||
pydantic==2.9.2
|
||||
python-multipart==0.0.9
|
||||
|
||||
Flask==3.0.3
|
||||
@@ -11,7 +11,7 @@ python-jose==3.3.0
|
||||
passlib[bcrypt]==1.7.4
|
||||
|
||||
requests==2.32.3
|
||||
aiohttp==3.10.5
|
||||
aiohttp==3.10.8
|
||||
|
||||
sqlalchemy==2.0.32
|
||||
alembic==1.13.2
|
||||
@@ -36,11 +36,18 @@ tiktoken
|
||||
|
||||
langchain==0.2.15
|
||||
langchain-community==0.2.12
|
||||
langchain-chroma==0.1.2
|
||||
langchain-chroma==0.1.4
|
||||
|
||||
fake-useragent==1.5.1
|
||||
chromadb==0.5.5
|
||||
chromadb==0.5.9
|
||||
pymilvus==2.4.7
|
||||
|
||||
sentence-transformers==3.0.1
|
||||
colbert-ai==0.2.21
|
||||
einops==0.8.0
|
||||
|
||||
|
||||
ftfy==6.2.3
|
||||
pypdf==4.3.1
|
||||
docx2txt==0.8
|
||||
python-pptx==1.0.0
|
||||
@@ -48,7 +55,7 @@ unstructured==0.15.9
|
||||
nltk==3.9.1
|
||||
Markdown==3.7
|
||||
pypandoc==1.13
|
||||
pandas==2.2.2
|
||||
pandas==2.2.3
|
||||
openpyxl==3.1.5
|
||||
pyxlsb==1.0.10
|
||||
xlrd==2.0.1
|
||||
@@ -73,7 +80,7 @@ pytube==15.0.0
|
||||
|
||||
extract_msg
|
||||
pydub
|
||||
duckduckgo-search~=6.2.11
|
||||
duckduckgo-search~=6.2.13
|
||||
|
||||
## Tests
|
||||
docker~=7.1.0
|
||||
|
||||
@@ -30,4 +30,4 @@ IF "%WEBUI_SECRET_KEY%%WEBUI_JWT_SECRET_KEY%" == " " (
|
||||
|
||||
:: Execute uvicorn
|
||||
SET "WEBUI_SECRET_KEY=%WEBUI_SECRET_KEY%"
|
||||
uvicorn main:app --host "%HOST%" --port "%PORT%" --forwarded-allow-ips '*'
|
||||
uvicorn open_webui.main:app --host "%HOST%" --port "%PORT%" --forwarded-allow-ips '*'
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
resources:
|
||||
- open-webui.yaml
|
||||
- ollama-service.yaml
|
||||
- ollama-statefulset.yaml
|
||||
- webui-deployment.yaml
|
||||
- webui-service.yaml
|
||||
- webui-ingress.yaml
|
||||
- webui-pvc.yaml
|
||||
@@ -0,0 +1,8 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- ../base
|
||||
|
||||
patches:
|
||||
- path: ollama-statefulset-gpu.yaml
|
||||
@@ -1,13 +0,0 @@
|
||||
resources:
|
||||
- base/open-webui.yaml
|
||||
- base/ollama-service.yaml
|
||||
- base/ollama-statefulset.yaml
|
||||
- base/webui-deployment.yaml
|
||||
- base/webui-service.yaml
|
||||
- base/webui-ingress.yaml
|
||||
- base/webui-pvc.yaml
|
||||
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
patches:
|
||||
- path: patches/ollama-statefulset-gpu.yaml
|
||||
Generated
+645
-167
File diff suppressed because it is too large
Load Diff
+7
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "open-webui",
|
||||
"version": "0.3.17",
|
||||
"version": "0.3.32",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "npm run pyodide:fetch && vite dev --host",
|
||||
@@ -25,11 +25,10 @@
|
||||
"@sveltejs/kit": "^2.5.20",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"@types/bun": "latest",
|
||||
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
||||
"@typescript-eslint/parser": "^6.17.0",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"cypress": "^13.8.1",
|
||||
"cypress": "^13.15.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-cypress": "^3.4.0",
|
||||
@@ -51,9 +50,11 @@
|
||||
"dependencies": {
|
||||
"@codemirror/lang-javascript": "^6.2.2",
|
||||
"@codemirror/lang-python": "^6.1.6",
|
||||
"@codemirror/language-data": "^6.5.1",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@pyscript/core": "^0.4.32",
|
||||
"@sveltejs/adapter-node": "^2.0.0",
|
||||
"@xyflow/svelte": "^0.1.19",
|
||||
"async": "^3.2.5",
|
||||
"bits-ui": "^0.19.7",
|
||||
"codemirror": "^6.0.1",
|
||||
@@ -72,6 +73,8 @@
|
||||
"katex": "^0.16.9",
|
||||
"marked": "^9.1.0",
|
||||
"mermaid": "^10.9.1",
|
||||
"paneforge": "^0.0.6",
|
||||
"panzoom": "^9.4.3",
|
||||
"pyodide": "^0.26.1",
|
||||
"socket.io-client": "^4.2.0",
|
||||
"sortablejs": "^1.15.2",
|
||||
@@ -81,7 +84,7 @@
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.13.0 <=21.x.x",
|
||||
"node": ">=18.13.0 <=22.x.x",
|
||||
"npm": ">=6.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
+14
-7
@@ -8,7 +8,7 @@ license = { file = "LICENSE" }
|
||||
dependencies = [
|
||||
"fastapi==0.111.0",
|
||||
"uvicorn[standard]==0.30.6",
|
||||
"pydantic==2.8.2",
|
||||
"pydantic==2.9.2",
|
||||
"python-multipart==0.0.9",
|
||||
|
||||
"Flask==3.0.3",
|
||||
@@ -19,7 +19,7 @@ dependencies = [
|
||||
"passlib[bcrypt]==1.7.4",
|
||||
|
||||
"requests==2.32.3",
|
||||
"aiohttp==3.10.5",
|
||||
"aiohttp==3.10.8",
|
||||
|
||||
"sqlalchemy==2.0.32",
|
||||
"alembic==1.13.2",
|
||||
@@ -43,11 +43,18 @@ dependencies = [
|
||||
|
||||
"langchain==0.2.15",
|
||||
"langchain-community==0.2.12",
|
||||
"langchain-chroma==0.1.2",
|
||||
"langchain-chroma==0.1.4",
|
||||
|
||||
"fake-useragent==1.5.1",
|
||||
"chromadb==0.5.5",
|
||||
"chromadb==0.5.9",
|
||||
"pymilvus==2.4.7",
|
||||
|
||||
"sentence-transformers==3.0.1",
|
||||
"colbert-ai==0.2.21",
|
||||
"einops==0.8.0",
|
||||
|
||||
|
||||
"ftfy==6.2.3",
|
||||
"pypdf==4.3.1",
|
||||
"docx2txt==0.8",
|
||||
"python-pptx==1.0.0",
|
||||
@@ -55,7 +62,7 @@ dependencies = [
|
||||
"nltk==3.9.1",
|
||||
"Markdown==3.7",
|
||||
"pypandoc==1.13",
|
||||
"pandas==2.2.2",
|
||||
"pandas==2.2.3",
|
||||
"openpyxl==3.1.5",
|
||||
"pyxlsb==1.0.10",
|
||||
"xlrd==2.0.1",
|
||||
@@ -80,10 +87,10 @@ dependencies = [
|
||||
|
||||
"extract_msg",
|
||||
"pydub",
|
||||
"duckduckgo-search~=6.2.11",
|
||||
"duckduckgo-search~=6.2.13",
|
||||
|
||||
"docker~=7.1.0",
|
||||
"pytest~=8.2.2",
|
||||
"pytest~=8.3.2",
|
||||
"pytest-docker~=3.1.1"
|
||||
]
|
||||
readme = "README.md"
|
||||
|
||||
+23
-15
@@ -50,21 +50,6 @@ iframe {
|
||||
@apply rounded-lg;
|
||||
}
|
||||
|
||||
ol > li {
|
||||
counter-increment: list-number;
|
||||
display: block;
|
||||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.prose ol > li::before {
|
||||
content: counters(list-number, '.') '.';
|
||||
padding-right: 0.5rem;
|
||||
color: var(--tw-prose-counters);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
li p {
|
||||
display: inline;
|
||||
}
|
||||
@@ -77,6 +62,12 @@ li p {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
/* Dark theme scrollbar styles */
|
||||
.dark ::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(69, 69, 74, 0.8); /* Darker color for dark theme */
|
||||
border-color: rgba(0, 0, 0, var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
height: 0.4rem;
|
||||
width: 0.4rem;
|
||||
@@ -171,3 +162,20 @@ input[type='number'] {
|
||||
font-weight: 600;
|
||||
@apply rounded-md dark:bg-gray-800 bg-gray-100 mx-0.5;
|
||||
}
|
||||
|
||||
.svelte-flow {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.svelte-flow__edge > path {
|
||||
stroke-width: 0.5;
|
||||
}
|
||||
|
||||
.svelte-flow__edge.animated > path {
|
||||
stroke-width: 2;
|
||||
@apply stroke-gray-600 dark:stroke-gray-500;
|
||||
}
|
||||
|
||||
.bg-gray-950-90 {
|
||||
background-color: rgba(var(--color-gray-950, #0d0d0d), 0.9);
|
||||
}
|
||||
|
||||
+21
-14
@@ -3,8 +3,13 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<link rel="apple-touch-icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<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="viewport"
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover"
|
||||
/>
|
||||
<meta name="theme-color" content="#171717" />
|
||||
<meta name="robots" content="noindex,nofollow" />
|
||||
<meta name="description" content="Open WebUI" />
|
||||
<link
|
||||
@@ -23,33 +28,33 @@
|
||||
<script>
|
||||
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
|
||||
(() => {
|
||||
const metaThemeColorTag = document.querySelector('meta[name="theme-color"]');
|
||||
const prefersDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
if (!localStorage?.theme) {
|
||||
localStorage.theme = 'system';
|
||||
}
|
||||
|
||||
if (localStorage?.theme && localStorage?.theme.includes('oled')) {
|
||||
if (localStorage.theme === 'system') {
|
||||
document.documentElement.classList.add(prefersDarkTheme ? 'dark' : 'light');
|
||||
metaThemeColorTag.setAttribute('content', prefersDarkTheme ? '#171717' : '#ffffff');
|
||||
} else if (localStorage.theme === 'oled-dark') {
|
||||
document.documentElement.style.setProperty('--color-gray-800', '#101010');
|
||||
document.documentElement.style.setProperty('--color-gray-850', '#050505');
|
||||
document.documentElement.style.setProperty('--color-gray-900', '#000000');
|
||||
document.documentElement.style.setProperty('--color-gray-950', '#000000');
|
||||
document.documentElement.classList.add('dark');
|
||||
} else if (
|
||||
localStorage.theme === 'light' ||
|
||||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: light)').matches)
|
||||
) {
|
||||
metaThemeColorTag.setAttribute('content', '#000000');
|
||||
} else if (localStorage.theme === 'light') {
|
||||
document.documentElement.classList.add('light');
|
||||
} else if (localStorage.theme && localStorage.theme !== 'system') {
|
||||
localStorage.theme.split(' ').forEach((e) => {
|
||||
document.documentElement.classList.add(e);
|
||||
});
|
||||
} else if (localStorage.theme && localStorage.theme === 'system') {
|
||||
systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
document.documentElement.classList.add(systemTheme ? 'dark' : 'light');
|
||||
} else if (localStorage.theme && localStorage.theme === 'her') {
|
||||
metaThemeColorTag.setAttribute('content', '#ffffff');
|
||||
} else if (localStorage.theme === 'her') {
|
||||
document.documentElement.classList.add('dark');
|
||||
document.documentElement.classList.add('her');
|
||||
metaThemeColorTag.setAttribute('content', '#983724');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
metaThemeColorTag.setAttribute('content', '#171717');
|
||||
}
|
||||
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addListener((e) => {
|
||||
@@ -57,9 +62,11 @@
|
||||
if (e.matches) {
|
||||
document.documentElement.classList.add('dark');
|
||||
document.documentElement.classList.remove('light');
|
||||
metaThemeColorTag.setAttribute('content', '#171717');
|
||||
} else {
|
||||
document.documentElement.classList.add('light');
|
||||
document.documentElement.classList.remove('dark');
|
||||
metaThemeColorTag.setAttribute('content', '#ffffff');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -30,6 +30,32 @@ export const uploadFile = async (token: string, file: File) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const uploadDir = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/files/upload/dir`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getFiles = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
@@ -92,6 +118,40 @@ export const getFileById = async (token: string, id: string) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const updateFileDataContentById = async (token: string, id: string, content: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/files/${id}/data/content/update`, {
|
||||
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();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getFileContentById = async (id: string) => {
|
||||
let error = null;
|
||||
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
export const createNewKnowledge = async (token: string, name: string, description: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/create`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: name,
|
||||
description: description
|
||||
})
|
||||
})
|
||||
.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 getKnowledgeItems = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/`, {
|
||||
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();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getKnowledgeById = async (token: string, id: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/${id}`, {
|
||||
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();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
type KnowledgeUpdateForm = {
|
||||
name?: string;
|
||||
description?: string;
|
||||
data?: object;
|
||||
};
|
||||
|
||||
export const updateKnowledgeById = async (token: string, id: string, form: KnowledgeUpdateForm) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/${id}/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: form?.name ? form.name : undefined,
|
||||
description: form?.description ? form.description : undefined,
|
||||
data: form?.data ? form.data : undefined
|
||||
})
|
||||
})
|
||||
.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 addFileToKnowledgeById = async (token: string, id: string, fileId: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/${id}/file/add`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
file_id: fileId
|
||||
})
|
||||
})
|
||||
.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 updateFileFromKnowledgeById = async (token: string, id: string, fileId: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/${id}/file/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
file_id: fileId
|
||||
})
|
||||
})
|
||||
.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 removeFileFromKnowledgeById = async (token: string, id: string, fileId: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/${id}/file/remove`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
file_id: fileId
|
||||
})
|
||||
})
|
||||
.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 resetKnowledgeById = async (token: string, id: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/${id}/reset`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const deleteKnowledgeById = async (token: string, id: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/${id}/delete`, {
|
||||
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;
|
||||
};
|
||||
@@ -1,9 +1,9 @@
|
||||
import { RAG_API_BASE_URL } from '$lib/constants';
|
||||
import { RETRIEVAL_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
export const getRAGConfig = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${RAG_API_BASE_URL}/config`, {
|
||||
const res = await fetch(`${RETRIEVAL_API_BASE_URL}/config`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -53,7 +53,7 @@ type RAGConfigForm = {
|
||||
export const updateRAGConfig = async (token: string, payload: RAGConfigForm) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${RAG_API_BASE_URL}/config/update`, {
|
||||
const res = await fetch(`${RETRIEVAL_API_BASE_URL}/config/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -83,7 +83,7 @@ export const updateRAGConfig = async (token: string, payload: RAGConfigForm) =>
|
||||
export const getRAGTemplate = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${RAG_API_BASE_URL}/template`, {
|
||||
const res = await fetch(`${RETRIEVAL_API_BASE_URL}/template`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -110,7 +110,7 @@ export const getRAGTemplate = async (token: string) => {
|
||||
export const getQuerySettings = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${RAG_API_BASE_URL}/query/settings`, {
|
||||
const res = await fetch(`${RETRIEVAL_API_BASE_URL}/query/settings`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -143,7 +143,7 @@ type QuerySettings = {
|
||||
export const updateQuerySettings = async (token: string, settings: QuerySettings) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${RAG_API_BASE_URL}/query/settings/update`, {
|
||||
const res = await fetch(`${RETRIEVAL_API_BASE_URL}/query/settings/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -170,288 +170,10 @@ export const updateQuerySettings = async (token: string, settings: QuerySettings
|
||||
return res;
|
||||
};
|
||||
|
||||
export const processDocToVectorDB = async (token: string, file_id: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${RAG_API_BASE_URL}/process/doc`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
file_id: file_id
|
||||
})
|
||||
})
|
||||
.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 uploadDocToVectorDB = async (token: string, collection_name: string, file: File) => {
|
||||
const data = new FormData();
|
||||
data.append('file', file);
|
||||
data.append('collection_name', collection_name);
|
||||
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${RAG_API_BASE_URL}/doc`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: data
|
||||
})
|
||||
.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 uploadWebToVectorDB = async (token: string, collection_name: string, url: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${RAG_API_BASE_URL}/web`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: url,
|
||||
collection_name: collection_name
|
||||
})
|
||||
})
|
||||
.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 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,
|
||||
query: string,
|
||||
k: number | null = null
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${RAG_API_BASE_URL}/query/doc`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
collection_name: collection_name,
|
||||
query: query,
|
||||
k: k
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const queryCollection = async (
|
||||
token: string,
|
||||
collection_names: string,
|
||||
query: string,
|
||||
k: number | null = null
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${RAG_API_BASE_URL}/query/collection`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
collection_names: collection_names,
|
||||
query: query,
|
||||
k: k
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const scanDocs = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${RAG_API_BASE_URL}/scan`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const resetUploadDir = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${RAG_API_BASE_URL}/reset/uploads`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const resetVectorDB = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${RAG_API_BASE_URL}/reset/db`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getEmbeddingConfig = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${RAG_API_BASE_URL}/embedding`, {
|
||||
const res = await fetch(`${RETRIEVAL_API_BASE_URL}/embedding`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -490,7 +212,7 @@ type EmbeddingModelUpdateForm = {
|
||||
export const updateEmbeddingConfig = async (token: string, payload: EmbeddingModelUpdateForm) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${RAG_API_BASE_URL}/embedding/update`, {
|
||||
const res = await fetch(`${RETRIEVAL_API_BASE_URL}/embedding/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -520,7 +242,7 @@ export const updateEmbeddingConfig = async (token: string, payload: EmbeddingMod
|
||||
export const getRerankingConfig = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${RAG_API_BASE_URL}/reranking`, {
|
||||
const res = await fetch(`${RETRIEVAL_API_BASE_URL}/reranking`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -551,7 +273,7 @@ type RerankingModelUpdateForm = {
|
||||
export const updateRerankingConfig = async (token: string, payload: RerankingModelUpdateForm) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${RAG_API_BASE_URL}/reranking/update`, {
|
||||
const res = await fetch(`${RETRIEVAL_API_BASE_URL}/reranking/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -578,14 +300,119 @@ export const updateRerankingConfig = async (token: string, payload: RerankingMod
|
||||
return res;
|
||||
};
|
||||
|
||||
export const runWebSearch = async (
|
||||
export interface SearchDocument {
|
||||
status: boolean;
|
||||
collection_name: string;
|
||||
filenames: string[];
|
||||
}
|
||||
|
||||
export const processFile = async (
|
||||
token: string,
|
||||
file_id: string,
|
||||
collection_name: string | null = null
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${RETRIEVAL_API_BASE_URL}/process/file`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
file_id: file_id,
|
||||
collection_name: collection_name ? collection_name : undefined
|
||||
})
|
||||
})
|
||||
.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 processYoutubeVideo = async (token: string, url: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${RETRIEVAL_API_BASE_URL}/process/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 processWeb = async (token: string, collection_name: string, url: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${RETRIEVAL_API_BASE_URL}/process/web`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: url,
|
||||
collection_name: collection_name
|
||||
})
|
||||
})
|
||||
.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 processWebSearch = async (
|
||||
token: string,
|
||||
query: string,
|
||||
collection_name?: string
|
||||
): Promise<SearchDocument | null> => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${RAG_API_BASE_URL}/web/search`, {
|
||||
const res = await fetch(`${RETRIEVAL_API_BASE_URL}/process/web/search`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -613,8 +440,128 @@ export const runWebSearch = async (
|
||||
return res;
|
||||
};
|
||||
|
||||
export interface SearchDocument {
|
||||
status: boolean;
|
||||
collection_name: string;
|
||||
filenames: string[];
|
||||
}
|
||||
export const queryDoc = async (
|
||||
token: string,
|
||||
collection_name: string,
|
||||
query: string,
|
||||
k: number | null = null
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${RETRIEVAL_API_BASE_URL}/query/doc`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
collection_name: collection_name,
|
||||
query: query,
|
||||
k: k
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const queryCollection = async (
|
||||
token: string,
|
||||
collection_names: string,
|
||||
query: string,
|
||||
k: number | null = null
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${RETRIEVAL_API_BASE_URL}/query/collection`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
collection_names: collection_names,
|
||||
query: query,
|
||||
k: k
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const resetUploadDir = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${RETRIEVAL_API_BASE_URL}/reset/uploads`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const resetVectorDB = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${RETRIEVAL_API_BASE_URL}/reset/db`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
@@ -18,6 +18,8 @@ type ResponseUsage = {
|
||||
completion_tokens: number;
|
||||
/** Sum of the above two fields */
|
||||
total_tokens: number;
|
||||
/** Any other fields that aren't part of the base OpenAI spec */
|
||||
[other: string]: unknown;
|
||||
};
|
||||
|
||||
// createOpenAITextStream takes a responseBody with a SSE response,
|
||||
|
||||
@@ -55,7 +55,7 @@ export const formatPythonCode = async (code: string) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const downloadChatAsPDF = async (chat: object) => {
|
||||
export const downloadChatAsPDF = async (title: string, messages: object[]) => {
|
||||
let error = null;
|
||||
|
||||
const blob = await fetch(`${WEBUI_API_BASE_URL}/utils/pdf`, {
|
||||
@@ -64,8 +64,8 @@ export const downloadChatAsPDF = async (chat: object) => {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: chat.title,
|
||||
messages: chat.messages
|
||||
title: title,
|
||||
messages: messages
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
<script>
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
export let title = '';
|
||||
const i18n = getContext('i18n');
|
||||
</script>
|
||||
|
||||
<div class=" text-center text-6xl mb-3">📄</div>
|
||||
<div class="text-center dark:text-white text-2xl font-semibold z-50">{$i18n.t('Add Files')}</div>
|
||||
<div class="text-center dark:text-white text-2xl font-semibold z-50">
|
||||
{#if title}
|
||||
{title}
|
||||
{:else}
|
||||
{$i18n.t('Add Files')}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<slot
|
||||
><div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
let TTS_MODEL = '';
|
||||
let TTS_VOICE = '';
|
||||
let TTS_SPLIT_ON: TTS_RESPONSE_SPLIT = TTS_RESPONSE_SPLIT.PUNCTUATION;
|
||||
let TTS_AZURE_SPEECH_REGION = '';
|
||||
let TTS_AZURE_SPEECH_OUTPUT_FORMAT = '';
|
||||
|
||||
let STT_OPENAI_API_BASE_URL = '';
|
||||
let STT_OPENAI_API_KEY = '';
|
||||
@@ -64,6 +66,7 @@
|
||||
// do your loop
|
||||
if (voices.length > 0) {
|
||||
clearInterval(getVoicesLoop);
|
||||
voices.sort((a, b) => a.name.localeCompare(b.name, $i18n.resolvedLanguage));
|
||||
}
|
||||
}, 100);
|
||||
} else {
|
||||
@@ -74,6 +77,7 @@
|
||||
if (res) {
|
||||
console.log(res);
|
||||
voices = res.voices;
|
||||
voices.sort((a, b) => a.name.localeCompare(b.name, $i18n.resolvedLanguage));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -87,7 +91,9 @@
|
||||
ENGINE: TTS_ENGINE,
|
||||
MODEL: TTS_MODEL,
|
||||
VOICE: TTS_VOICE,
|
||||
SPLIT_ON: TTS_SPLIT_ON
|
||||
SPLIT_ON: TTS_SPLIT_ON,
|
||||
AZURE_SPEECH_REGION: TTS_AZURE_SPEECH_REGION,
|
||||
AZURE_SPEECH_OUTPUT_FORMAT: TTS_AZURE_SPEECH_OUTPUT_FORMAT
|
||||
},
|
||||
stt: {
|
||||
OPENAI_API_BASE_URL: STT_OPENAI_API_BASE_URL,
|
||||
@@ -120,6 +126,9 @@
|
||||
|
||||
TTS_SPLIT_ON = res.tts.SPLIT_ON || TTS_RESPONSE_SPLIT.PUNCTUATION;
|
||||
|
||||
TTS_AZURE_SPEECH_OUTPUT_FORMAT = res.tts.AZURE_SPEECH_OUTPUT_FORMAT;
|
||||
TTS_AZURE_SPEECH_REGION = res.tts.AZURE_SPEECH_REGION;
|
||||
|
||||
STT_OPENAI_API_BASE_URL = res.stt.OPENAI_API_BASE_URL;
|
||||
STT_OPENAI_API_KEY = res.stt.OPENAI_API_KEY;
|
||||
|
||||
@@ -224,6 +233,7 @@
|
||||
<option value="">{$i18n.t('Web API')}</option>
|
||||
<option value="openai">{$i18n.t('OpenAI')}</option>
|
||||
<option value="elevenlabs">{$i18n.t('ElevenLabs')}</option>
|
||||
<option value="azure">{$i18n.t('Azure AI Speech')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -252,6 +262,23 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else if TTS_ENGINE === 'azure'}
|
||||
<div>
|
||||
<div class="mt-1 flex gap-2 mb-1">
|
||||
<input
|
||||
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('API Key')}
|
||||
bind:value={TTS_API_KEY}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('Azure Region')}
|
||||
bind:value={TTS_AZURE_SPEECH_REGION}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<hr class=" dark:border-gray-850 my-2" />
|
||||
@@ -270,7 +297,8 @@
|
||||
<option
|
||||
value={voice.voiceURI}
|
||||
class="bg-gray-100 dark:bg-gray-700"
|
||||
selected={TTS_VOICE === voice.voiceURI}>{voice.name}</option
|
||||
selected={TTS_VOICE === voice.voiceURI}
|
||||
>{voice.name.replace('+', ', ')}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
@@ -359,6 +387,49 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if TTS_ENGINE === 'azure'}
|
||||
<div class=" flex gap-2">
|
||||
<div class="w-full">
|
||||
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Voice')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
list="voice-list"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={TTS_VOICE}
|
||||
placeholder="Select a voice"
|
||||
/>
|
||||
|
||||
<datalist id="voice-list">
|
||||
{#each voices as voice}
|
||||
<option value={voice.id}>{voice.name}</option>
|
||||
{/each}
|
||||
</datalist>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class=" mb-1.5 text-sm font-medium">
|
||||
{$i18n.t('Output format')}
|
||||
<a
|
||||
href="https://learn.microsoft.com/en-us/azure/ai-services/speech-service/rest-text-to-speech?tabs=streaming#audio-outputs"
|
||||
target="_blank"
|
||||
>
|
||||
<small>{$i18n.t('Available list')}</small>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
list="tts-model-list"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={TTS_AZURE_SPEECH_OUTPUT_FORMAT}
|
||||
placeholder="Select a output format"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<hr class="dark:border-gray-850 my-2" />
|
||||
|
||||
@@ -150,18 +150,20 @@
|
||||
})()
|
||||
]);
|
||||
|
||||
OPENAI_API_BASE_URLS.forEach(async (url, idx) => {
|
||||
const res = await getOpenAIModels(localStorage.token, idx);
|
||||
if (res.pipelines) {
|
||||
pipelineUrls[url] = true;
|
||||
}
|
||||
});
|
||||
|
||||
const ollamaConfig = await getOllamaConfig(localStorage.token);
|
||||
const openaiConfig = await getOpenAIConfig(localStorage.token);
|
||||
|
||||
ENABLE_OPENAI_API = openaiConfig.ENABLE_OPENAI_API;
|
||||
ENABLE_OLLAMA_API = ollamaConfig.ENABLE_OLLAMA_API;
|
||||
|
||||
if (ENABLE_OPENAI_API) {
|
||||
OPENAI_API_BASE_URLS.forEach(async (url, idx) => {
|
||||
const res = await getOpenAIModels(localStorage.token, idx);
|
||||
if (res.pipelines) {
|
||||
pipelineUrls[url] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import { onMount, getContext, createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import { getDocs } from '$lib/apis/documents';
|
||||
import { deleteAllFiles, deleteFileById } from '$lib/apis/files';
|
||||
import {
|
||||
getQuerySettings,
|
||||
scanDocs,
|
||||
updateQuerySettings,
|
||||
resetVectorDB,
|
||||
getEmbeddingConfig,
|
||||
@@ -17,12 +16,14 @@
|
||||
resetUploadDir,
|
||||
getRAGConfig,
|
||||
updateRAGConfig
|
||||
} from '$lib/apis/rag';
|
||||
} from '$lib/apis/retrieval';
|
||||
|
||||
import { knowledge, models } from '$lib/stores';
|
||||
import { getKnowledgeItems } from '$lib/apis/knowledge';
|
||||
import { uploadDir, deleteAllFiles, deleteFileById } from '$lib/apis/files';
|
||||
|
||||
import ResetUploadDirConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
import ResetVectorDBConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
|
||||
import { documents, models } from '$lib/stores';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
||||
@@ -61,17 +62,6 @@
|
||||
hybrid: false
|
||||
};
|
||||
|
||||
const scanHandler = async () => {
|
||||
scanDirLoading = true;
|
||||
const res = await scanDocs(localStorage.token);
|
||||
scanDirLoading = false;
|
||||
|
||||
if (res) {
|
||||
await documents.set(await getDocs(localStorage.token));
|
||||
toast.success($i18n.t('Scan complete!'));
|
||||
}
|
||||
};
|
||||
|
||||
const embeddingModelUpdateHandler = async () => {
|
||||
if (embeddingEngine === '' && embeddingModel.split('/').length - 1 > 1) {
|
||||
toast.error(
|
||||
@@ -284,58 +274,6 @@
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<div class=" mb-0.5 text-sm font-medium">{$i18n.t('General Settings')}</div>
|
||||
|
||||
<div class=" flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Scan for documents from {{path}}', { path: 'DOCS_DIR (/data/docs)' })}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class=" self-center text-xs p-1 px-3 bg-gray-50 dark:bg-gray-800 dark:hover:bg-gray-700 rounded-lg flex flex-row space-x-1 items-center {scanDirLoading
|
||||
? ' cursor-not-allowed'
|
||||
: ''}"
|
||||
on:click={() => {
|
||||
scanHandler();
|
||||
console.log('check');
|
||||
}}
|
||||
type="button"
|
||||
disabled={scanDirLoading}
|
||||
>
|
||||
<div class="self-center font-medium">{$i18n.t('Scan')}</div>
|
||||
|
||||
{#if scanDirLoading}
|
||||
<div class="ml-3 self-center">
|
||||
<svg
|
||||
class=" w-3 h-3"
|
||||
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>
|
||||
|
||||
<div class=" flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Embedding Model Engine')}</div>
|
||||
<div class="flex items-center relative">
|
||||
@@ -732,11 +670,17 @@
|
||||
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('RAG Template')}</div>
|
||||
<textarea
|
||||
bind:value={querySettings.template}
|
||||
class="w-full rounded-lg px-4 py-3 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||
rows="4"
|
||||
/>
|
||||
<Tooltip
|
||||
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
||||
placement="top-start"
|
||||
>
|
||||
<textarea
|
||||
bind:value={querySettings.template}
|
||||
placeholder={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
||||
class="w-full rounded-lg px-4 py-3 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||
rows="4"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -27,6 +27,43 @@
|
||||
|
||||
let models = null;
|
||||
|
||||
let samplers = [
|
||||
'DPM++ 2M',
|
||||
'DPM++ SDE',
|
||||
'DPM++ 2M SDE',
|
||||
'DPM++ 2M SDE Heun',
|
||||
'DPM++ 2S a',
|
||||
'DPM++ 3M SDE',
|
||||
'Euler a',
|
||||
'Euler',
|
||||
'LMS',
|
||||
'Heun',
|
||||
'DPM2',
|
||||
'DPM2 a',
|
||||
'DPM fast',
|
||||
'DPM adaptive',
|
||||
'Restart',
|
||||
'DDIM',
|
||||
'DDIM CFG++',
|
||||
'PLMS',
|
||||
'UniPC'
|
||||
];
|
||||
|
||||
let schedulers = [
|
||||
'Automatic',
|
||||
'Uniform',
|
||||
'Karras',
|
||||
'Exponential',
|
||||
'Polyexponential',
|
||||
'SGM Uniform',
|
||||
'KL Optimal',
|
||||
'Align Your Steps',
|
||||
'Simple',
|
||||
'Normal',
|
||||
'DDIM',
|
||||
'Beta'
|
||||
];
|
||||
|
||||
let requiredWorkflowNodes = [
|
||||
{
|
||||
type: 'prompt',
|
||||
@@ -326,6 +363,66 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!---Sampler-->
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Sampler')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<Tooltip content={$i18n.t('Enter Sampler (e.g. Euler a)')} placement="top-start">
|
||||
<input
|
||||
list="sampler-list"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('Enter Sampler (e.g. Euler a)')}
|
||||
bind:value={config.automatic1111.AUTOMATIC1111_SAMPLER}
|
||||
/>
|
||||
|
||||
<datalist id="sampler-list">
|
||||
{#each samplers ?? [] as sampler}
|
||||
<option value={sampler}>{sampler}</option>
|
||||
{/each}
|
||||
</datalist>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!---Scheduler-->
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Scheduler')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<Tooltip content={$i18n.t('Enter Scheduler (e.g. Karras)')} placement="top-start">
|
||||
<input
|
||||
list="scheduler-list"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('Enter Scheduler (e.g. Karras)')}
|
||||
bind:value={config.automatic1111.AUTOMATIC1111_SCHEDULER}
|
||||
/>
|
||||
|
||||
<datalist id="scheduler-list">
|
||||
{#each schedulers ?? [] as scheduler}
|
||||
<option value={scheduler}>{scheduler}</option>
|
||||
{/each}
|
||||
</datalist>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!---CFG scale-->
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set CFG Scale')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<Tooltip content={$i18n.t('Enter CFG Scale (e.g. 7.0)')} placement="top-start">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('Enter CFG Scale (e.g. 7.0)')}
|
||||
bind:value={config.automatic1111.AUTOMATIC1111_CFG_SCALE}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if config?.engine === 'comfyui'}
|
||||
<div class="">
|
||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('ComfyUI Base URL')}</div>
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
TASK_MODEL: '',
|
||||
TASK_MODEL_EXTERNAL: '',
|
||||
TITLE_GENERATION_PROMPT_TEMPLATE: '',
|
||||
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE: '',
|
||||
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD: 0
|
||||
ENABLE_SEARCH_QUERY: true,
|
||||
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE: ''
|
||||
};
|
||||
|
||||
let promptSuggestions = [];
|
||||
@@ -43,7 +43,6 @@
|
||||
taskConfig = await getTaskConfig(localStorage.token);
|
||||
|
||||
promptSuggestions = $config?.default_prompt_suggestions;
|
||||
|
||||
banners = await getBanners(localStorage.token);
|
||||
});
|
||||
|
||||
@@ -119,33 +118,50 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Title Generation Prompt')}</div>
|
||||
<textarea
|
||||
bind:value={taskConfig.TITLE_GENERATION_PROMPT_TEMPLATE}
|
||||
class="w-full rounded-lg py-3 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||
rows="6"
|
||||
/>
|
||||
<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Title Generation Prompt')}</div>
|
||||
|
||||
<Tooltip
|
||||
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
||||
placement="top-start"
|
||||
>
|
||||
<textarea
|
||||
bind:value={taskConfig.TITLE_GENERATION_PROMPT_TEMPLATE}
|
||||
class="w-full rounded-lg py-3 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||
rows="3"
|
||||
placeholder={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Search Query Generation Prompt')}</div>
|
||||
<textarea
|
||||
bind:value={taskConfig.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE}
|
||||
class="w-full rounded-lg py-3 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||
rows="6"
|
||||
/>
|
||||
</div>
|
||||
<hr class=" dark:border-gray-850 my-3" />
|
||||
|
||||
<div class="mt-3">
|
||||
<div class=" mb-2.5 text-sm font-medium">
|
||||
{$i18n.t('Search Query Generation Prompt Length Threshold')}
|
||||
<div class="my-3 flex w-full items-center justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Enable Web Search Query Generation')}
|
||||
</div>
|
||||
<input
|
||||
bind:value={taskConfig.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD}
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||
type="number"
|
||||
/>
|
||||
|
||||
<Switch bind:state={taskConfig.ENABLE_SEARCH_QUERY} />
|
||||
</div>
|
||||
|
||||
{#if taskConfig.ENABLE_SEARCH_QUERY}
|
||||
<div class="">
|
||||
<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Search Query Generation Prompt')}</div>
|
||||
|
||||
<Tooltip
|
||||
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
||||
placement="top-start"
|
||||
>
|
||||
<textarea
|
||||
bind:value={taskConfig.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE}
|
||||
class="w-full rounded-lg py-3 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||
rows="3"
|
||||
placeholder={$i18n.t(
|
||||
'Leave empty to use the default prompt, or enter a custom prompt'
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-850 my-3" />
|
||||
@@ -291,9 +307,10 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<input
|
||||
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800"
|
||||
<textarea
|
||||
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800 resize-none"
|
||||
placeholder={$i18n.t('Prompt (e.g. Tell me a fun fact about the Roman Empire)')}
|
||||
rows="3"
|
||||
bind:value={prompt.content}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { getRAGConfig, updateRAGConfig } from '$lib/apis/rag';
|
||||
import { getRAGConfig, updateRAGConfig } from '$lib/apis/retrieval';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
|
||||
import { documents, models } from '$lib/stores';
|
||||
import { models } from '$lib/stores';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { onMount, getContext, createEventDispatcher } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import { chatId, showArtifacts, showControls } from '$lib/stores';
|
||||
import XMark from '../icons/XMark.svelte';
|
||||
import { copyToClipboard, createMessagesList } from '$lib/utils';
|
||||
import ArrowsPointingOut from '../icons/ArrowsPointingOut.svelte';
|
||||
import Tooltip from '../common/Tooltip.svelte';
|
||||
import SvgPanZoom from '../common/SVGPanZoom.svelte';
|
||||
|
||||
export let overlay = false;
|
||||
export let history;
|
||||
let messages = [];
|
||||
|
||||
let contents: Array<{ type: string; content: string }> = [];
|
||||
let selectedContentIdx = 0;
|
||||
|
||||
let copied = false;
|
||||
let iframeElement: HTMLIFrameElement;
|
||||
|
||||
$: if (history) {
|
||||
messages = createMessagesList(history, history.currentId);
|
||||
getContents();
|
||||
} else {
|
||||
messages = [];
|
||||
getContents();
|
||||
}
|
||||
|
||||
const getContents = () => {
|
||||
contents = [];
|
||||
messages.forEach((message) => {
|
||||
if (message?.role !== 'user' && message?.content) {
|
||||
const codeBlockContents = message.content.match(/```[\s\S]*?```/g);
|
||||
let codeBlocks = [];
|
||||
|
||||
if (codeBlockContents) {
|
||||
codeBlockContents.forEach((block) => {
|
||||
const lang = block.split('\n')[0].replace('```', '').trim().toLowerCase();
|
||||
const code = block.replace(/```[\s\S]*?\n/, '').replace(/```$/, '');
|
||||
codeBlocks.push({ lang, code });
|
||||
});
|
||||
}
|
||||
|
||||
let htmlContent = '';
|
||||
let cssContent = '';
|
||||
let jsContent = '';
|
||||
|
||||
codeBlocks.forEach((block) => {
|
||||
const { lang, code } = block;
|
||||
|
||||
if (lang === 'html') {
|
||||
htmlContent += code + '\n';
|
||||
} else if (lang === 'css') {
|
||||
cssContent += code + '\n';
|
||||
} else if (lang === 'javascript' || lang === 'js') {
|
||||
jsContent += code + '\n';
|
||||
}
|
||||
});
|
||||
|
||||
const inlineHtml = message.content.match(/<html>[\s\S]*?<\/html>/gi);
|
||||
const inlineCss = message.content.match(/<style>[\s\S]*?<\/style>/gi);
|
||||
const inlineJs = message.content.match(/<script>[\s\S]*?<\/script>/gi);
|
||||
|
||||
if (inlineHtml) {
|
||||
inlineHtml.forEach((block) => {
|
||||
const content = block.replace(/<\/?html>/gi, ''); // Remove <html> tags
|
||||
htmlContent += content + '\n';
|
||||
});
|
||||
}
|
||||
if (inlineCss) {
|
||||
inlineCss.forEach((block) => {
|
||||
const content = block.replace(/<\/?style>/gi, ''); // Remove <style> tags
|
||||
cssContent += content + '\n';
|
||||
});
|
||||
}
|
||||
if (inlineJs) {
|
||||
inlineJs.forEach((block) => {
|
||||
const content = block.replace(/<\/?script>/gi, ''); // Remove <script> tags
|
||||
jsContent += content + '\n';
|
||||
});
|
||||
}
|
||||
|
||||
if (htmlContent || cssContent || jsContent) {
|
||||
const renderedContent = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<${''}style>
|
||||
body {
|
||||
background-color: white; /* Ensure the iframe has a white background */
|
||||
}
|
||||
|
||||
${cssContent}
|
||||
</${''}style>
|
||||
</head>
|
||||
<body>
|
||||
${htmlContent}
|
||||
|
||||
<${''}script>
|
||||
${jsContent}
|
||||
</${''}script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
contents = [...contents, { type: 'iframe', content: renderedContent }];
|
||||
} else {
|
||||
// Check for SVG content
|
||||
for (const block of codeBlocks) {
|
||||
if (block.lang === 'svg' || (block.lang === 'xml' && block.code.includes('<svg'))) {
|
||||
contents = [...contents, { type: 'svg', content: block.code }];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
selectedContentIdx = contents ? contents.length - 1 : 0;
|
||||
};
|
||||
|
||||
function navigateContent(direction: 'prev' | 'next') {
|
||||
console.log(selectedContentIdx);
|
||||
|
||||
selectedContentIdx =
|
||||
direction === 'prev'
|
||||
? Math.max(selectedContentIdx - 1, 0)
|
||||
: Math.min(selectedContentIdx + 1, contents.length - 1);
|
||||
|
||||
console.log(selectedContentIdx);
|
||||
}
|
||||
|
||||
const iframeLoadHandler = () => {
|
||||
iframeElement.contentWindow.addEventListener(
|
||||
'click',
|
||||
function (e) {
|
||||
const target = e.target.closest('a');
|
||||
if (target && target.href) {
|
||||
e.preventDefault();
|
||||
const url = new URL(target.href, iframeElement.baseURI);
|
||||
if (url.origin === window.location.origin) {
|
||||
iframeElement.contentWindow.history.pushState(
|
||||
null,
|
||||
'',
|
||||
url.pathname + url.search + url.hash
|
||||
);
|
||||
} else {
|
||||
console.log('External navigation blocked:', url.href);
|
||||
}
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
// Cancel drag when hovering over iframe
|
||||
iframeElement.contentWindow.addEventListener('mouseenter', function (e) {
|
||||
e.preventDefault();
|
||||
iframeElement.contentWindow.addEventListener('dragstart', (event) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const showFullScreen = () => {
|
||||
if (iframeElement.requestFullscreen) {
|
||||
iframeElement.requestFullscreen();
|
||||
} else if (iframeElement.webkitRequestFullscreen) {
|
||||
iframeElement.webkitRequestFullscreen();
|
||||
} else if (iframeElement.msRequestFullscreen) {
|
||||
iframeElement.msRequestFullscreen();
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {});
|
||||
</script>
|
||||
|
||||
<div class=" w-full h-full relative flex flex-col bg-gray-50 dark:bg-gray-850">
|
||||
<div class="w-full h-full flex-1 relative">
|
||||
{#if overlay}
|
||||
<div class=" absolute top-0 left-0 right-0 bottom-0 z-10"></div>
|
||||
{/if}
|
||||
|
||||
<div class=" absolute pointer-events-none z-50 w-full flex items-center justify-end p-4">
|
||||
<button
|
||||
class="self-center pointer-events-auto p-1 rounded-full bg-white dark:bg-gray-850"
|
||||
on:click={() => {
|
||||
dispatch('close');
|
||||
showControls.set(false);
|
||||
showArtifacts.set(false);
|
||||
}}
|
||||
>
|
||||
<XMark className="size-3 text-gray-900 dark:text-white" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 w-full h-full">
|
||||
<div class=" h-full flex flex-col">
|
||||
{#if contents.length > 0}
|
||||
<div class="max-w-full w-full h-full">
|
||||
{#if contents[selectedContentIdx].type === 'iframe'}
|
||||
<iframe
|
||||
bind:this={iframeElement}
|
||||
title="Content"
|
||||
srcdoc={contents[selectedContentIdx].content}
|
||||
class="w-full border-0 h-full rounded-none"
|
||||
sandbox="allow-scripts allow-forms allow-same-origin"
|
||||
on:load={iframeLoadHandler}
|
||||
></iframe>
|
||||
{:else if contents[selectedContentIdx].type === 'svg'}
|
||||
<SvgPanZoom
|
||||
className=" w-full h-full max-h-full overflow-hidden"
|
||||
svg={contents[selectedContentIdx].content}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="m-auto font-medium text-xs text-gray-900 dark:text-white">
|
||||
{$i18n.t('No HTML, CSS, or JavaScript content found.')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if contents.length > 0}
|
||||
<div class="flex justify-between items-center p-2.5 font-primar text-gray-900 dark:text-white">
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="flex items-center gap-0.5 self-center min-w-fit" dir="ltr">
|
||||
<button
|
||||
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition disabled:cursor-not-allowed"
|
||||
on:click={() => navigateContent('prev')}
|
||||
disabled={contents.length <= 1}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
class="size-3.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15.75 19.5 8.25 12l7.5-7.5"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="text-xs self-center dark:text-gray-100 min-w-fit">
|
||||
{$i18n.t('Version {{selectedVersion}} of {{totalVersions}}', {
|
||||
selectedVersion: selectedContentIdx + 1,
|
||||
totalVersions: contents.length
|
||||
})}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition disabled:cursor-not-allowed"
|
||||
on:click={() => navigateContent('next')}
|
||||
disabled={contents.length <= 1}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
class="size-3.5"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
<button
|
||||
class="copy-code-button bg-none border-none text-xs bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5"
|
||||
on:click={() => {
|
||||
copyToClipboard(contents[selectedContentIdx].content);
|
||||
copied = true;
|
||||
|
||||
setTimeout(() => {
|
||||
copied = false;
|
||||
}, 2000);
|
||||
}}>{copied ? $i18n.t('Copied') : $i18n.t('Copy')}</button
|
||||
>
|
||||
|
||||
{#if contents[selectedContentIdx].type === 'iframe'}
|
||||
<Tooltip content={$i18n.t('Open in full screen')}>
|
||||
<button
|
||||
class=" bg-none border-none text-xs bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md p-0.5"
|
||||
on:click={showFullScreen}
|
||||
>
|
||||
<ArrowsPointingOut className="size-3.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
+758
-451
File diff suppressed because it is too large
Load Diff
@@ -1,68 +1,149 @@
|
||||
<script lang="ts">
|
||||
import { SvelteFlowProvider } from '@xyflow/svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
|
||||
import { onDestroy, onMount, tick } from 'svelte';
|
||||
import { mobile, showControls, showCallOverlay, showOverview, showArtifacts } from '$lib/stores';
|
||||
|
||||
import Modal from '../common/Modal.svelte';
|
||||
import Controls from './Controls/Controls.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { mobile, showCallOverlay } from '$lib/stores';
|
||||
import CallOverlay from './MessageInput/CallOverlay.svelte';
|
||||
import Drawer from '../common/Drawer.svelte';
|
||||
import Overview from './Overview.svelte';
|
||||
import { Pane, PaneResizer } from 'paneforge';
|
||||
import EllipsisVertical from '../icons/EllipsisVertical.svelte';
|
||||
import Artifacts from './Artifacts.svelte';
|
||||
|
||||
export let show = false;
|
||||
|
||||
export let history;
|
||||
export let models = [];
|
||||
|
||||
export let chatId = null;
|
||||
|
||||
export let chatFiles = [];
|
||||
export let params = {};
|
||||
|
||||
export let eventTarget: EventTarget;
|
||||
export let submitPrompt: Function;
|
||||
export let stopResponse: Function;
|
||||
export let showMessage: Function;
|
||||
export let files;
|
||||
export let modelId;
|
||||
|
||||
export let pane;
|
||||
|
||||
let mediaQuery;
|
||||
let largeScreen = false;
|
||||
let dragged = false;
|
||||
|
||||
const handleMediaQuery = async (e) => {
|
||||
if (e.matches) {
|
||||
largeScreen = true;
|
||||
|
||||
if ($showCallOverlay) {
|
||||
showCallOverlay.set(false);
|
||||
await tick();
|
||||
showCallOverlay.set(true);
|
||||
}
|
||||
} else {
|
||||
largeScreen = false;
|
||||
|
||||
if ($showCallOverlay) {
|
||||
showCallOverlay.set(false);
|
||||
await tick();
|
||||
showCallOverlay.set(true);
|
||||
}
|
||||
pane = null;
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseDown = (event) => {
|
||||
dragged = true;
|
||||
};
|
||||
|
||||
const onMouseUp = (event) => {
|
||||
dragged = false;
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
// listen to resize 1024px
|
||||
const mediaQuery = window.matchMedia('(min-width: 1024px)');
|
||||
|
||||
const handleMediaQuery = (e) => {
|
||||
if (e.matches) {
|
||||
largeScreen = true;
|
||||
} else {
|
||||
largeScreen = false;
|
||||
}
|
||||
};
|
||||
mediaQuery = window.matchMedia('(min-width: 1024px)');
|
||||
|
||||
mediaQuery.addEventListener('change', handleMediaQuery);
|
||||
|
||||
handleMediaQuery(mediaQuery);
|
||||
|
||||
return () => {
|
||||
mediaQuery.removeEventListener('change', handleMediaQuery);
|
||||
};
|
||||
document.addEventListener('mousedown', onMouseDown);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
showControls.set(false);
|
||||
|
||||
mediaQuery.removeEventListener('change', handleMediaQuery);
|
||||
document.removeEventListener('mousedown', onMouseDown);
|
||||
document.removeEventListener('mouseup', onMouseUp);
|
||||
});
|
||||
|
||||
const closeHandler = () => {
|
||||
showControls.set(false);
|
||||
showOverview.set(false);
|
||||
showArtifacts.set(false);
|
||||
|
||||
if ($showCallOverlay) {
|
||||
showCallOverlay.set(false);
|
||||
}
|
||||
};
|
||||
|
||||
$: if (!chatId) {
|
||||
closeHandler();
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if largeScreen}
|
||||
{#if show}
|
||||
<div class=" absolute bottom-0 right-0 z-20 h-full pointer-events-none">
|
||||
<div class="pr-4 pt-14 pb-8 w-[24rem] h-full" in:slide={{ duration: 200, axis: 'x' }}>
|
||||
<SvelteFlowProvider>
|
||||
{#if !largeScreen}
|
||||
{#if $showControls}
|
||||
<Drawer
|
||||
show={$showControls}
|
||||
on:close={() => {
|
||||
showControls.set(false);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="w-full h-full px-5 py-4 bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-50 dark:border-gray-800 rounded-xl z-50 pointer-events-auto overflow-y-auto scrollbar-hidden"
|
||||
class=" {$showCallOverlay || $showOverview || $showArtifacts
|
||||
? ' h-screen w-screen'
|
||||
: 'px-6 py-4'} h-full"
|
||||
>
|
||||
{#if $showCallOverlay}
|
||||
<CallOverlay
|
||||
bind:files
|
||||
{submitPrompt}
|
||||
{stopResponse}
|
||||
{modelId}
|
||||
{chatId}
|
||||
{eventTarget}
|
||||
<div
|
||||
class=" h-full max-h-[100dvh] bg-white text-gray-700 dark:bg-black dark:text-gray-300 flex justify-center"
|
||||
>
|
||||
<CallOverlay
|
||||
bind:files
|
||||
{submitPrompt}
|
||||
{stopResponse}
|
||||
{modelId}
|
||||
{chatId}
|
||||
{eventTarget}
|
||||
on:close={() => {
|
||||
showControls.set(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else if $showArtifacts}
|
||||
<Artifacts {history} />
|
||||
{:else if $showOverview}
|
||||
<Overview
|
||||
{history}
|
||||
on:nodeclick={(e) => {
|
||||
showMessage(e.detail.node.data.message);
|
||||
}}
|
||||
on:close={() => {
|
||||
showControls.set(false);
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<Controls
|
||||
on:close={() => {
|
||||
show = false;
|
||||
showControls.set(false);
|
||||
}}
|
||||
{models}
|
||||
bind:chatFiles
|
||||
@@ -70,38 +151,89 @@
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if $showCallOverlay}
|
||||
<div class=" absolute w-full h-screen max-h-[100dvh] flex z-[999] overflow-hidden">
|
||||
<div
|
||||
class="absolute w-full h-screen max-h-[100dvh] bg-white text-gray-700 dark:bg-black dark:text-gray-300 flex justify-center"
|
||||
</Drawer>
|
||||
{/if}
|
||||
{:else}
|
||||
<!-- if $showControls -->
|
||||
|
||||
{#if $showControls}
|
||||
<PaneResizer class="relative flex w-2 items-center justify-center bg-background group">
|
||||
<div class="z-10 flex h-7 w-5 items-center justify-center rounded-sm">
|
||||
<EllipsisVertical className="size-4 invisible group-hover:visible" />
|
||||
</div>
|
||||
</PaneResizer>
|
||||
{/if}
|
||||
<Pane
|
||||
bind:pane
|
||||
defaultSize={$showControls
|
||||
? parseInt(localStorage?.chatControlsSize ?? '30')
|
||||
? parseInt(localStorage?.chatControlsSize ?? '30')
|
||||
: 30
|
||||
: 0}
|
||||
onResize={(size) => {
|
||||
if (size === 0) {
|
||||
showControls.set(false);
|
||||
} else {
|
||||
if (!$showControls) {
|
||||
showControls.set(true);
|
||||
}
|
||||
localStorage.chatControlsSize = size;
|
||||
}
|
||||
}}
|
||||
class="pt-8"
|
||||
>
|
||||
<CallOverlay
|
||||
bind:files
|
||||
{submitPrompt}
|
||||
{stopResponse}
|
||||
{modelId}
|
||||
{chatId}
|
||||
{eventTarget}
|
||||
on:close={() => {
|
||||
show = false;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<Modal bind:show>
|
||||
<div class=" px-6 py-4 h-full">
|
||||
<Controls
|
||||
on:close={() => {
|
||||
show = false;
|
||||
}}
|
||||
{models}
|
||||
bind:chatFiles
|
||||
bind:params
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
{/if}
|
||||
{#if $showControls}
|
||||
<div class="pr-4 pb-8 flex max-h-full min-h-full">
|
||||
<div
|
||||
class="w-full {($showOverview || $showArtifacts) && !$showCallOverlay
|
||||
? ' '
|
||||
: 'px-5 py-4 bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-50 dark:border-gray-800'} rounded-lg z-40 pointer-events-auto overflow-y-auto scrollbar-hidden"
|
||||
>
|
||||
{#if $showCallOverlay}
|
||||
<div class="w-full h-full flex justify-center">
|
||||
<CallOverlay
|
||||
bind:files
|
||||
{submitPrompt}
|
||||
{stopResponse}
|
||||
{modelId}
|
||||
{chatId}
|
||||
{eventTarget}
|
||||
on:close={() => {
|
||||
showControls.set(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else if $showArtifacts}
|
||||
<Artifacts {history} overlay={dragged} />
|
||||
{:else if $showOverview}
|
||||
<Overview
|
||||
{history}
|
||||
on:nodeclick={(e) => {
|
||||
if (e.detail.node.data.message.favorite) {
|
||||
history.messages[e.detail.node.data.message.id].favorite = true;
|
||||
} else {
|
||||
history.messages[e.detail.node.data.message.id].favorite = null;
|
||||
}
|
||||
|
||||
showMessage(e.detail.node.data.message);
|
||||
}}
|
||||
on:close={() => {
|
||||
showControls.set(false);
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<Controls
|
||||
on:close={() => {
|
||||
showControls.set(false);
|
||||
}}
|
||||
{models}
|
||||
bind:chatFiles
|
||||
bind:params
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</Pane>
|
||||
{/if}
|
||||
</SvelteFlowProvider>
|
||||
|
||||
+8
-5
@@ -7,7 +7,7 @@
|
||||
|
||||
import { blur, fade } from 'svelte/transition';
|
||||
|
||||
import Suggestions from '../MessageInput/Suggestions.svelte';
|
||||
import Suggestions from './Suggestions.svelte';
|
||||
import { sanitizeResponseContent } from '$lib/utils';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import EyeSlash from '$lib/components/icons/EyeSlash.svelte';
|
||||
@@ -34,9 +34,9 @@
|
||||
</script>
|
||||
|
||||
{#key mounted}
|
||||
<div class="m-auto w-full max-w-6xl px-8 lg:px-20 pb-10">
|
||||
<div class="m-auto w-full max-w-6xl px-8 lg:px-20">
|
||||
<div class="flex justify-start">
|
||||
<div class="flex -space-x-4 mb-1" in:fade={{ duration: 200 }}>
|
||||
<div class="flex -space-x-4 mb-0.5" in:fade={{ duration: 200 }}>
|
||||
{#each models as model, modelIdx}
|
||||
<button
|
||||
on:click={() => {
|
||||
@@ -78,7 +78,7 @@
|
||||
{/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 font-primary"
|
||||
class=" mt-2 mb-4 text-3xl text-gray-800 dark:text-gray-100 font-medium text-left flex items-center gap-4 font-primary"
|
||||
>
|
||||
<div>
|
||||
<div class=" capitalize line-clamp-1" in:fade={{ duration: 200 }}>
|
||||
@@ -125,10 +125,13 @@
|
||||
|
||||
<div class=" w-full font-primary" in:fade={{ duration: 200, delay: 300 }}>
|
||||
<Suggestions
|
||||
className="grid grid-cols-2"
|
||||
suggestionPrompts={models[selectedModelIdx]?.info?.meta?.suggestion_prompts ??
|
||||
$config?.default_prompt_suggestions ??
|
||||
[]}
|
||||
{submitPrompt}
|
||||
on:select={(e) => {
|
||||
submitPrompt(e.detail);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user