Compare commits
832 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c41b33c9c0 | |||
| 7131ac24a7 | |||
| 501cd2071c | |||
| 85a1233733 | |||
| 670fa83e2c | |||
| d03a53e755 | |||
| 2ec416bd6b | |||
| b1a391ca5d | |||
| c0ca447041 | |||
| 5d3db15eca | |||
| eead69068c | |||
| edae1ca4c7 | |||
| 0c9cc2baa3 | |||
| 326ac41422 | |||
| dd2941bb43 | |||
| a691ee08d8 | |||
| e8fc522eba | |||
| 482a41e49b | |||
| ae3d8ce0c0 | |||
| a2e30ace02 | |||
| cde9672a09 | |||
| db6ca856de | |||
| bb5e615841 | |||
| 48ac4d9f2a | |||
| eb7bba81fe | |||
| a43cb1eb25 | |||
| e2d8872060 | |||
| ede584320f | |||
| bc458d3ee0 | |||
| 0d1f7265c4 | |||
| 539201a032 | |||
| 50ad2aa215 | |||
| 5cd28c04b8 | |||
| c3ce4d7f6a | |||
| 9bd054490f | |||
| 2be14d57bf | |||
| 61e0a85aed | |||
| e483c6b598 | |||
| db3b075796 | |||
| 0bb26ae504 | |||
| 9d16dd997a | |||
| e4fe1fff97 | |||
| 37ed31b0e2 | |||
| dc82e681dd | |||
| 4944824544 | |||
| 67cd9b76ae | |||
| 8a86f32700 | |||
| f62b15d8da | |||
| d709038b5b | |||
| a2631ed38e | |||
| 8ffd8c3505 | |||
| f74df0433b | |||
| 93fa98245b | |||
| e7727aea9e | |||
| 884eb9f7ee | |||
| 912b75f8ac | |||
| 3bec60b80c | |||
| 07b08ef67e | |||
| 83f9475584 | |||
| 1ad5456fe0 | |||
| bf1936de34 | |||
| 827dd1b044 | |||
| b2028d187d | |||
| 1e1b372d07 | |||
| bd5a8567ef | |||
| fc46532955 | |||
| 5f29a9edf4 | |||
| 91bc65c7da | |||
| a91db7f7a6 | |||
| e6fe3ada57 | |||
| 354683296b | |||
| 5237439e29 | |||
| 049b3136e8 | |||
| 3d6f5f418d | |||
| a27175d672 | |||
| ff1cd306d8 | |||
| aa7d25600f | |||
| 0bae7ca615 | |||
| 3578b5e337 | |||
| 8464b30485 | |||
| c961964647 | |||
| dd7ac4c53a | |||
| 4601a0246f | |||
| 25c0eca414 | |||
| ca8be1ee4a | |||
| 1611a3aa70 | |||
| b434ebf3ad | |||
| d2ed99e201 | |||
| 06976c4551 | |||
| 626643104e | |||
| c5683dd24c | |||
| 6589464ddf | |||
| e27c264081 | |||
| 3a96e1f109 | |||
| c4629c8c2d | |||
| adea3af8d7 | |||
| ec516e95fa | |||
| 481fd9f88a | |||
| 2bbaf1234c | |||
| 690c25215d | |||
| b72243c166 | |||
| 8f2e799615 | |||
| 8ecf262457 | |||
| 7fb785901e | |||
| 5dc9b93916 | |||
| 1241bc3e89 | |||
| c5ed3452d2 | |||
| c2e6e44714 | |||
| 6e7e575a18 | |||
| f43b545bdd | |||
| 12a04b118f | |||
| 8b1e2ce279 | |||
| fb0f106afe | |||
| dd423f43de | |||
| f2bd3fdf19 | |||
| 8ad52f0fcc | |||
| 5a3736f1ee | |||
| 54bbfa5e89 | |||
| 3933db2c91 | |||
| dc8f0987e1 | |||
| 644f0fe6c3 | |||
| 87b04b0fc3 | |||
| 455403a4a4 | |||
| 547b990041 | |||
| fcf8f2a704 | |||
| 496e798cc7 | |||
| e01b3569d5 | |||
| a75b68c19c | |||
| 34f04c53fc | |||
| 2b87ab32ff | |||
| d20f6cb45b | |||
| 48c9fb0ddf | |||
| c609cc23eb | |||
| 3566002b0b | |||
| 140d7ff721 | |||
| 7349e1d8c8 | |||
| bf5a62298c | |||
| 2972774d87 | |||
| 75d455ac8f | |||
| 64b6a8c5db | |||
| 8947da57af | |||
| 16f09fff63 | |||
| 14eb667fc8 | |||
| 79bf8d6dd3 | |||
| 14d33f0fcc | |||
| 4dcec4855e | |||
| 96a004d4d8 | |||
| 1fa16d73f0 | |||
| d2cd803a57 | |||
| e685953df7 | |||
| d6520e7fd8 | |||
| c54e3d5bf4 | |||
| efb4a710c8 | |||
| a71b824728 | |||
| bc1a7ab567 | |||
| b565301a47 | |||
| 78272aed8d | |||
| 3f7913b36f | |||
| e1edf9227d | |||
| a8d709102f | |||
| 9032ab8857 | |||
| c9137edf5a | |||
| d11c373cf1 | |||
| 2755ef62d9 | |||
| 8debb71197 | |||
| 8b4867deb5 | |||
| a93645aee8 | |||
| 28ca6fb678 | |||
| da4586f27d | |||
| 591cd993c2 | |||
| aa1bb4fb6d | |||
| 5e7237b9cb | |||
| 84defafc14 | |||
| 7b1404f490 | |||
| 9b38788aa2 | |||
| c44fc82ecd | |||
| 8b6f422d45 | |||
| 2eb8c3456f | |||
| fa9835a7ad | |||
| 3db9d19ae3 | |||
| 762db11bf4 | |||
| 8e9e429a91 | |||
| f2b9a5f5bf | |||
| 277fc3feac | |||
| 9053bfdadf | |||
| 414ab53144 | |||
| 4a7d3a076c | |||
| 8198807fc9 | |||
| e556c02fe9 | |||
| f7de01cbc2 | |||
| 1bc96de620 | |||
| 23489105fc | |||
| 8c95a8be3a | |||
| d00c8b00df | |||
| 4bae43096b | |||
| d90e138773 | |||
| bb15eb016e | |||
| 97b39115a1 | |||
| cc53fd8379 | |||
| 5342063e24 | |||
| e274a0dc3e | |||
| 9c3920a489 | |||
| d1da3dde65 | |||
| 0ccd96c950 | |||
| 3499ec3f79 | |||
| 40ea18d54d | |||
| ffbc480bb4 | |||
| 9b839e5c9f | |||
| 575f23cde9 | |||
| 711317555c | |||
| 063e1ee46c | |||
| 0d0f8aac15 | |||
| 5cbb79fa6e | |||
| 277e7aead7 | |||
| d3558fdb33 | |||
| 1271d1dccc | |||
| 3be1cb98d3 | |||
| c2a7e7f510 | |||
| dbde628141 | |||
| 2412f31ed9 | |||
| 8df0c9e0dd | |||
| 6f9148ac4c | |||
| d7d8896e43 | |||
| d5a2a8a880 | |||
| cff8534f33 | |||
| f69bc57fed | |||
| 4ecc1c06d3 | |||
| f78e6a5fba | |||
| 411e6c2fe4 | |||
| 369816ace0 | |||
| 06e4b87c9e | |||
| c6b74a3cf9 | |||
| 27417dd771 | |||
| 16615010f7 | |||
| e516374d54 | |||
| 55dc6c1b3b | |||
| da47c2dfa3 | |||
| 694d1708a3 | |||
| f9aa03bc6d | |||
| bd3e5d7bfa | |||
| 5743df2280 | |||
| e3efe2c565 | |||
| 32c4748749 | |||
| f4a15f9590 | |||
| f92ef3211b | |||
| 508e8eb4b1 | |||
| dfc2fb65af | |||
| d7ce408d25 | |||
| 374f647048 | |||
| 00018085ed | |||
| d17cdc8068 | |||
| e5ad76615c | |||
| 54420f5300 | |||
| f076f81e0c | |||
| ea80285e9c | |||
| f43d255e8f | |||
| fc3a31e3d5 | |||
| 4e640daf83 | |||
| 404bb3fd67 | |||
| eacce66a82 | |||
| 4f19a92dc5 | |||
| 5196b65c2e | |||
| 340b716a90 | |||
| ff4cf16742 | |||
| 7eaf83cb7d | |||
| 4a73a01c24 | |||
| 1a9dee50ce | |||
| f97f6601f7 | |||
| 5ab5231c73 | |||
| 046dd02a6d | |||
| 72e1615fe1 | |||
| b512501d9d | |||
| 36cc3c24e6 | |||
| 62bfcf35e5 | |||
| 9bec2cea12 | |||
| f4a122a4bb | |||
| 34aea03c02 | |||
| e7fb86190a | |||
| 194564e253 | |||
| d4b10097d2 | |||
| 3069fa8074 | |||
| c2bbb37fc1 | |||
| e52568e7cb | |||
| bc4aeeeec8 | |||
| 25ee5f6cb0 | |||
| 882ad219ea | |||
| c108d863f6 | |||
| a8d80f936a | |||
| 1bb7fc7c92 | |||
| dbb83f9824 | |||
| e1889b0c5e | |||
| 88758af621 | |||
| 43f83673e2 | |||
| 950073c1de | |||
| 651c85fcc4 | |||
| ec7c5a3f07 | |||
| d077b3dcdb | |||
| 1d6bbdf917 | |||
| f248425931 | |||
| bdf7ed7f02 | |||
| 66200cfb7d | |||
| 9174e01d6a | |||
| f17df4f114 | |||
| 1948e78351 | |||
| 73a98ff938 | |||
| 92d056c982 | |||
| 2be9c25ba7 | |||
| 56fda46215 | |||
| 1d1b945483 | |||
| 9aac6e05a9 | |||
| 73e2912849 | |||
| 7cf80289c3 | |||
| 2fa594d5bd | |||
| 45f1a4157b | |||
| b3e4ecd2f4 | |||
| ab04e0dfd4 | |||
| ee625a0519 | |||
| fea563cf89 | |||
| fcfdfa3dea | |||
| 844923d0a4 | |||
| bbfa54a6b9 | |||
| e2429314f3 | |||
| 4925a6530b | |||
| 85484392b2 | |||
| 8d9712d66e | |||
| 0495f01acb | |||
| f28877f4db | |||
| b673ae7ab2 | |||
| 8c3a6eb262 | |||
| 25336f85f3 | |||
| 74ed74f1a4 | |||
| 94ebf02719 | |||
| 11bb1a6f7d | |||
| 61867c1545 | |||
| 4068a421bf | |||
| 8a94d8a226 | |||
| 5a24ce06f0 | |||
| e72170bac7 | |||
| dd93b0dd24 | |||
| 0ac04c9518 | |||
| d28925de05 | |||
| 2c4c62fb75 | |||
| de162a1f32 | |||
| aa7e5dbfce | |||
| 8d99bffbdc | |||
| 768941bded | |||
| e0772c6807 | |||
| cbd60168ea | |||
| c6c5c0ddb2 | |||
| 7c35447666 | |||
| 58b5e0bf3e | |||
| 0586d76b5d | |||
| eb3fcef6d7 | |||
| 59a730cb8b | |||
| 7fefbb316d | |||
| 14646e84ea | |||
| ba56edab19 | |||
| a233a8855b | |||
| c11fdad82d | |||
| 82f7b6c315 | |||
| 92d9b38110 | |||
| 36a66fcfc4 | |||
| ea27bc50f5 | |||
| 8a9c18ddd5 | |||
| 3729771f33 | |||
| ab58947a19 | |||
| f9351af6a3 | |||
| 8f03daeaf6 | |||
| de03bf834e | |||
| db3d48ee15 | |||
| 6022f6f5df | |||
| 2d1a890ed9 | |||
| 08443b3c55 | |||
| 7e761a69a7 | |||
| be08dbf00d | |||
| b4fca046a8 | |||
| 61287a16d5 | |||
| 16e72b2f67 | |||
| 80d173c1c3 | |||
| 4b459e372f | |||
| fddf693dad | |||
| a39ab4dd24 | |||
| 13db33e947 | |||
| 727a837e53 | |||
| 22d7ce2b7f | |||
| 1d4c358624 | |||
| a3ecf62564 | |||
| 6199296205 | |||
| 5ea88f2e4f | |||
| 5be97b81d6 | |||
| 42f335851f | |||
| 0744806523 | |||
| a36555a25a | |||
| 814fbb73f3 | |||
| 3d74c04f50 | |||
| a9e5003c4f | |||
| 8114fa93cf | |||
| 32a61ea15d | |||
| d9a3e4db39 | |||
| c5ff4c24e1 | |||
| 9b3fdb1838 | |||
| 27ff3ab112 | |||
| d09164b729 | |||
| 44597314c6 | |||
| 3176fe0c2b | |||
| cfc78dedf0 | |||
| 642079ca29 | |||
| 523455a46e | |||
| 8eb81c9127 | |||
| 6e818e3694 | |||
| 6de8327665 | |||
| 0a9f41a044 | |||
| 2e24c2d4d3 | |||
| 41442897e3 | |||
| e0ba585204 | |||
| 0bae97218d | |||
| 2ad286698b | |||
| 5015486fc2 | |||
| b5b2b70f4a | |||
| 7f74426a22 | |||
| 4dd51badfe | |||
| 24c35c308d | |||
| 0cb8163321 | |||
| 80c6a39fab | |||
| 45bff743a2 | |||
| afd62c3b22 | |||
| 72354e06a7 | |||
| 207e25035a | |||
| e99b2ecdd6 | |||
| 1c7dd7b859 | |||
| 32a8884c0c | |||
| f1855f5208 | |||
| 6895e2439c | |||
| 1285ec1828 | |||
| bdcfbfeb11 | |||
| aa7fcca4ee | |||
| fc5f74cc64 | |||
| 86510de343 | |||
| a53796270f | |||
| fbdfb7e4fa | |||
| 999d2bc21b | |||
| 912a704fdc | |||
| ea6b8984ab | |||
| 0b8b039aae | |||
| dd799ebea8 | |||
| f2e5bebdbc | |||
| abbf622fba | |||
| e4808db281 | |||
| 500765a778 | |||
| c5c33aabf9 | |||
| ef4af86883 | |||
| d993beb18f | |||
| 53b44b75dc | |||
| 7f2e89a8aa | |||
| c6a57b1f6f | |||
| 475e09be45 | |||
| b124cd4ac5 | |||
| d2cbb14ebf | |||
| 1cef48730e | |||
| 561778d04d | |||
| 51d2cecd36 | |||
| 6758f3d390 | |||
| 6bd7c20fbb | |||
| 572155b40c | |||
| 950231acf7 | |||
| 78b279cabc | |||
| fd31b5f8d6 | |||
| afbea2ecea | |||
| 3730251958 | |||
| 2dbbd1bc6f | |||
| 5b01d7994a | |||
| 5ef478a154 | |||
| 92700302b9 | |||
| cb8c45d864 | |||
| fc2b314c4f | |||
| 9c67a94542 | |||
| cddbeb023d | |||
| eca20b1b2c | |||
| 0cf9b07ec2 | |||
| 9dfa334a83 | |||
| 7674229e3a | |||
| 36e2a5e674 | |||
| eb12d1e111 | |||
| ffcb8da346 | |||
| c999c86c5e | |||
| 9283ed1685 | |||
| 995f7bc51b | |||
| e1d65065f5 | |||
| 6922529b78 | |||
| 9cd150a048 | |||
| fec51342ca | |||
| ef8d84296e | |||
| d9ceb31674 | |||
| c76330710f | |||
| 4715160b53 | |||
| 7cec88c776 | |||
| 340b399fc2 | |||
| 12e60d8ebf | |||
| c1cabf1415 | |||
| 2981caa34b | |||
| 5c6373e057 | |||
| e427ef767b | |||
| 37c87e3a14 | |||
| 7f8be4a709 | |||
| 5cf44ac7da | |||
| e8d4e03c0d | |||
| 5609b12852 | |||
| 943baad689 | |||
| 2bdfd85137 | |||
| 834bdf46be | |||
| cf1c8be85f | |||
| 2d596d7307 | |||
| 7c271734a1 | |||
| 130d15a2fb | |||
| 0bef1b44c0 | |||
| 0383efa207 | |||
| e231333bcd | |||
| eea8fb591a | |||
| 0bef7fef29 | |||
| b870fd1118 | |||
| ec36493d61 | |||
| 966f10e715 | |||
| 4fac99c5b3 | |||
| cc6d9bb8c0 | |||
| 2070fa9e99 | |||
| 0edb940dca | |||
| da4a057273 | |||
| cc01c5004b | |||
| abce172b9d | |||
| 9d370c51e2 | |||
| e2e96b5776 | |||
| 5de2b00064 | |||
| ddc8d22e98 | |||
| 66734a7080 | |||
| 4d64893661 | |||
| 74a8deb19f | |||
| f3527df644 | |||
| 4685f523b6 | |||
| 73178cf519 | |||
| 5166e92f90 | |||
| b443d61c6f | |||
| 99b1661638 | |||
| f1a7c76693 | |||
| bced90734b | |||
| b6b71c08f3 | |||
| 47a9a2a190 | |||
| f2285f29b7 | |||
| ee913cb6e8 | |||
| dd959c8f88 | |||
| ccbafca74c | |||
| f98ada1776 | |||
| fab3f7fb1a | |||
| 9c2bd0ef7f | |||
| ac0d7201e3 | |||
| f3dd271e2c | |||
| 9d4c07b76a | |||
| 3b6acaa513 | |||
| 839877e197 | |||
| e39815420e | |||
| fc59db3181 | |||
| 5ea47c1258 | |||
| 225ec3d174 | |||
| b7fc37d992 | |||
| 9fb6160010 | |||
| c62cb43f7e | |||
| 0c04b18f36 | |||
| 78dedb3389 | |||
| efedd39db1 | |||
| 36f92164cd | |||
| a8e3d80256 | |||
| e8a3fe2e07 | |||
| d77657f0d0 | |||
| 3a737af190 | |||
| ecfc057f25 | |||
| fd98add6aa | |||
| 242d4f0c8d | |||
| 2e215ca85e | |||
| 00f32e2651 | |||
| e06417f0aa | |||
| ccff221921 | |||
| 1235714914 | |||
| 6c5f5fe368 | |||
| 276b7b90b8 | |||
| aba6308825 | |||
| 7b81271b9e | |||
| 83107c8ea5 | |||
| bf8c32f09f | |||
| d9de426fcd | |||
| e20bb23409 | |||
| 62d37f1f10 | |||
| 555e4e32d5 | |||
| 84bfebd05e | |||
| 633607a5c2 | |||
| 1fce466253 | |||
| 4fdb26fdc4 | |||
| 6e89a481be | |||
| 27f034b216 | |||
| f755f5e512 | |||
| 47e8465626 | |||
| e2fe87a4d0 | |||
| c2c7299650 | |||
| 98ec38a8e7 | |||
| 1ab01170b9 | |||
| c1526b0022 | |||
| 5e4a1ff6fb | |||
| 74ca4189e2 | |||
| 7f77b3addb | |||
| 79f440f302 | |||
| 50951459c9 | |||
| 6c8997fcbc | |||
| 9ad4a93876 | |||
| afcab78cab | |||
| 3d0b3eb577 | |||
| 92e5e6c78e | |||
| b1265c9c34 | |||
| b936c9bff7 | |||
| b6474aa02d | |||
| e9c8341d38 | |||
| 1b5dfb3aac | |||
| 67a5020cdc | |||
| a6af20e1eb | |||
| 42dcf1b08c | |||
| 88d053833d | |||
| 7d2ab168f1 | |||
| a579f5f8cf | |||
| 3d1cc1e422 | |||
| 1fbcf13371 | |||
| bd6ccf8c15 | |||
| 1aa33a4667 | |||
| fad07d2049 | |||
| ea69087697 | |||
| 331941dbc1 | |||
| 1fb4c60233 | |||
| 0c7f0f4430 | |||
| bbf5e37f6f | |||
| 96c0b8a6a6 | |||
| 0715cd2811 | |||
| 89d80b58e1 | |||
| 83097cd8be | |||
| c8393db97c | |||
| ce3d582e61 | |||
| dac9634242 | |||
| ca3108a54d | |||
| e316abcfc8 | |||
| 8bca17ee1d | |||
| 708d755eda | |||
| 0a48114bd2 | |||
| 468c6398cd | |||
| e80e4c304a | |||
| 404875cab9 | |||
| 6dbd59c7c6 | |||
| 110ed67468 | |||
| 4d57e08b38 | |||
| 17e4be49c0 | |||
| 3be0fa63ee | |||
| 363ab562c3 | |||
| 7d159ca723 | |||
| bfdf4f7a8a | |||
| f34fd3fbe1 | |||
| d0d76e2ad5 | |||
| 60433856a2 | |||
| 98194d97df | |||
| 1c2c244041 | |||
| f8e77ea129 | |||
| bf6f0c380b | |||
| 929384e39a | |||
| c7027f11d6 | |||
| f8778333e5 | |||
| ffd157d80c | |||
| b4d737c2ff | |||
| 482d615950 | |||
| 1aebf88069 | |||
| dcae813d05 | |||
| 8df0429c99 | |||
| 59602eea09 | |||
| 21ca55dd30 | |||
| ab1aeba1dd | |||
| af022947be | |||
| 798e6e4128 | |||
| 793a8603ec | |||
| 771657266a | |||
| 45bd0f2a91 | |||
| 25c9fb6a3c | |||
| f753db603b | |||
| c8a60cc1ea | |||
| 5d356bdc6b | |||
| d08693f4eb | |||
| 44d73dd62a | |||
| c23a944bac | |||
| e645679496 | |||
| bf614068c0 | |||
| 0cc4366f70 | |||
| cc47e63526 | |||
| ecb656af70 | |||
| b20ec0586e | |||
| 889a2fd77e | |||
| f21c8626d6 | |||
| 2502bd8aeb | |||
| 54c29f8bd3 | |||
| 19bb6e36e1 | |||
| e3f8ca993d | |||
| c1a97278a8 | |||
| 7ccef3e77a | |||
| 247af333b6 | |||
| 34253d216a | |||
| 2e16fe9f85 | |||
| 3878a09095 | |||
| 928b791950 | |||
| 3aa201b7d5 | |||
| 6d237999dc | |||
| b089986248 | |||
| 5d384272b7 | |||
| 5eb9b00f8d | |||
| 8fc64e45b9 | |||
| d43ee0fc5b | |||
| 6b57d8d370 | |||
| b3de61247b | |||
| 87982fcab3 | |||
| 2e953e6faa | |||
| b87cf0db7d | |||
| a07b89da6f | |||
| 8116b04338 | |||
| 968df25f35 | |||
| 4f0b6e39a8 | |||
| 51b6f0d490 | |||
| 7d5a7c6194 | |||
| 7a556b2240 | |||
| 3355577bac | |||
| 23859756d8 | |||
| 9ee2f98a6a | |||
| 88ee3a4205 | |||
| e344d3fbf7 | |||
| e9e17cd0a8 | |||
| cbec9ee6ca | |||
| a937f6504a | |||
| 33195b1c4b | |||
| ec4c6e3918 | |||
| e142223ed1 | |||
| 224a578e6b | |||
| aea81fc988 | |||
| 54677a3374 | |||
| 69bac2a20e | |||
| eb509c460a | |||
| 97e6c2ac04 | |||
| 175293fb63 | |||
| 322db31dc9 | |||
| 985ee1c261 | |||
| 6e6b1382ef | |||
| 5437d90a25 | |||
| baddd68741 | |||
| 936401b456 | |||
| f680817386 | |||
| 9e23a3bde7 | |||
| a626796650 | |||
| 04c2db430d | |||
| 23fef594fe | |||
| 1ad8746f69 | |||
| 60df8a6203 | |||
| 92f46faf6e | |||
| d203a2a7c6 | |||
| 5b96939342 | |||
| 2684ef64c7 | |||
| 8aef3934c4 | |||
| 0d43d9ef3a | |||
| e337c7827b | |||
| 62b886a030 | |||
| f83dee792a | |||
| a354d6043b | |||
| 20b9e2abd8 | |||
| 02a0a599ff | |||
| 314bd28d21 | |||
| 9fdaece415 | |||
| 9d1013748f | |||
| b60989627e | |||
| a018df2734 | |||
| d5a4ab46f4 | |||
| 1a16f8fb1c | |||
| 2ce6535a2d | |||
| 056c413e23 | |||
| 3c55773bb2 | |||
| a600a32f18 | |||
| 7068ea9279 | |||
| b088b06575 | |||
| 064d307d96 | |||
| 66c2024d76 | |||
| ab271c82ee | |||
| dc6db5e8b0 | |||
| e10d029b06 | |||
| 197af1264d | |||
| ae26596d0d | |||
| 715a4a6c27 | |||
| fa4c22492f | |||
| 4002ead6af | |||
| 1bacd5d93f | |||
| 5d3eddf7e9 | |||
| 0665703401 | |||
| c9799991f2 | |||
| b95027f182 | |||
| 9021f068b8 | |||
| 867c179e2d | |||
| 81a3c97069 | |||
| f94690386c | |||
| 5e1c408937 | |||
| f49e1afaa6 | |||
| 44c8b0bb83 | |||
| 466b3e3637 | |||
| 654cc09128 | |||
| d98051862d | |||
| 3baeda7edc | |||
| d45804d7f4 | |||
| 9ed1a31575 | |||
| 7538dc051e | |||
| 77928ae141 | |||
| 2660a6e5b8 | |||
| 619c2f9c71 | |||
| 14a902fcfa | |||
| fb8069123e | |||
| 3a1fbb936b | |||
| 0f50c12c59 | |||
| fa6e21f5e0 | |||
| 02a4412dfc | |||
| 0dbddebcb0 | |||
| 8effff6524 | |||
| 9bc628ca75 | |||
| 584385e4bf | |||
| e69f31267a | |||
| e76a444ed9 | |||
| 635951b55c | |||
| 8b3e370a6e | |||
| 83f086ccdd | |||
| 99e4edd364 | |||
| 501ff7a98b |
+1
-5
@@ -10,8 +10,4 @@ OPENAI_API_KEY=''
|
||||
# DO NOT TRACK
|
||||
SCARF_NO_ANALYTICS=true
|
||||
DO_NOT_TRACK=true
|
||||
ANONYMIZED_TELEMETRY=false
|
||||
|
||||
# Use locally bundled version of the LiteLLM cost map json
|
||||
# to avoid repetitive startup connections
|
||||
LITELLM_LOCAL_MODEL_COST_MAP="True"
|
||||
ANONYMIZED_TELEMETRY=false
|
||||
@@ -11,7 +11,7 @@
|
||||
- [ ] **Dependencies:** Are there any new dependencies? Have you updated the dependency versions in the documentation?
|
||||
- [ ] **Testing:** Have you written and run sufficient tests for validating the changes?
|
||||
- [ ] **Code review:** Have you performed a self-review of your code, addressing any coding standard issues and ensuring adherence to the project's coding standards?
|
||||
- [ ] **Label:** To cleary categorize this pull request, assign a relevant label to the pull request title, using one of the following:
|
||||
- [ ] **Prefix:** To cleary categorize this pull request, prefix the pull request title, using one of the following:
|
||||
- **BREAKING CHANGE**: Significant changes that may affect compatibility
|
||||
- **build**: Changes that affect the build system or external dependencies
|
||||
- **ci**: Changes to our continuous integration processes or workflows
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check for changes in package.json
|
||||
run: |
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
echo "::set-output name=content::$CHANGELOG_ESCAPED"
|
||||
|
||||
- name: Create GitHub release
|
||||
uses: actions/github-script@v5
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
console.log(`Created release ${release.data.html_url}`)
|
||||
|
||||
- name: Upload package to GitHub release
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: package
|
||||
path: .
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
name: Deploy to HuggingFace Spaces
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check-secret:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
token-set: ${{ steps.check-key.outputs.defined }}
|
||||
steps:
|
||||
- id: check-key
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
||||
if: "${{ env.HF_TOKEN != '' }}"
|
||||
run: echo "defined=true" >> $GITHUB_OUTPUT
|
||||
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [check-secret]
|
||||
if: needs.check-secret.outputs.token-set == 'true'
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Remove git history
|
||||
run: rm -rf .git
|
||||
|
||||
- name: Prepend YAML front matter to README.md
|
||||
run: |
|
||||
echo "---" > temp_readme.md
|
||||
echo "title: Open WebUI" >> temp_readme.md
|
||||
echo "emoji: 🐳" >> temp_readme.md
|
||||
echo "colorFrom: purple" >> temp_readme.md
|
||||
echo "colorTo: gray" >> temp_readme.md
|
||||
echo "sdk: docker" >> temp_readme.md
|
||||
echo "app_port: 8080" >> temp_readme.md
|
||||
echo "---" >> temp_readme.md
|
||||
cat README.md >> temp_readme.md
|
||||
mv temp_readme.md README.md
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config --global user.name "github-actions[bot]"
|
||||
- name: Set up Git and push to Space
|
||||
run: |
|
||||
git init --initial-branch=main
|
||||
git lfs track "*.ttf"
|
||||
rm demo.gif
|
||||
git add .
|
||||
git commit -m "GitHub deploy: ${{ github.sha }}"
|
||||
git push --force https://open-webui:${HF_TOKEN}@huggingface.co/spaces/open-webui/open-webui main
|
||||
@@ -63,6 +63,18 @@ jobs:
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Extract metadata for Docker cache
|
||||
id: cache-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
|
||||
flavor: |
|
||||
prefix=cache-${{ matrix.platform }}-
|
||||
latest=false
|
||||
|
||||
- name: Build Docker image (latest)
|
||||
uses: docker/build-push-action@v5
|
||||
id: build
|
||||
@@ -72,8 +84,10 @@ jobs:
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
|
||||
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
|
||||
build-args: |
|
||||
BUILD_HASH=${{ github.sha }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
@@ -123,7 +137,7 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images (default latest tag)
|
||||
- name: Extract metadata for Docker images (cuda tag)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
@@ -139,6 +153,18 @@ jobs:
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
suffix=-cuda,onlatest=true
|
||||
|
||||
- name: Extract metadata for Docker cache
|
||||
id: cache-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
|
||||
flavor: |
|
||||
prefix=cache-cuda-${{ matrix.platform }}-
|
||||
latest=false
|
||||
|
||||
- name: Build Docker image (cuda)
|
||||
uses: docker/build-push-action@v5
|
||||
id: build
|
||||
@@ -148,9 +174,11 @@ jobs:
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: USE_CUDA=true
|
||||
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
|
||||
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
|
||||
build-args: |
|
||||
BUILD_HASH=${{ github.sha }}
|
||||
USE_CUDA=true
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
@@ -216,6 +244,18 @@ jobs:
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
suffix=-ollama,onlatest=true
|
||||
|
||||
- name: Extract metadata for Docker cache
|
||||
id: cache-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
|
||||
flavor: |
|
||||
prefix=cache-ollama-${{ matrix.platform }}-
|
||||
latest=false
|
||||
|
||||
- name: Build Docker image (ollama)
|
||||
uses: docker/build-push-action@v5
|
||||
id: build
|
||||
@@ -225,9 +265,11 @@ jobs:
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: USE_OLLAMA=true
|
||||
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
|
||||
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
|
||||
build-args: |
|
||||
BUILD_HASH=${{ github.sha }}
|
||||
USE_OLLAMA=true
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20' # Or specify any other version you want to use
|
||||
|
||||
|
||||
@@ -20,7 +20,11 @@ jobs:
|
||||
|
||||
- name: Build and run Compose Stack
|
||||
run: |
|
||||
docker compose --file docker-compose.yaml --file docker-compose.api.yaml up --detach --build
|
||||
docker compose \
|
||||
--file docker-compose.yaml \
|
||||
--file docker-compose.api.yaml \
|
||||
--file docker-compose.a1111-test.yaml \
|
||||
up --detach --build
|
||||
|
||||
- name: Wait for Ollama to be up
|
||||
timeout-minutes: 5
|
||||
@@ -95,7 +99,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
name: Release to PyPI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main # or whatever branch you want to use
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/p/open-webui
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
- name: Build
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install build
|
||||
python -m build .
|
||||
- name: Publish package distributions to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
+176
@@ -5,6 +5,182 @@ 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.3] - 2024-06-12
|
||||
|
||||
### Added
|
||||
|
||||
- **🛠️ Native Python Function Calling**: Introducing native Python function calling within Open WebUI. We’ve also included a built-in code editor to seamlessly develop and integrate function code within the 'Tools' workspace. With this, you can significantly enhance your LLM’s capabilities by creating custom RAG pipelines, web search tools, and even agent-like features such as sending Discord messages.
|
||||
- **🌐 DuckDuckGo Integration**: Added DuckDuckGo as a web search provider, giving you more search options.
|
||||
- **🌏 Enhanced Translations**: Improved translations for Vietnamese and Chinese languages, making the interface more accessible.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🔗 Web Search URL Error Handling**: Fixed the issue where a single URL error would disrupt the data loading process in Web Search mode. Now, such errors will be handled gracefully to ensure uninterrupted data loading.
|
||||
- **🖥️ Frontend Responsiveness**: Resolved the problem where the frontend would stop responding if the backend encounters an error while downloading a model. Improved error handling to maintain frontend stability.
|
||||
- **🔧 Dependency Issues in pip**: Fixed issues related to pip installations, ensuring all dependencies are correctly managed to prevent installation errors.
|
||||
|
||||
## [0.3.2] - 2024-06-10
|
||||
|
||||
### Added
|
||||
|
||||
- **🔍 Web Search Query Status**: The web search query will now persist in the results section to aid in easier debugging and tracking of search queries.
|
||||
- **🌐 New Web Search Provider**: We have added Serply as a new option for web search providers, giving you more choices for your search needs.
|
||||
- **🌏 Improved Translations**: We've enhanced translations for Chinese and Portuguese.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🎤 Audio File Upload Issue**: The bug that prevented audio files from being uploaded in chat input has been fixed, ensuring smooth communication.
|
||||
- **💬 Message Input Handling**: Improved the handling of message inputs by instantly clearing images and text after sending, along with immediate visual indications when a response message is loading, enhancing user feedback.
|
||||
- **⚙️ Parameter Registration and Validation**: Fixed the issue where parameters were not registering in certain cases and addressed the problem where users were unable to save due to invalid input errors.
|
||||
|
||||
## [0.3.1] - 2024-06-09
|
||||
|
||||
### Fixed
|
||||
|
||||
- **💬 Chat Functionality**: Resolved the issue where chat functionality was not working for specific models.
|
||||
|
||||
## [0.3.0] - 2024-06-09
|
||||
|
||||
### Added
|
||||
|
||||
- **📚 Knowledge Support for Models**: Attach documents directly to models from the models workspace, enhancing the information available to each model.
|
||||
- **🎙️ Hands-Free Voice Call Feature**: Initiate voice calls without needing to use your hands, making interactions more seamless.
|
||||
- **📹 Video Call Feature**: Enable video calls with supported vision models like Llava and GPT-4o, adding a visual dimension to your communications.
|
||||
- **🎛️ Enhanced UI for Voice Recording**: Improved user interface for the voice recording feature, making it more intuitive and user-friendly.
|
||||
- **🌐 External STT Support**: Now support for external Speech-To-Text services, providing more flexibility in choosing your STT provider.
|
||||
- **⚙️ Unified Settings**: Consolidated settings including document settings under a new admin settings section for easier management.
|
||||
- **🌑 Dark Mode Splash Screen**: A new splash screen for dark mode, ensuring a consistent and visually appealing experience for dark mode users.
|
||||
- **📥 Upload Pipeline**: Directly upload pipelines from the admin settings > pipelines section, streamlining the pipeline management process.
|
||||
- **🌍 Improved Language Support**: Enhanced support for Chinese and Ukrainian languages, better catering to a global user base.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🛠️ Playground Issue**: Fixed the playground not functioning properly, ensuring a smoother user experience.
|
||||
- **🔥 Temperature Parameter Issue**: Corrected the issue where the temperature value '0' was not being passed correctly.
|
||||
- **📝 Prompt Input Clearing**: Resolved prompt input textarea not being cleared right away, ensuring a clean slate for new inputs.
|
||||
- **✨ Various UI Styling Issues**: Fixed numerous user interface styling problems for a more cohesive look.
|
||||
- **👥 Active Users Display**: Fixed active users showing active sessions instead of actual users, now reflecting accurate user activity.
|
||||
- **🌐 Community Platform Compatibility**: The Community Platform is back online and fully compatible with Open WebUI.
|
||||
|
||||
### Changed
|
||||
|
||||
- **📝 RAG Implementation**: Updated the RAG (Retrieval-Augmented Generation) implementation to use a system prompt for context, instead of overriding the user's prompt.
|
||||
- **🔄 Settings Relocation**: Moved Models, Connections, Audio, and Images settings to the admin settings for better organization.
|
||||
- **✍️ Improved Title Generation**: Enhanced the default prompt for title generation, yielding better results.
|
||||
- **🔧 Backend Task Management**: Tasks like title generation and search query generation are now managed on the backend side and controlled only by the admin.
|
||||
- **🔍 Editable Search Query Prompt**: You can now edit the search query generation prompt, offering more control over how queries are generated.
|
||||
- **📏 Prompt Length Threshold**: Set the prompt length threshold for search query generation from the admin settings, giving more customization options.
|
||||
- **📣 Settings Consolidation**: Merged the Banners admin setting with the Interface admin setting for a more streamlined settings area.
|
||||
|
||||
## [0.2.5] - 2024-06-05
|
||||
|
||||
### Added
|
||||
|
||||
- **👥 Active Users Indicator**: Now you can see how many people are currently active and what they are running. This helps you gauge when performance might slow down due to a high number of users.
|
||||
- **🗂️ Create Ollama Modelfile**: The option to create a modelfile for Ollama has been reintroduced in the Settings > Models section, making it easier to manage your models.
|
||||
- **⚙️ Default Model Setting**: Added an option to set the default model from Settings > Interface. This feature is now easily accessible, especially convenient for mobile users as it was previously hidden.
|
||||
- **🌐 Enhanced Translations**: We've improved the Chinese translations and added support for Turkmen and Norwegian languages to make the interface more accessible globally.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **📱 Mobile View Improvements**: The UI now uses dvh (dynamic viewport height) instead of vh (viewport height), providing a better and more responsive experience for mobile users.
|
||||
|
||||
## [0.2.4] - 2024-06-03
|
||||
|
||||
### Added
|
||||
|
||||
- **👤 Improved Account Pending Page**: The account pending page now displays admin details by default to avoid confusion. You can disable this feature in the admin settings if needed.
|
||||
- **🌐 HTTP Proxy Support**: We have enabled the use of the 'http_proxy' environment variable in OpenAI and Ollama API calls, making it easier to configure network settings.
|
||||
- **❓ Quick Access to Documentation**: You can now easily access Open WebUI documents via a question mark button located at the bottom right corner of the screen (available on larger screens like PCs).
|
||||
- **🌍 Enhanced Translation**: Improvements have been made to translations.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🔍 SearxNG Web Search**: Fixed the issue where the SearxNG web search functionality was not working properly.
|
||||
|
||||
## [0.2.3] - 2024-06-03
|
||||
|
||||
### Added
|
||||
|
||||
- **📁 Export Chat as JSON**: You can now export individual chats as JSON files from the navbar menu by navigating to 'Download > Export Chat'. This makes sharing specific conversations easier.
|
||||
- **✏️ Edit Titles with Double Click**: Double-click on titles to rename them quickly and efficiently.
|
||||
- **🧩 Batch Multiple Embeddings**: Introduced 'RAG_EMBEDDING_OPENAI_BATCH_SIZE' to process multiple embeddings in a batch, enhancing performance for large datasets.
|
||||
- **🌍 Improved Translations**: Enhanced the translation quality across various languages for a better user experience.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🛠️ Modelfile Migration Script**: Fixed an issue where the modelfile migration script would fail if an invalid modelfile was encountered.
|
||||
- **💬 Zhuyin Input Method on Mac**: Resolved an issue where using the Zhuyin input method in the Web UI on a Mac caused text to send immediately upon pressing the enter key, leading to incorrect input.
|
||||
- **🔊 Local TTS Voice Selection**: Fixed the issue where the selected local Text-to-Speech (TTS) voice was not being displayed in settings.
|
||||
|
||||
## [0.2.2] - 2024-06-02
|
||||
|
||||
### Added
|
||||
|
||||
- **🌊 Mermaid Rendering Support**: We've included support for Mermaid rendering. This allows you to create beautiful diagrams and flowcharts directly within Open WebUI.
|
||||
- **🔄 New Environment Variable 'RESET_CONFIG_ON_START'**: Introducing a new environment variable: 'RESET_CONFIG_ON_START'. Set this variable to reset your configuration settings upon starting the application, making it easier to revert to default settings.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🔧 Pipelines Filter Issue**: We've addressed an issue with the pipelines where filters were not functioning as expected.
|
||||
|
||||
## [0.2.1] - 2024-06-02
|
||||
|
||||
### Added
|
||||
|
||||
- **🖱️ Single Model Export Button**: Easily export models with just one click using the new single model export button.
|
||||
- **🖥️ Advanced Parameters Support**: Added support for 'num_thread', 'use_mmap', and 'use_mlock' parameters for Ollama.
|
||||
- **🌐 Improved Vietnamese Translation**: Enhanced Vietnamese language support for a better user experience for our Vietnamese-speaking community.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🔧 OpenAI URL API Save Issue**: Corrected a problem preventing the saving of OpenAI URL API settings.
|
||||
- **🚫 Display Issue with Disabled Ollama API**: Fixed the display bug causing models to appear in settings when the Ollama API was disabled.
|
||||
|
||||
### Changed
|
||||
|
||||
- **💡 Versioning Update**: As a reminder from our previous update, version 0.2.y will focus primarily on bug fixes, while major updates will be designated as 0.x from now on for better version tracking.
|
||||
|
||||
## [0.2.0] - 2024-06-01
|
||||
|
||||
### Added
|
||||
|
||||
- **🔧 Pipelines Support**: Open WebUI now includes a plugin framework for enhanced customization and functionality (https://github.com/open-webui/pipelines). Easily add custom logic and integrate Python libraries, from AI agents to home automation APIs.
|
||||
- **🔗 Function Calling via Pipelines**: Integrate function calling seamlessly through Pipelines.
|
||||
- **⚖️ User Rate Limiting via Pipelines**: Implement user-specific rate limits to manage API usage efficiently.
|
||||
- **📊 Usage Monitoring with Langfuse**: Track and analyze usage statistics with Langfuse integration through Pipelines.
|
||||
- **🕒 Conversation Turn Limits**: Set limits on conversation turns to manage interactions better through Pipelines.
|
||||
- **🛡️ Toxic Message Filtering**: Automatically filter out toxic messages to maintain a safe environment using Pipelines.
|
||||
- **🔍 Web Search Support**: Introducing built-in web search capabilities via RAG API, allowing users to search using SearXNG, Google Programmatic Search Engine, Brave Search, serpstack, and serper. Activate it effortlessly by adding necessary variables from Document settings > Web Params.
|
||||
- **🗂️ Models Workspace**: Create and manage model presets for both Ollama/OpenAI API. Note: The old Modelfiles workspace is deprecated.
|
||||
- **🛠️ Model Builder Feature**: Build and edit all models with persistent builder mode.
|
||||
- **🏷️ Model Tagging Support**: Organize models with tagging features in the models workspace.
|
||||
- **📋 Model Ordering Support**: Effortlessly organize models by dragging and dropping them into the desired positions within the models workspace.
|
||||
- **📈 OpenAI Generation Stats**: Access detailed generation statistics for OpenAI models.
|
||||
- **📅 System Prompt Variables**: New variables added: '{{CURRENT_DATE}}' and '{{USER_NAME}}' for dynamic prompts.
|
||||
- **📢 Global Banner Support**: Manage global banners from admin settings > banners.
|
||||
- **🗃️ Enhanced Archived Chats Modal**: Search and export archived chats easily.
|
||||
- **📂 Archive All Button**: Quickly archive all chats from settings > chats.
|
||||
- **🌐 Improved Translations**: Added and improved translations for French, Croatian, Cebuano, and Vietnamese.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🔍 Archived Chats Visibility**: Resolved issue with archived chats not showing in the admin panel.
|
||||
- **💬 Message Styling**: Fixed styling issues affecting message appearance.
|
||||
- **🔗 Shared Chat Responses**: Corrected the issue where shared chat response messages were not readonly.
|
||||
- **🖥️ UI Enhancement**: Fixed the scrollbar overlapping issue with the message box in the user interface.
|
||||
|
||||
### Changed
|
||||
|
||||
- **💾 User Settings Storage**: User settings are now saved on the backend, ensuring consistency across all devices.
|
||||
- **📡 Unified API Requests**: The API request for getting models is now unified to '/api/models' for easier usage.
|
||||
- **🔄 Versioning Update**: Our versioning will now follow the format 0.x for major updates and 0.x.y for patches.
|
||||
- **📦 Export All Chats (All Users)**: Moved this functionality to the Admin Panel settings for better organization and accessibility.
|
||||
|
||||
### Removed
|
||||
|
||||
- **🚫 Bundled LiteLLM Support Deprecated**: Migrate your LiteLLM config.yaml to a self-hosted LiteLLM instance. LiteLLM can still be added via OpenAI Connections. Download the LiteLLM config.yaml from admin settings > database > export LiteLLM config.yaml.
|
||||
|
||||
## [0.1.125] - 2024-05-19
|
||||
|
||||
### Added
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contribute to a positive environment for our community include:
|
||||
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
|
||||
- Focusing on what is best not just for us as individuals, but for the overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
- The use of sexualized language or imagery, and sexual attention or advances of any kind
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email address, without their explicit permission
|
||||
- **Spamming of any kind**
|
||||
- Aggressive sales tactics targeting our community members are strictly prohibited. You can mention your product if it's relevant to the discussion, but under no circumstances should you push it forcefully
|
||||
- Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, spamming, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at hello@openwebui.com. All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Temporary Ban
|
||||
|
||||
**Community Impact**: Any violation of community standards, including but not limited to inappropriate language, unprofessional behavior, harassment, or spamming.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 2. Permanent Ban
|
||||
|
||||
**Community Impact**: Repeated or severe violations of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
+12
-10
@@ -11,12 +11,14 @@ ARG USE_CUDA_VER=cu121
|
||||
# IMPORTANT: If you change the embedding model (sentence-transformers/all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them.
|
||||
ARG USE_EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
|
||||
ARG USE_RERANKING_MODEL=""
|
||||
ARG BUILD_HASH=dev-build
|
||||
# Override at your own risk - non-root configurations are untested
|
||||
ARG UID=0
|
||||
ARG GID=0
|
||||
|
||||
######## WebUI frontend ########
|
||||
FROM --platform=$BUILDPLATFORM node:21-alpine3.19 as build
|
||||
ARG BUILD_HASH
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -24,6 +26,7 @@ COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
ENV APP_BUILD_HASH=${BUILD_HASH}
|
||||
RUN npm run build
|
||||
|
||||
######## WebUI backend ########
|
||||
@@ -59,11 +62,6 @@ ENV OPENAI_API_KEY="" \
|
||||
DO_NOT_TRACK=true \
|
||||
ANONYMIZED_TELEMETRY=false
|
||||
|
||||
# Use locally bundled version of the LiteLLM cost map json
|
||||
# to avoid repetitive startup connections
|
||||
ENV LITELLM_LOCAL_MODEL_COST_MAP="True"
|
||||
|
||||
|
||||
#### Other models #########################################################
|
||||
## whisper TTS model settings ##
|
||||
ENV WHISPER_MODEL="base" \
|
||||
@@ -83,10 +81,10 @@ WORKDIR /app/backend
|
||||
ENV HOME /root
|
||||
# Create user and group if not root
|
||||
RUN if [ $UID -ne 0 ]; then \
|
||||
if [ $GID -ne 0 ]; then \
|
||||
addgroup --gid $GID app; \
|
||||
fi; \
|
||||
adduser --uid $UID --gid $GID --home $HOME --disabled-password --no-create-home app; \
|
||||
if [ $GID -ne 0 ]; then \
|
||||
addgroup --gid $GID app; \
|
||||
fi; \
|
||||
adduser --uid $UID --gid $GID --home $HOME --disabled-password --no-create-home app; \
|
||||
fi
|
||||
|
||||
RUN mkdir -p $HOME/.cache/chroma
|
||||
@@ -132,7 +130,8 @@ RUN pip3 install uv && \
|
||||
uv pip install --system -r requirements.txt --no-cache-dir && \
|
||||
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
|
||||
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
|
||||
fi
|
||||
fi; \
|
||||
chown -R $UID:$GID /app/backend/data/
|
||||
|
||||
|
||||
|
||||
@@ -154,4 +153,7 @@ HEALTHCHECK CMD curl --silent --fail http://localhost:8080/health | jq -e '.stat
|
||||
|
||||
USER $UID:$GID
|
||||
|
||||
ARG BUILD_HASH
|
||||
ENV WEBUI_BUILD_VERSION=${BUILD_HASH}
|
||||
|
||||
CMD [ "bash", "start.sh"]
|
||||
|
||||
@@ -11,97 +11,47 @@
|
||||
[](https://discord.gg/5rJgQTnV4s)
|
||||
[](https://github.com/sponsors/tjbck)
|
||||
|
||||
Open WebUI is an extensible, feature-rich, and user-friendly self-hosted WebUI designed to operate entirely offline. It supports various LLM runners, including Ollama and OpenAI-compatible APIs. For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/).
|
||||
Open WebUI is an [extensible](https://github.com/open-webui/pipelines), feature-rich, and user-friendly self-hosted WebUI designed to operate entirely offline. It supports various LLM runners, including Ollama and OpenAI-compatible APIs. For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/).
|
||||
|
||||

|
||||
|
||||
## Features ⭐
|
||||
## Key Features of Open WebUI ⭐
|
||||
|
||||
- 🖥️ **Intuitive Interface**: Our chat interface takes inspiration from ChatGPT, ensuring a user-friendly experience.
|
||||
- 🚀 **Effortless Setup**: Install seamlessly using Docker or Kubernetes (kubectl, kustomize or helm) for a hassle-free experience with support for both `:ollama` and `:cuda` tagged images.
|
||||
|
||||
- 📱 **Responsive Design**: Enjoy a seamless experience on both desktop and mobile devices.
|
||||
- 🤝 **Ollama/OpenAI API Integration**: Effortlessly integrate OpenAI-compatible APIs for versatile conversations alongside Ollama models. Customize the OpenAI API URL to link with **LMStudio, GroqCloud, Mistral, OpenRouter, and more**.
|
||||
|
||||
- ⚡ **Swift Responsiveness**: Enjoy fast and responsive performance.
|
||||
- 🧩 **Pipelines, Open WebUI Plugin Support**: Seamlessly integrate custom logic and Python libraries into Open WebUI using [Pipelines Plugin Framework](https://github.com/open-webui/pipelines). Launch your Pipelines instance, set the OpenAI URL to the Pipelines URL, and explore endless possibilities. [Examples](https://github.com/open-webui/pipelines/tree/main/examples) include **Function Calling**, User **Rate Limiting** to control access, **Usage Monitoring** with tools like Langfuse, **Live Translation with LibreTranslate** for multilingual support, **Toxic Message Filtering** and much more.
|
||||
|
||||
- 🚀 **Effortless Setup**: Install seamlessly using Docker or Kubernetes (kubectl, kustomize or helm) for a hassle-free experience.
|
||||
- 📱 **Responsive Design**: Enjoy a seamless experience across Desktop PC, Laptop, and Mobile devices.
|
||||
|
||||
- 🌈 **Theme Customization**: Choose from a variety of themes to personalize your Open WebUI experience.
|
||||
|
||||
- 💻 **Code Syntax Highlighting**: Enjoy enhanced code readability with our syntax highlighting feature.
|
||||
- 📱 **Progressive Web App (PWA) for Mobile**: Enjoy a native app-like experience on your mobile device with our PWA, providing offline access on localhost and a seamless user interface.
|
||||
|
||||
- ✒️🔢 **Full Markdown and LaTeX Support**: Elevate your LLM experience with comprehensive Markdown and LaTeX capabilities for enriched interaction.
|
||||
|
||||
- 📚 **Local RAG Integration**: Dive into the future of chat interactions with the groundbreaking Retrieval Augmented Generation (RAG) support. This feature seamlessly integrates document interactions into your chat experience. You can load documents directly into the chat or add files to your document library, effortlessly accessing them using `#` command in the prompt. In its alpha phase, occasional issues may arise as we actively refine and enhance this feature to ensure optimal performance and reliability.
|
||||
- 🎤📹 **Hands-Free Voice/Video Call**: Experience seamless communication with integrated hands-free voice and video call features, allowing for a more dynamic and interactive chat environment.
|
||||
|
||||
- 🔍 **RAG Embedding Support**: Change the RAG embedding model directly in document settings, enhancing document processing. This feature supports Ollama and OpenAI models.
|
||||
- 🛠️ **Model Builder**: Easily create Ollama models via the Web UI. Create and add custom characters/agents, customize chat elements, and import models effortlessly through [Open WebUI Community](https://openwebui.com/) integration.
|
||||
|
||||
- 🌐 **Web Browsing Capability**: Seamlessly integrate websites into your chat experience using the `#` command followed by the URL. This feature allows you to incorporate web content directly into your conversations, enhancing the richness and depth of your interactions.
|
||||
- 🐍 **Native Python Function Calling Tool**: Enhance your LLMs with built-in code editor support in the tools workspace. Bring Your Own Function (BYOF) by simply adding your pure Python functions, enabling seamless integration with LLMs.
|
||||
|
||||
- 📜 **Prompt Preset Support**: Instantly access preset prompts using the `/` command in the chat input. Load predefined conversation starters effortlessly and expedite your interactions. Effortlessly import prompts through [Open WebUI Community](https://openwebui.com/) integration.
|
||||
- 📚 **Local RAG Integration**: Dive into the future of chat interactions with groundbreaking Retrieval Augmented Generation (RAG) support. This feature seamlessly integrates document interactions into your chat experience. You can load documents directly into the chat or add files to your document library, effortlessly accessing them using the `#` command before a query.
|
||||
|
||||
- 👍👎 **RLHF Annotation**: Empower your messages by rating them with thumbs up and thumbs down, followed by the option to provide textual feedback, facilitating the creation of datasets for Reinforcement Learning from Human Feedback (RLHF). Utilize your messages to train or fine-tune models, all while ensuring the confidentiality of locally saved data.
|
||||
- 🔍 **Web Search for RAG**: Perform web searches using providers like `SearXNG`, `Google PSE`, `Brave Search`, `serpstack`, `serper`, and `Serply` and inject the results directly into your chat experience.
|
||||
|
||||
- 🏷️ **Conversation Tagging**: Effortlessly categorize and locate specific chats for quick reference and streamlined data collection.
|
||||
- 🌐 **Web Browsing Capability**: Seamlessly integrate websites into your chat experience using the `#` command followed by a URL. This feature allows you to incorporate web content directly into your conversations, enhancing the richness and depth of your interactions.
|
||||
|
||||
- 📥🗑️ **Download/Delete Models**: Easily download or remove models directly from the web UI.
|
||||
|
||||
- 🔄 **Update All Ollama Models**: Easily update locally installed models all at once with a convenient button, streamlining model management.
|
||||
|
||||
- ⬆️ **GGUF File Model Creation**: Effortlessly create Ollama models by uploading GGUF files directly from the web UI. Streamlined process with options to upload from your machine or download GGUF files from Hugging Face.
|
||||
|
||||
- 🤖 **Multiple Model Support**: Seamlessly switch between different chat models for diverse interactions.
|
||||
|
||||
- 🔄 **Multi-Modal Support**: Seamlessly engage with models that support multimodal interactions, including images (e.g., LLava).
|
||||
|
||||
- 🧩 **Modelfile Builder**: Easily create Ollama modelfiles via the web UI. Create and add characters/agents, customize chat elements, and import modelfiles effortlessly through [Open WebUI Community](https://openwebui.com/) integration.
|
||||
- 🎨 **Image Generation Integration**: Seamlessly incorporate image generation capabilities using options such as AUTOMATIC1111 API or ComfyUI (local), and OpenAI's DALL-E (external), enriching your chat experience with dynamic visual content.
|
||||
|
||||
- ⚙️ **Many Models Conversations**: Effortlessly engage with various models simultaneously, harnessing their unique strengths for optimal responses. Enhance your experience by leveraging a diverse set of models in parallel.
|
||||
|
||||
- 💬 **Collaborative Chat**: Harness the collective intelligence of multiple models by seamlessly orchestrating group conversations. Use the `@` command to specify the model, enabling dynamic and diverse dialogues within your chat interface. Immerse yourself in the collective intelligence woven into your chat environment.
|
||||
|
||||
- 🗨️ **Local Chat Sharing**: Generate and share chat links seamlessly between users, enhancing collaboration and communication.
|
||||
|
||||
- 🔄 **Regeneration History Access**: Easily revisit and explore your entire regeneration history.
|
||||
|
||||
- 📜 **Chat History**: Effortlessly access and manage your conversation history.
|
||||
|
||||
- 📬 **Archive Chats**: Effortlessly store away completed conversations with LLMs for future reference, maintaining a tidy and clutter-free chat interface while allowing for easy retrieval and reference.
|
||||
|
||||
- 📤📥 **Import/Export Chat History**: Seamlessly move your chat data in and out of the platform.
|
||||
|
||||
- 🗣️ **Voice Input Support**: Engage with your model through voice interactions; enjoy the convenience of talking to your model directly. Additionally, explore the option for sending voice input automatically after 3 seconds of silence for a streamlined experience.
|
||||
|
||||
- 🔊 **Configurable Text-to-Speech Endpoint**: Customize your Text-to-Speech experience with configurable OpenAI endpoints.
|
||||
|
||||
- ⚙️ **Fine-Tuned Control with Advanced Parameters**: Gain a deeper level of control by adjusting parameters such as temperature and defining your system prompts to tailor the conversation to your specific preferences and needs.
|
||||
|
||||
- 🎨🤖 **Image Generation Integration**: Seamlessly incorporate image generation capabilities using options such as AUTOMATIC1111 API (local), ComfyUI (local), and DALL-E, enriching your chat experience with dynamic visual content.
|
||||
|
||||
- 🤝 **OpenAI API Integration**: Effortlessly integrate OpenAI-compatible API for versatile conversations alongside Ollama models. Customize the API Base URL to link with **LMStudio, Mistral, OpenRouter, and more**.
|
||||
|
||||
- ✨ **Multiple OpenAI-Compatible API Support**: Seamlessly integrate and customize various OpenAI-compatible APIs, enhancing the versatility of your chat interactions.
|
||||
|
||||
- 🔑 **API Key Generation Support**: Generate secret keys to leverage Open WebUI with OpenAI libraries, simplifying integration and development.
|
||||
|
||||
- 🔗 **External Ollama Server Connection**: Seamlessly link to an external Ollama server hosted on a different address by configuring the environment variable.
|
||||
|
||||
- 🔀 **Multiple Ollama Instance Load Balancing**: Effortlessly distribute chat requests across multiple Ollama instances for enhanced performance and reliability.
|
||||
|
||||
- 👥 **Multi-User Management**: Easily oversee and administer users via our intuitive admin panel, streamlining user management processes.
|
||||
|
||||
- 🔗 **Webhook Integration**: Subscribe to new user sign-up events via webhook (compatible with Google Chat and Microsoft Teams), providing real-time notifications and automation capabilities.
|
||||
|
||||
- 🛡️ **Model Whitelisting**: Admins can whitelist models for users with the 'user' role, enhancing security and access control.
|
||||
|
||||
- 📧 **Trusted Email Authentication**: Authenticate using a trusted email header, adding an additional layer of security and authentication.
|
||||
|
||||
- 🔐 **Role-Based Access Control (RBAC)**: Ensure secure access with restricted permissions; only authorized individuals can access your Ollama, and exclusive model creation/pulling rights are reserved for administrators.
|
||||
|
||||
- 🔒 **Backend Reverse Proxy Support**: Bolster security through direct communication between Open WebUI backend and Ollama. This key feature eliminates the need to expose Ollama over LAN. Requests made to the '/ollama/api' route from the web UI are seamlessly redirected to Ollama from the backend, enhancing overall system security.
|
||||
|
||||
- 🌐🌍 **Multilingual Support**: Experience Open WebUI in your preferred language with our internationalization (i18n) support. Join us in expanding our supported languages! We're actively seeking contributors!
|
||||
|
||||
- 🌟 **Continuous Updates**: We are committed to improving Open WebUI with regular updates and new features.
|
||||
- 🌟 **Continuous Updates**: We are committed to improving Open WebUI with regular updates, fixes, and new features.
|
||||
|
||||
Want to learn more about Open WebUI's features? Check out our [Open WebUI documentation](https://docs.openwebui.com/features) for a comprehensive overview!
|
||||
|
||||
## 🔗 Also Check Out Open WebUI Community!
|
||||
|
||||
@@ -200,10 +150,19 @@ docker run --rm --volume /var/run/docker.sock:/var/run/docker.sock containrrr/wa
|
||||
|
||||
In the last part of the command, replace `open-webui` with your container name if it is different.
|
||||
|
||||
### Moving from Ollama WebUI to Open WebUI
|
||||
|
||||
Check our Migration Guide available in our [Open WebUI Documentation](https://docs.openwebui.com/migration/).
|
||||
|
||||
### Using the Dev Branch 🌙
|
||||
|
||||
> [!WARNING]
|
||||
> The `:dev` branch contains the latest unstable features and changes. Use it at your own risk as it may have bugs or incomplete features.
|
||||
|
||||
If you want to try out the latest bleeding-edge features and are okay with occasional instability, you can use the `:dev` tag like this:
|
||||
|
||||
```bash
|
||||
docker run -d -p 3000:8080 -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:dev
|
||||
```
|
||||
|
||||
## What's Next? 🌟
|
||||
|
||||
Discover upcoming features on our roadmap in the [Open WebUI Documentation](https://docs.openwebui.com/roadmap/).
|
||||
|
||||
+207
-59
@@ -17,13 +17,12 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
from faster_whisper import WhisperModel
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
import uuid
|
||||
import requests
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
|
||||
from constants import ERROR_MESSAGES
|
||||
from utils.utils import (
|
||||
decode_token,
|
||||
@@ -41,10 +40,15 @@ from config import (
|
||||
WHISPER_MODEL_DIR,
|
||||
WHISPER_MODEL_AUTO_UPDATE,
|
||||
DEVICE_TYPE,
|
||||
AUDIO_OPENAI_API_BASE_URL,
|
||||
AUDIO_OPENAI_API_KEY,
|
||||
AUDIO_OPENAI_API_MODEL,
|
||||
AUDIO_OPENAI_API_VOICE,
|
||||
AUDIO_STT_OPENAI_API_BASE_URL,
|
||||
AUDIO_STT_OPENAI_API_KEY,
|
||||
AUDIO_TTS_OPENAI_API_BASE_URL,
|
||||
AUDIO_TTS_OPENAI_API_KEY,
|
||||
AUDIO_STT_ENGINE,
|
||||
AUDIO_STT_MODEL,
|
||||
AUDIO_TTS_ENGINE,
|
||||
AUDIO_TTS_MODEL,
|
||||
AUDIO_TTS_VOICE,
|
||||
AppConfig,
|
||||
)
|
||||
|
||||
@@ -61,10 +65,17 @@ app.add_middleware(
|
||||
)
|
||||
|
||||
app.state.config = AppConfig()
|
||||
app.state.config.OPENAI_API_BASE_URL = AUDIO_OPENAI_API_BASE_URL
|
||||
app.state.config.OPENAI_API_KEY = AUDIO_OPENAI_API_KEY
|
||||
app.state.config.OPENAI_API_MODEL = AUDIO_OPENAI_API_MODEL
|
||||
app.state.config.OPENAI_API_VOICE = AUDIO_OPENAI_API_VOICE
|
||||
|
||||
app.state.config.STT_OPENAI_API_BASE_URL = AUDIO_STT_OPENAI_API_BASE_URL
|
||||
app.state.config.STT_OPENAI_API_KEY = AUDIO_STT_OPENAI_API_KEY
|
||||
app.state.config.STT_ENGINE = AUDIO_STT_ENGINE
|
||||
app.state.config.STT_MODEL = AUDIO_STT_MODEL
|
||||
|
||||
app.state.config.TTS_OPENAI_API_BASE_URL = AUDIO_TTS_OPENAI_API_BASE_URL
|
||||
app.state.config.TTS_OPENAI_API_KEY = AUDIO_TTS_OPENAI_API_KEY
|
||||
app.state.config.TTS_ENGINE = AUDIO_TTS_ENGINE
|
||||
app.state.config.TTS_MODEL = AUDIO_TTS_MODEL
|
||||
app.state.config.TTS_VOICE = AUDIO_TTS_VOICE
|
||||
|
||||
# setting device type for whisper model
|
||||
whisper_device_type = DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == "cuda" else "cpu"
|
||||
@@ -74,41 +85,101 @@ SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/")
|
||||
SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
class OpenAIConfigUpdateForm(BaseModel):
|
||||
url: str
|
||||
key: str
|
||||
model: str
|
||||
speaker: str
|
||||
class TTSConfigForm(BaseModel):
|
||||
OPENAI_API_BASE_URL: str
|
||||
OPENAI_API_KEY: str
|
||||
ENGINE: str
|
||||
MODEL: str
|
||||
VOICE: str
|
||||
|
||||
|
||||
class STTConfigForm(BaseModel):
|
||||
OPENAI_API_BASE_URL: str
|
||||
OPENAI_API_KEY: str
|
||||
ENGINE: str
|
||||
MODEL: str
|
||||
|
||||
|
||||
class AudioConfigUpdateForm(BaseModel):
|
||||
tts: TTSConfigForm
|
||||
stt: STTConfigForm
|
||||
|
||||
|
||||
from pydub import AudioSegment
|
||||
from pydub.utils import mediainfo
|
||||
|
||||
|
||||
def is_mp4_audio(file_path):
|
||||
"""Check if the given file is an MP4 audio file."""
|
||||
if not os.path.isfile(file_path):
|
||||
print(f"File not found: {file_path}")
|
||||
return False
|
||||
|
||||
info = mediainfo(file_path)
|
||||
if (
|
||||
info.get("codec_name") == "aac"
|
||||
and info.get("codec_type") == "audio"
|
||||
and info.get("codec_tag_string") == "mp4a"
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def convert_mp4_to_wav(file_path, output_path):
|
||||
"""Convert MP4 audio file to WAV format."""
|
||||
audio = AudioSegment.from_file(file_path, format="mp4")
|
||||
audio.export(output_path, format="wav")
|
||||
print(f"Converted {file_path} to {output_path}")
|
||||
|
||||
|
||||
@app.get("/config")
|
||||
async def get_openai_config(user=Depends(get_admin_user)):
|
||||
async def get_audio_config(user=Depends(get_admin_user)):
|
||||
return {
|
||||
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
|
||||
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
|
||||
"OPENAI_API_MODEL": app.state.config.OPENAI_API_MODEL,
|
||||
"OPENAI_API_VOICE": app.state.config.OPENAI_API_VOICE,
|
||||
"tts": {
|
||||
"OPENAI_API_BASE_URL": app.state.config.TTS_OPENAI_API_BASE_URL,
|
||||
"OPENAI_API_KEY": app.state.config.TTS_OPENAI_API_KEY,
|
||||
"ENGINE": app.state.config.TTS_ENGINE,
|
||||
"MODEL": app.state.config.TTS_MODEL,
|
||||
"VOICE": app.state.config.TTS_VOICE,
|
||||
},
|
||||
"stt": {
|
||||
"OPENAI_API_BASE_URL": app.state.config.STT_OPENAI_API_BASE_URL,
|
||||
"OPENAI_API_KEY": app.state.config.STT_OPENAI_API_KEY,
|
||||
"ENGINE": app.state.config.STT_ENGINE,
|
||||
"MODEL": app.state.config.STT_MODEL,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@app.post("/config/update")
|
||||
async def update_openai_config(
|
||||
form_data: OpenAIConfigUpdateForm, user=Depends(get_admin_user)
|
||||
async def update_audio_config(
|
||||
form_data: AudioConfigUpdateForm, user=Depends(get_admin_user)
|
||||
):
|
||||
if form_data.key == "":
|
||||
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
|
||||
app.state.config.TTS_OPENAI_API_BASE_URL = form_data.tts.OPENAI_API_BASE_URL
|
||||
app.state.config.TTS_OPENAI_API_KEY = form_data.tts.OPENAI_API_KEY
|
||||
app.state.config.TTS_ENGINE = form_data.tts.ENGINE
|
||||
app.state.config.TTS_MODEL = form_data.tts.MODEL
|
||||
app.state.config.TTS_VOICE = form_data.tts.VOICE
|
||||
|
||||
app.state.config.OPENAI_API_BASE_URL = form_data.url
|
||||
app.state.config.OPENAI_API_KEY = form_data.key
|
||||
app.state.config.OPENAI_API_MODEL = form_data.model
|
||||
app.state.config.OPENAI_API_VOICE = form_data.speaker
|
||||
app.state.config.STT_OPENAI_API_BASE_URL = form_data.stt.OPENAI_API_BASE_URL
|
||||
app.state.config.STT_OPENAI_API_KEY = form_data.stt.OPENAI_API_KEY
|
||||
app.state.config.STT_ENGINE = form_data.stt.ENGINE
|
||||
app.state.config.STT_MODEL = form_data.stt.MODEL
|
||||
|
||||
return {
|
||||
"status": True,
|
||||
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
|
||||
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
|
||||
"OPENAI_API_MODEL": app.state.config.OPENAI_API_MODEL,
|
||||
"OPENAI_API_VOICE": app.state.config.OPENAI_API_VOICE,
|
||||
"tts": {
|
||||
"OPENAI_API_BASE_URL": app.state.config.TTS_OPENAI_API_BASE_URL,
|
||||
"OPENAI_API_KEY": app.state.config.TTS_OPENAI_API_KEY,
|
||||
"ENGINE": app.state.config.TTS_ENGINE,
|
||||
"MODEL": app.state.config.TTS_MODEL,
|
||||
"VOICE": app.state.config.TTS_VOICE,
|
||||
},
|
||||
"stt": {
|
||||
"OPENAI_API_BASE_URL": app.state.config.STT_OPENAI_API_BASE_URL,
|
||||
"OPENAI_API_KEY": app.state.config.STT_OPENAI_API_KEY,
|
||||
"ENGINE": app.state.config.STT_ENGINE,
|
||||
"MODEL": app.state.config.STT_MODEL,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -125,13 +196,21 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
||||
return FileResponse(file_path)
|
||||
|
||||
headers = {}
|
||||
headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEY}"
|
||||
headers["Authorization"] = f"Bearer {app.state.config.TTS_OPENAI_API_KEY}"
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
try:
|
||||
body = body.decode("utf-8")
|
||||
body = json.loads(body)
|
||||
body["model"] = app.state.config.TTS_MODEL
|
||||
body = json.dumps(body).encode("utf-8")
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
r = None
|
||||
try:
|
||||
r = requests.post(
|
||||
url=f"{app.state.config.OPENAI_API_BASE_URL}/audio/speech",
|
||||
url=f"{app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech",
|
||||
data=body,
|
||||
headers=headers,
|
||||
stream=True,
|
||||
@@ -181,41 +260,110 @@ def transcribe(
|
||||
)
|
||||
|
||||
try:
|
||||
filename = file.filename
|
||||
file_path = f"{UPLOAD_DIR}/{filename}"
|
||||
ext = file.filename.split(".")[-1]
|
||||
|
||||
id = uuid.uuid4()
|
||||
filename = f"{id}.{ext}"
|
||||
|
||||
file_dir = f"{CACHE_DIR}/audio/transcriptions"
|
||||
os.makedirs(file_dir, exist_ok=True)
|
||||
file_path = f"{file_dir}/{filename}"
|
||||
|
||||
print(filename)
|
||||
|
||||
contents = file.file.read()
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(contents)
|
||||
f.close()
|
||||
|
||||
whisper_kwargs = {
|
||||
"model_size_or_path": WHISPER_MODEL,
|
||||
"device": whisper_device_type,
|
||||
"compute_type": "int8",
|
||||
"download_root": WHISPER_MODEL_DIR,
|
||||
"local_files_only": not WHISPER_MODEL_AUTO_UPDATE,
|
||||
}
|
||||
if app.state.config.STT_ENGINE == "":
|
||||
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}")
|
||||
log.debug(f"whisper_kwargs: {whisper_kwargs}")
|
||||
|
||||
try:
|
||||
model = WhisperModel(**whisper_kwargs)
|
||||
except:
|
||||
log.warning(
|
||||
"WhisperModel initialization failed, attempting download with local_files_only=False"
|
||||
try:
|
||||
model = WhisperModel(**whisper_kwargs)
|
||||
except:
|
||||
log.warning(
|
||||
"WhisperModel initialization failed, attempting download with local_files_only=False"
|
||||
)
|
||||
whisper_kwargs["local_files_only"] = False
|
||||
model = WhisperModel(**whisper_kwargs)
|
||||
|
||||
segments, info = model.transcribe(file_path, beam_size=5)
|
||||
log.info(
|
||||
"Detected language '%s' with probability %f"
|
||||
% (info.language, info.language_probability)
|
||||
)
|
||||
whisper_kwargs["local_files_only"] = False
|
||||
model = WhisperModel(**whisper_kwargs)
|
||||
|
||||
segments, info = model.transcribe(file_path, beam_size=5)
|
||||
log.info(
|
||||
"Detected language '%s' with probability %f"
|
||||
% (info.language, info.language_probability)
|
||||
)
|
||||
transcript = "".join([segment.text for segment in list(segments)])
|
||||
|
||||
transcript = "".join([segment.text for segment in list(segments)])
|
||||
data = {"text": transcript.strip()}
|
||||
|
||||
return {"text": transcript.strip()}
|
||||
# save the transcript to a json file
|
||||
transcript_file = f"{file_dir}/{id}.json"
|
||||
with open(transcript_file, "w") as f:
|
||||
json.dump(data, f)
|
||||
|
||||
print(data)
|
||||
|
||||
return data
|
||||
|
||||
elif app.state.config.STT_ENGINE == "openai":
|
||||
if is_mp4_audio(file_path):
|
||||
print("is_mp4_audio")
|
||||
os.rename(file_path, file_path.replace(".wav", ".mp4"))
|
||||
# Convert MP4 audio file to WAV format
|
||||
convert_mp4_to_wav(file_path.replace(".wav", ".mp4"), file_path)
|
||||
|
||||
headers = {"Authorization": f"Bearer {app.state.config.STT_OPENAI_API_KEY}"}
|
||||
|
||||
files = {"file": (filename, open(file_path, "rb"))}
|
||||
data = {"model": "whisper-1"}
|
||||
|
||||
print(files, data)
|
||||
|
||||
r = None
|
||||
try:
|
||||
r = requests.post(
|
||||
url=f"{app.state.config.STT_OPENAI_API_BASE_URL}/audio/transcriptions",
|
||||
headers=headers,
|
||||
files=files,
|
||||
data=data,
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
data = r.json()
|
||||
|
||||
# save the transcript to a json file
|
||||
transcript_file = f"{file_dir}/{id}.json"
|
||||
with open(transcript_file, "w") as f:
|
||||
json.dump(data, f)
|
||||
|
||||
print(data)
|
||||
return data
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"External: {res['error']['message']}"
|
||||
except:
|
||||
error_detail = f"External: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r != None else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
||||
@@ -1,379 +0,0 @@
|
||||
import sys
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI, Depends, HTTPException
|
||||
from fastapi.routing import APIRoute
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
import logging
|
||||
from fastapi import FastAPI, Request, Depends, status, Response
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
||||
from starlette.responses import StreamingResponse
|
||||
import json
|
||||
import time
|
||||
import requests
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing import Optional, List
|
||||
|
||||
from utils.utils import get_verified_user, get_current_user, get_admin_user
|
||||
from config import SRC_LOG_LEVELS, ENV
|
||||
from constants import MESSAGES
|
||||
|
||||
import os
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["LITELLM"])
|
||||
|
||||
|
||||
from config import (
|
||||
ENABLE_LITELLM,
|
||||
ENABLE_MODEL_FILTER,
|
||||
MODEL_FILTER_LIST,
|
||||
DATA_DIR,
|
||||
LITELLM_PROXY_PORT,
|
||||
LITELLM_PROXY_HOST,
|
||||
)
|
||||
|
||||
import warnings
|
||||
|
||||
warnings.simplefilter("ignore")
|
||||
|
||||
from litellm.utils import get_llm_provider
|
||||
|
||||
import asyncio
|
||||
import subprocess
|
||||
import yaml
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
log.info("startup_event")
|
||||
# TODO: Check config.yaml file and create one
|
||||
asyncio.create_task(start_litellm_background())
|
||||
yield
|
||||
|
||||
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
|
||||
origins = ["*"]
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
LITELLM_CONFIG_DIR = f"{DATA_DIR}/litellm/config.yaml"
|
||||
|
||||
with open(LITELLM_CONFIG_DIR, "r") as file:
|
||||
litellm_config = yaml.safe_load(file)
|
||||
|
||||
|
||||
app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER.value
|
||||
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST.value
|
||||
|
||||
|
||||
app.state.ENABLE = ENABLE_LITELLM
|
||||
app.state.CONFIG = litellm_config
|
||||
|
||||
# Global variable to store the subprocess reference
|
||||
background_process = None
|
||||
|
||||
CONFLICT_ENV_VARS = [
|
||||
# Uvicorn uses PORT, so LiteLLM might use it as well
|
||||
"PORT",
|
||||
# LiteLLM uses DATABASE_URL for Prisma connections
|
||||
"DATABASE_URL",
|
||||
]
|
||||
|
||||
|
||||
async def run_background_process(command):
|
||||
global background_process
|
||||
log.info("run_background_process")
|
||||
|
||||
try:
|
||||
# Log the command to be executed
|
||||
log.info(f"Executing command: {command}")
|
||||
# Filter environment variables known to conflict with litellm
|
||||
env = {k: v for k, v in os.environ.items() if k not in CONFLICT_ENV_VARS}
|
||||
# Execute the command and create a subprocess
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env
|
||||
)
|
||||
background_process = process
|
||||
log.info("Subprocess started successfully.")
|
||||
|
||||
# Capture STDERR for debugging purposes
|
||||
stderr_output = await process.stderr.read()
|
||||
stderr_text = stderr_output.decode().strip()
|
||||
if stderr_text:
|
||||
log.info(f"Subprocess STDERR: {stderr_text}")
|
||||
|
||||
# log.info output line by line
|
||||
async for line in process.stdout:
|
||||
log.info(line.decode().strip())
|
||||
|
||||
# Wait for the process to finish
|
||||
returncode = await process.wait()
|
||||
log.info(f"Subprocess exited with return code {returncode}")
|
||||
except Exception as e:
|
||||
log.error(f"Failed to start subprocess: {e}")
|
||||
raise # Optionally re-raise the exception if you want it to propagate
|
||||
|
||||
|
||||
async def start_litellm_background():
|
||||
log.info("start_litellm_background")
|
||||
# Command to run in the background
|
||||
command = [
|
||||
"litellm",
|
||||
"--port",
|
||||
str(LITELLM_PROXY_PORT),
|
||||
"--host",
|
||||
LITELLM_PROXY_HOST,
|
||||
"--telemetry",
|
||||
"False",
|
||||
"--config",
|
||||
LITELLM_CONFIG_DIR,
|
||||
]
|
||||
|
||||
await run_background_process(command)
|
||||
|
||||
|
||||
async def shutdown_litellm_background():
|
||||
log.info("shutdown_litellm_background")
|
||||
global background_process
|
||||
if background_process:
|
||||
background_process.terminate()
|
||||
await background_process.wait() # Ensure the process has terminated
|
||||
log.info("Subprocess terminated")
|
||||
background_process = None
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def get_status():
|
||||
return {"status": True}
|
||||
|
||||
|
||||
async def restart_litellm():
|
||||
"""
|
||||
Endpoint to restart the litellm background service.
|
||||
"""
|
||||
log.info("Requested restart of litellm service.")
|
||||
try:
|
||||
# Shut down the existing process if it is running
|
||||
await shutdown_litellm_background()
|
||||
log.info("litellm service shutdown complete.")
|
||||
|
||||
# Restart the background service
|
||||
|
||||
asyncio.create_task(start_litellm_background())
|
||||
log.info("litellm service restart complete.")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "litellm service restarted successfully.",
|
||||
}
|
||||
except Exception as e:
|
||||
log.info(f"Error restarting litellm service: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
|
||||
)
|
||||
|
||||
|
||||
@app.get("/restart")
|
||||
async def restart_litellm_handler(user=Depends(get_admin_user)):
|
||||
return await restart_litellm()
|
||||
|
||||
|
||||
@app.get("/config")
|
||||
async def get_config(user=Depends(get_admin_user)):
|
||||
return app.state.CONFIG
|
||||
|
||||
|
||||
class LiteLLMConfigForm(BaseModel):
|
||||
general_settings: Optional[dict] = None
|
||||
litellm_settings: Optional[dict] = None
|
||||
model_list: Optional[List[dict]] = None
|
||||
router_settings: Optional[dict] = None
|
||||
|
||||
model_config = ConfigDict(protected_namespaces=())
|
||||
|
||||
|
||||
@app.post("/config/update")
|
||||
async def update_config(form_data: LiteLLMConfigForm, user=Depends(get_admin_user)):
|
||||
app.state.CONFIG = form_data.model_dump(exclude_none=True)
|
||||
|
||||
with open(LITELLM_CONFIG_DIR, "w") as file:
|
||||
yaml.dump(app.state.CONFIG, file)
|
||||
|
||||
await restart_litellm()
|
||||
return app.state.CONFIG
|
||||
|
||||
|
||||
@app.get("/models")
|
||||
@app.get("/v1/models")
|
||||
async def get_models(user=Depends(get_current_user)):
|
||||
|
||||
if app.state.ENABLE:
|
||||
while not background_process:
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
url = f"http://localhost:{LITELLM_PROXY_PORT}/v1"
|
||||
r = None
|
||||
try:
|
||||
r = requests.request(method="GET", url=f"{url}/models")
|
||||
r.raise_for_status()
|
||||
|
||||
data = r.json()
|
||||
|
||||
if app.state.ENABLE_MODEL_FILTER:
|
||||
if user and user.role == "user":
|
||||
data["data"] = list(
|
||||
filter(
|
||||
lambda model: model["id"] in app.state.MODEL_FILTER_LIST,
|
||||
data["data"],
|
||||
)
|
||||
)
|
||||
|
||||
return data
|
||||
except Exception as e:
|
||||
|
||||
log.exception(e)
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"External: {res['error']}"
|
||||
except:
|
||||
error_detail = f"External: {e}"
|
||||
|
||||
return {
|
||||
"data": [
|
||||
{
|
||||
"id": model["model_name"],
|
||||
"object": "model",
|
||||
"created": int(time.time()),
|
||||
"owned_by": "openai",
|
||||
}
|
||||
for model in app.state.CONFIG["model_list"]
|
||||
],
|
||||
"object": "list",
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"data": [],
|
||||
"object": "list",
|
||||
}
|
||||
|
||||
|
||||
@app.get("/model/info")
|
||||
async def get_model_list(user=Depends(get_admin_user)):
|
||||
return {"data": app.state.CONFIG["model_list"]}
|
||||
|
||||
|
||||
class AddLiteLLMModelForm(BaseModel):
|
||||
model_name: str
|
||||
litellm_params: dict
|
||||
|
||||
model_config = ConfigDict(protected_namespaces=())
|
||||
|
||||
|
||||
@app.post("/model/new")
|
||||
async def add_model_to_config(
|
||||
form_data: AddLiteLLMModelForm, user=Depends(get_admin_user)
|
||||
):
|
||||
try:
|
||||
get_llm_provider(model=form_data.model_name)
|
||||
app.state.CONFIG["model_list"].append(form_data.model_dump())
|
||||
|
||||
with open(LITELLM_CONFIG_DIR, "w") as file:
|
||||
yaml.dump(app.state.CONFIG, file)
|
||||
|
||||
await restart_litellm()
|
||||
|
||||
return {"message": MESSAGES.MODEL_ADDED(form_data.model_name)}
|
||||
except Exception as e:
|
||||
print(e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
|
||||
)
|
||||
|
||||
|
||||
class DeleteLiteLLMModelForm(BaseModel):
|
||||
id: str
|
||||
|
||||
|
||||
@app.post("/model/delete")
|
||||
async def delete_model_from_config(
|
||||
form_data: DeleteLiteLLMModelForm, user=Depends(get_admin_user)
|
||||
):
|
||||
app.state.CONFIG["model_list"] = [
|
||||
model
|
||||
for model in app.state.CONFIG["model_list"]
|
||||
if model["model_name"] != form_data.id
|
||||
]
|
||||
|
||||
with open(LITELLM_CONFIG_DIR, "w") as file:
|
||||
yaml.dump(app.state.CONFIG, file)
|
||||
|
||||
await restart_litellm()
|
||||
|
||||
return {"message": MESSAGES.MODEL_DELETED(form_data.id)}
|
||||
|
||||
|
||||
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
||||
async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
||||
body = await request.body()
|
||||
|
||||
url = f"http://localhost:{LITELLM_PROXY_PORT}"
|
||||
|
||||
target_url = f"{url}/{path}"
|
||||
|
||||
headers = {}
|
||||
# headers["Authorization"] = f"Bearer {key}"
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
r = None
|
||||
|
||||
try:
|
||||
r = requests.request(
|
||||
method=request.method,
|
||||
url=target_url,
|
||||
data=body,
|
||||
headers=headers,
|
||||
stream=True,
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
# Check if response is SSE
|
||||
if "text/event-stream" in r.headers.get("Content-Type", ""):
|
||||
return StreamingResponse(
|
||||
r.iter_content(chunk_size=8192),
|
||||
status_code=r.status_code,
|
||||
headers=dict(r.headers),
|
||||
)
|
||||
else:
|
||||
response_data = r.json()
|
||||
return response_data
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
|
||||
except:
|
||||
error_detail = f"External: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500, detail=error_detail
|
||||
)
|
||||
+317
-415
@@ -29,8 +29,10 @@ import time
|
||||
from urllib.parse import urlparse
|
||||
from typing import Optional, List, Union
|
||||
|
||||
from starlette.background import BackgroundTask
|
||||
|
||||
from apps.web.models.users import Users
|
||||
from apps.webui.models.models import Models
|
||||
from apps.webui.models.users import Users
|
||||
from constants import ERROR_MESSAGES
|
||||
from utils.utils import (
|
||||
decode_token,
|
||||
@@ -43,6 +45,7 @@ from utils.utils import (
|
||||
from config import (
|
||||
SRC_LOG_LEVELS,
|
||||
OLLAMA_BASE_URLS,
|
||||
ENABLE_OLLAMA_API,
|
||||
ENABLE_MODEL_FILTER,
|
||||
MODEL_FILTER_LIST,
|
||||
UPLOAD_DIR,
|
||||
@@ -67,13 +70,11 @@ app.state.config = AppConfig()
|
||||
app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
|
||||
app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
||||
|
||||
app.state.config.ENABLE_OLLAMA_API = ENABLE_OLLAMA_API
|
||||
app.state.config.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
|
||||
app.state.MODELS = {}
|
||||
|
||||
|
||||
REQUEST_POOL = []
|
||||
|
||||
|
||||
# TODO: Implement a more intelligent load balancing mechanism for distributing requests among multiple backend instances.
|
||||
# Current implementation uses a simple round-robin approach (random.choice). Consider incorporating algorithms like weighted round-robin,
|
||||
# least connections, or least response time for better resource utilization and performance optimization.
|
||||
@@ -96,6 +97,21 @@ async def get_status():
|
||||
return {"status": True}
|
||||
|
||||
|
||||
@app.get("/config")
|
||||
async def get_config(user=Depends(get_admin_user)):
|
||||
return {"ENABLE_OLLAMA_API": app.state.config.ENABLE_OLLAMA_API}
|
||||
|
||||
|
||||
class OllamaConfigForm(BaseModel):
|
||||
enable_ollama_api: Optional[bool] = None
|
||||
|
||||
|
||||
@app.post("/config/update")
|
||||
async def update_config(form_data: OllamaConfigForm, user=Depends(get_admin_user)):
|
||||
app.state.config.ENABLE_OLLAMA_API = form_data.enable_ollama_api
|
||||
return {"ENABLE_OLLAMA_API": app.state.config.ENABLE_OLLAMA_API}
|
||||
|
||||
|
||||
@app.get("/urls")
|
||||
async def get_ollama_api_urls(user=Depends(get_admin_user)):
|
||||
return {"OLLAMA_BASE_URLS": app.state.config.OLLAMA_BASE_URLS}
|
||||
@@ -113,20 +129,10 @@ async def update_ollama_api_url(form_data: UrlUpdateForm, user=Depends(get_admin
|
||||
return {"OLLAMA_BASE_URLS": app.state.config.OLLAMA_BASE_URLS}
|
||||
|
||||
|
||||
@app.get("/cancel/{request_id}")
|
||||
async def cancel_ollama_request(request_id: str, user=Depends(get_current_user)):
|
||||
if user:
|
||||
if request_id in REQUEST_POOL:
|
||||
REQUEST_POOL.remove(request_id)
|
||||
return True
|
||||
else:
|
||||
raise HTTPException(status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED)
|
||||
|
||||
|
||||
async def fetch_url(url):
|
||||
timeout = aiohttp.ClientTimeout(total=5)
|
||||
try:
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
|
||||
async with session.get(url) as response:
|
||||
return await response.json()
|
||||
except Exception as e:
|
||||
@@ -135,6 +141,45 @@ async def fetch_url(url):
|
||||
return None
|
||||
|
||||
|
||||
async def cleanup_response(
|
||||
response: Optional[aiohttp.ClientResponse],
|
||||
session: Optional[aiohttp.ClientSession],
|
||||
):
|
||||
if response:
|
||||
response.close()
|
||||
if session:
|
||||
await session.close()
|
||||
|
||||
|
||||
async def post_streaming_url(url: str, payload: str):
|
||||
r = None
|
||||
try:
|
||||
session = aiohttp.ClientSession(trust_env=True)
|
||||
r = await session.post(url, data=payload)
|
||||
r.raise_for_status()
|
||||
|
||||
return StreamingResponse(
|
||||
r.content,
|
||||
status_code=r.status,
|
||||
headers=dict(r.headers),
|
||||
background=BackgroundTask(cleanup_response, response=r, session=session),
|
||||
)
|
||||
except Exception as e:
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = await r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status if r else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
|
||||
|
||||
def merge_models_lists(model_lists):
|
||||
merged_models = {}
|
||||
|
||||
@@ -156,14 +201,23 @@ def merge_models_lists(model_lists):
|
||||
|
||||
async def get_all_models():
|
||||
log.info("get_all_models()")
|
||||
tasks = [fetch_url(f"{url}/api/tags") for url in app.state.config.OLLAMA_BASE_URLS]
|
||||
responses = await asyncio.gather(*tasks)
|
||||
|
||||
models = {
|
||||
"models": merge_models_lists(
|
||||
map(lambda response: response["models"] if response else None, responses)
|
||||
)
|
||||
}
|
||||
if app.state.config.ENABLE_OLLAMA_API:
|
||||
tasks = [
|
||||
fetch_url(f"{url}/api/tags") for url in app.state.config.OLLAMA_BASE_URLS
|
||||
]
|
||||
responses = await asyncio.gather(*tasks)
|
||||
|
||||
models = {
|
||||
"models": merge_models_lists(
|
||||
map(
|
||||
lambda response: response["models"] if response else None, responses
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
else:
|
||||
models = {"models": []}
|
||||
|
||||
app.state.MODELS = {model["model"]: model for model in models["models"]}
|
||||
|
||||
@@ -191,6 +245,8 @@ async def get_ollama_tags(
|
||||
return models
|
||||
else:
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
|
||||
r = None
|
||||
try:
|
||||
r = requests.request(method="GET", url=f"{url}/api/tags")
|
||||
r.raise_for_status()
|
||||
@@ -216,52 +272,57 @@ async def get_ollama_tags(
|
||||
@app.get("/api/version")
|
||||
@app.get("/api/version/{url_idx}")
|
||||
async def get_ollama_versions(url_idx: Optional[int] = None):
|
||||
if app.state.config.ENABLE_OLLAMA_API:
|
||||
if url_idx == None:
|
||||
|
||||
if url_idx == None:
|
||||
# returns lowest version
|
||||
tasks = [
|
||||
fetch_url(f"{url}/api/version")
|
||||
for url in app.state.config.OLLAMA_BASE_URLS
|
||||
]
|
||||
responses = await asyncio.gather(*tasks)
|
||||
responses = list(filter(lambda x: x is not None, responses))
|
||||
|
||||
# returns lowest version
|
||||
tasks = [
|
||||
fetch_url(f"{url}/api/version") for url in app.state.config.OLLAMA_BASE_URLS
|
||||
]
|
||||
responses = await asyncio.gather(*tasks)
|
||||
responses = list(filter(lambda x: x is not None, responses))
|
||||
if len(responses) > 0:
|
||||
lowest_version = min(
|
||||
responses,
|
||||
key=lambda x: tuple(
|
||||
map(int, re.sub(r"^v|-.*", "", x["version"]).split("."))
|
||||
),
|
||||
)
|
||||
|
||||
if len(responses) > 0:
|
||||
lowest_version = min(
|
||||
responses,
|
||||
key=lambda x: tuple(
|
||||
map(int, re.sub(r"^v|-.*", "", x["version"]).split("."))
|
||||
),
|
||||
)
|
||||
|
||||
return {"version": lowest_version["version"]}
|
||||
return {"version": lowest_version["version"]}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=ERROR_MESSAGES.OLLAMA_NOT_FOUND,
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=ERROR_MESSAGES.OLLAMA_NOT_FOUND,
|
||||
)
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
|
||||
r = None
|
||||
try:
|
||||
r = requests.request(method="GET", url=f"{url}/api/version")
|
||||
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:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
else:
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
try:
|
||||
r = requests.request(method="GET", url=f"{url}/api/version")
|
||||
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:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
return {"version": False}
|
||||
|
||||
|
||||
class ModelNameForm(BaseModel):
|
||||
@@ -278,65 +339,10 @@ async def pull_model(
|
||||
|
||||
r = None
|
||||
|
||||
def get_request():
|
||||
nonlocal url
|
||||
nonlocal r
|
||||
# Admin should be able to pull models from any source
|
||||
payload = {**form_data.model_dump(exclude_none=True), "insecure": True}
|
||||
|
||||
request_id = str(uuid.uuid4())
|
||||
try:
|
||||
REQUEST_POOL.append(request_id)
|
||||
|
||||
def stream_content():
|
||||
try:
|
||||
yield json.dumps({"id": request_id, "done": False}) + "\n"
|
||||
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
if request_id in REQUEST_POOL:
|
||||
yield chunk
|
||||
else:
|
||||
log.warning("User: canceled request")
|
||||
break
|
||||
finally:
|
||||
if hasattr(r, "close"):
|
||||
r.close()
|
||||
if request_id in REQUEST_POOL:
|
||||
REQUEST_POOL.remove(request_id)
|
||||
|
||||
r = requests.request(
|
||||
method="POST",
|
||||
url=f"{url}/api/pull",
|
||||
data=form_data.model_dump_json(exclude_none=True).encode(),
|
||||
stream=True,
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
return StreamingResponse(
|
||||
stream_content(),
|
||||
status_code=r.status_code,
|
||||
headers=dict(r.headers),
|
||||
)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
try:
|
||||
return await run_in_threadpool(get_request)
|
||||
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
return await post_streaming_url(f"{url}/api/pull", json.dumps(payload))
|
||||
|
||||
|
||||
class PushModelForm(BaseModel):
|
||||
@@ -364,50 +370,9 @@ async def push_model(
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
log.debug(f"url: {url}")
|
||||
|
||||
r = None
|
||||
|
||||
def get_request():
|
||||
nonlocal url
|
||||
nonlocal r
|
||||
try:
|
||||
|
||||
def stream_content():
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
yield chunk
|
||||
|
||||
r = requests.request(
|
||||
method="POST",
|
||||
url=f"{url}/api/push",
|
||||
data=form_data.model_dump_json(exclude_none=True).encode(),
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
return StreamingResponse(
|
||||
stream_content(),
|
||||
status_code=r.status_code,
|
||||
headers=dict(r.headers),
|
||||
)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
try:
|
||||
return await run_in_threadpool(get_request)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
return await post_streaming_url(
|
||||
f"{url}/api/push", form_data.model_dump_json(exclude_none=True).encode()
|
||||
)
|
||||
|
||||
|
||||
class CreateModelForm(BaseModel):
|
||||
@@ -426,53 +391,9 @@ async def create_model(
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
log.info(f"url: {url}")
|
||||
|
||||
r = None
|
||||
|
||||
def get_request():
|
||||
nonlocal url
|
||||
nonlocal r
|
||||
try:
|
||||
|
||||
def stream_content():
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
yield chunk
|
||||
|
||||
r = requests.request(
|
||||
method="POST",
|
||||
url=f"{url}/api/create",
|
||||
data=form_data.model_dump_json(exclude_none=True).encode(),
|
||||
stream=True,
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
log.debug(f"r: {r}")
|
||||
|
||||
return StreamingResponse(
|
||||
stream_content(),
|
||||
status_code=r.status_code,
|
||||
headers=dict(r.headers),
|
||||
)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
try:
|
||||
return await run_in_threadpool(get_request)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
return await post_streaming_url(
|
||||
f"{url}/api/create", form_data.model_dump_json(exclude_none=True).encode()
|
||||
)
|
||||
|
||||
|
||||
class CopyModelForm(BaseModel):
|
||||
@@ -762,66 +683,9 @@ async def generate_completion(
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
log.info(f"url: {url}")
|
||||
|
||||
r = None
|
||||
|
||||
def get_request():
|
||||
nonlocal form_data
|
||||
nonlocal r
|
||||
|
||||
request_id = str(uuid.uuid4())
|
||||
try:
|
||||
REQUEST_POOL.append(request_id)
|
||||
|
||||
def stream_content():
|
||||
try:
|
||||
if form_data.stream:
|
||||
yield json.dumps({"id": request_id, "done": False}) + "\n"
|
||||
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
if request_id in REQUEST_POOL:
|
||||
yield chunk
|
||||
else:
|
||||
log.warning("User: canceled request")
|
||||
break
|
||||
finally:
|
||||
if hasattr(r, "close"):
|
||||
r.close()
|
||||
if request_id in REQUEST_POOL:
|
||||
REQUEST_POOL.remove(request_id)
|
||||
|
||||
r = requests.request(
|
||||
method="POST",
|
||||
url=f"{url}/api/generate",
|
||||
data=form_data.model_dump_json(exclude_none=True).encode(),
|
||||
stream=True,
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
return StreamingResponse(
|
||||
stream_content(),
|
||||
status_code=r.status_code,
|
||||
headers=dict(r.headers),
|
||||
)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
try:
|
||||
return await run_in_threadpool(get_request)
|
||||
except Exception as e:
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
return await post_streaming_url(
|
||||
f"{url}/api/generate", form_data.model_dump_json(exclude_none=True).encode()
|
||||
)
|
||||
|
||||
|
||||
class ChatMessage(BaseModel):
|
||||
@@ -848,14 +712,125 @@ async def generate_chat_completion(
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
|
||||
log.debug(
|
||||
"form_data.model_dump_json(exclude_none=True).encode(): {0} ".format(
|
||||
form_data.model_dump_json(exclude_none=True).encode()
|
||||
)
|
||||
)
|
||||
|
||||
payload = {
|
||||
**form_data.model_dump(exclude_none=True),
|
||||
}
|
||||
|
||||
model_id = form_data.model
|
||||
model_info = Models.get_model_by_id(model_id)
|
||||
|
||||
if model_info:
|
||||
if model_info.base_model_id:
|
||||
payload["model"] = model_info.base_model_id
|
||||
|
||||
model_info.params = model_info.params.model_dump()
|
||||
|
||||
if model_info.params:
|
||||
payload["options"] = {}
|
||||
|
||||
if model_info.params.get("mirostat", None):
|
||||
payload["options"]["mirostat"] = model_info.params.get("mirostat", None)
|
||||
|
||||
if model_info.params.get("mirostat_eta", None):
|
||||
payload["options"]["mirostat_eta"] = model_info.params.get(
|
||||
"mirostat_eta", None
|
||||
)
|
||||
|
||||
if model_info.params.get("mirostat_tau", None):
|
||||
|
||||
payload["options"]["mirostat_tau"] = model_info.params.get(
|
||||
"mirostat_tau", None
|
||||
)
|
||||
|
||||
if model_info.params.get("num_ctx", None):
|
||||
payload["options"]["num_ctx"] = model_info.params.get("num_ctx", None)
|
||||
|
||||
if model_info.params.get("repeat_last_n", None):
|
||||
payload["options"]["repeat_last_n"] = model_info.params.get(
|
||||
"repeat_last_n", None
|
||||
)
|
||||
|
||||
if model_info.params.get("frequency_penalty", None):
|
||||
payload["options"]["repeat_penalty"] = model_info.params.get(
|
||||
"frequency_penalty", None
|
||||
)
|
||||
|
||||
if model_info.params.get("temperature", None) is not None:
|
||||
payload["options"]["temperature"] = model_info.params.get(
|
||||
"temperature", None
|
||||
)
|
||||
|
||||
if model_info.params.get("seed", None):
|
||||
payload["options"]["seed"] = model_info.params.get("seed", None)
|
||||
|
||||
if model_info.params.get("stop", None):
|
||||
payload["options"]["stop"] = (
|
||||
[
|
||||
bytes(stop, "utf-8").decode("unicode_escape")
|
||||
for stop in model_info.params["stop"]
|
||||
]
|
||||
if model_info.params.get("stop", None)
|
||||
else None
|
||||
)
|
||||
|
||||
if model_info.params.get("tfs_z", None):
|
||||
payload["options"]["tfs_z"] = model_info.params.get("tfs_z", None)
|
||||
|
||||
if model_info.params.get("max_tokens", None):
|
||||
payload["options"]["num_predict"] = model_info.params.get(
|
||||
"max_tokens", None
|
||||
)
|
||||
|
||||
if model_info.params.get("top_k", None):
|
||||
payload["options"]["top_k"] = model_info.params.get("top_k", None)
|
||||
|
||||
if model_info.params.get("top_p", None):
|
||||
payload["options"]["top_p"] = model_info.params.get("top_p", None)
|
||||
|
||||
if model_info.params.get("use_mmap", None):
|
||||
payload["options"]["use_mmap"] = model_info.params.get("use_mmap", None)
|
||||
|
||||
if model_info.params.get("use_mlock", None):
|
||||
payload["options"]["use_mlock"] = model_info.params.get(
|
||||
"use_mlock", None
|
||||
)
|
||||
|
||||
if model_info.params.get("num_thread", None):
|
||||
payload["options"]["num_thread"] = model_info.params.get(
|
||||
"num_thread", None
|
||||
)
|
||||
|
||||
if model_info.params.get("system", None):
|
||||
# Check if the payload already has a system message
|
||||
# If not, add a system message to the payload
|
||||
if payload.get("messages"):
|
||||
for message in payload["messages"]:
|
||||
if message.get("role") == "system":
|
||||
message["content"] = (
|
||||
model_info.params.get("system", None) + message["content"]
|
||||
)
|
||||
break
|
||||
else:
|
||||
payload["messages"].insert(
|
||||
0,
|
||||
{
|
||||
"role": "system",
|
||||
"content": model_info.params.get("system", None),
|
||||
},
|
||||
)
|
||||
|
||||
if url_idx == None:
|
||||
model = form_data.model
|
||||
if ":" not in payload["model"]:
|
||||
payload["model"] = f"{payload['model']}:latest"
|
||||
|
||||
if ":" not in model:
|
||||
model = f"{model}:latest"
|
||||
|
||||
if model in app.state.MODELS:
|
||||
url_idx = random.choice(app.state.MODELS[model]["urls"])
|
||||
if payload["model"] in app.state.MODELS:
|
||||
url_idx = random.choice(app.state.MODELS[payload["model"]]["urls"])
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
@@ -865,79 +840,20 @@ async def generate_chat_completion(
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
log.info(f"url: {url}")
|
||||
|
||||
r = None
|
||||
print(payload)
|
||||
|
||||
log.debug(
|
||||
"form_data.model_dump_json(exclude_none=True).encode(): {0} ".format(
|
||||
form_data.model_dump_json(exclude_none=True).encode()
|
||||
)
|
||||
)
|
||||
|
||||
def get_request():
|
||||
nonlocal form_data
|
||||
nonlocal r
|
||||
|
||||
request_id = str(uuid.uuid4())
|
||||
try:
|
||||
REQUEST_POOL.append(request_id)
|
||||
|
||||
def stream_content():
|
||||
try:
|
||||
if form_data.stream:
|
||||
yield json.dumps({"id": request_id, "done": False}) + "\n"
|
||||
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
if request_id in REQUEST_POOL:
|
||||
yield chunk
|
||||
else:
|
||||
log.warning("User: canceled request")
|
||||
break
|
||||
finally:
|
||||
if hasattr(r, "close"):
|
||||
r.close()
|
||||
if request_id in REQUEST_POOL:
|
||||
REQUEST_POOL.remove(request_id)
|
||||
|
||||
r = requests.request(
|
||||
method="POST",
|
||||
url=f"{url}/api/chat",
|
||||
data=form_data.model_dump_json(exclude_none=True).encode(),
|
||||
stream=True,
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
return StreamingResponse(
|
||||
stream_content(),
|
||||
status_code=r.status_code,
|
||||
headers=dict(r.headers),
|
||||
)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
raise e
|
||||
|
||||
try:
|
||||
return await run_in_threadpool(get_request)
|
||||
except Exception as e:
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
return await post_streaming_url(f"{url}/api/chat", json.dumps(payload))
|
||||
|
||||
|
||||
# TODO: we should update this part once Ollama supports other types
|
||||
class OpenAIChatMessageContent(BaseModel):
|
||||
type: str
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
|
||||
class OpenAIChatMessage(BaseModel):
|
||||
role: str
|
||||
content: str
|
||||
content: Union[str, OpenAIChatMessageContent]
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
@@ -957,14 +873,61 @@ async def generate_openai_chat_completion(
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
|
||||
payload = {
|
||||
**form_data.model_dump(exclude_none=True),
|
||||
}
|
||||
|
||||
model_id = form_data.model
|
||||
model_info = Models.get_model_by_id(model_id)
|
||||
|
||||
if model_info:
|
||||
if model_info.base_model_id:
|
||||
payload["model"] = model_info.base_model_id
|
||||
|
||||
model_info.params = model_info.params.model_dump()
|
||||
|
||||
if model_info.params:
|
||||
payload["temperature"] = model_info.params.get("temperature", None)
|
||||
payload["top_p"] = model_info.params.get("top_p", None)
|
||||
payload["max_tokens"] = model_info.params.get("max_tokens", None)
|
||||
payload["frequency_penalty"] = model_info.params.get(
|
||||
"frequency_penalty", None
|
||||
)
|
||||
payload["seed"] = model_info.params.get("seed", None)
|
||||
payload["stop"] = (
|
||||
[
|
||||
bytes(stop, "utf-8").decode("unicode_escape")
|
||||
for stop in model_info.params["stop"]
|
||||
]
|
||||
if model_info.params.get("stop", None)
|
||||
else None
|
||||
)
|
||||
|
||||
if model_info.params.get("system", None):
|
||||
# Check if the payload already has a system message
|
||||
# If not, add a system message to the payload
|
||||
if payload.get("messages"):
|
||||
for message in payload["messages"]:
|
||||
if message.get("role") == "system":
|
||||
message["content"] = (
|
||||
model_info.params.get("system", None) + message["content"]
|
||||
)
|
||||
break
|
||||
else:
|
||||
payload["messages"].insert(
|
||||
0,
|
||||
{
|
||||
"role": "system",
|
||||
"content": model_info.params.get("system", None),
|
||||
},
|
||||
)
|
||||
|
||||
if url_idx == None:
|
||||
model = form_data.model
|
||||
if ":" not in payload["model"]:
|
||||
payload["model"] = f"{payload['model']}:latest"
|
||||
|
||||
if ":" not in model:
|
||||
model = f"{model}:latest"
|
||||
|
||||
if model in app.state.MODELS:
|
||||
url_idx = random.choice(app.state.MODELS[model]["urls"])
|
||||
if payload["model"] in app.state.MODELS:
|
||||
url_idx = random.choice(app.state.MODELS[payload["model"]]["urls"])
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
@@ -974,68 +937,7 @@ async def generate_openai_chat_completion(
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
log.info(f"url: {url}")
|
||||
|
||||
r = None
|
||||
|
||||
def get_request():
|
||||
nonlocal form_data
|
||||
nonlocal r
|
||||
|
||||
request_id = str(uuid.uuid4())
|
||||
try:
|
||||
REQUEST_POOL.append(request_id)
|
||||
|
||||
def stream_content():
|
||||
try:
|
||||
if form_data.stream:
|
||||
yield json.dumps(
|
||||
{"request_id": request_id, "done": False}
|
||||
) + "\n"
|
||||
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
if request_id in REQUEST_POOL:
|
||||
yield chunk
|
||||
else:
|
||||
log.warning("User: canceled request")
|
||||
break
|
||||
finally:
|
||||
if hasattr(r, "close"):
|
||||
r.close()
|
||||
if request_id in REQUEST_POOL:
|
||||
REQUEST_POOL.remove(request_id)
|
||||
|
||||
r = requests.request(
|
||||
method="POST",
|
||||
url=f"{url}/v1/chat/completions",
|
||||
data=form_data.model_dump_json(exclude_none=True).encode(),
|
||||
stream=True,
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
return StreamingResponse(
|
||||
stream_content(),
|
||||
status_code=r.status_code,
|
||||
headers=dict(r.headers),
|
||||
)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
try:
|
||||
return await run_in_threadpool(get_request)
|
||||
except Exception as e:
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
return await post_streaming_url(f"{url}/v1/chat/completions", json.dumps(payload))
|
||||
|
||||
|
||||
@app.get("/v1/models")
|
||||
@@ -1147,7 +1049,7 @@ async def download_file_stream(
|
||||
|
||||
timeout = aiohttp.ClientTimeout(total=600) # Set the timeout
|
||||
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
|
||||
async with session.get(file_url, headers=headers) as response:
|
||||
total_size = int(response.headers.get("content-length", 0)) + current_size
|
||||
|
||||
@@ -1364,7 +1266,7 @@ async def deprecated_proxy(
|
||||
if path == "generate":
|
||||
data = json.loads(body.decode("utf-8"))
|
||||
|
||||
if not ("stream" in data and data["stream"] == False):
|
||||
if data.get("stream", True):
|
||||
yield json.dumps({"id": request_id, "done": False}) + "\n"
|
||||
|
||||
elif path == "chat":
|
||||
|
||||
+223
-57
@@ -9,9 +9,10 @@ import json
|
||||
import logging
|
||||
|
||||
from pydantic import BaseModel
|
||||
from starlette.background import BackgroundTask
|
||||
|
||||
|
||||
from apps.web.models.users import Users
|
||||
from apps.webui.models.models import Models
|
||||
from apps.webui.models.users import Users
|
||||
from constants import ERROR_MESSAGES
|
||||
from utils.utils import (
|
||||
decode_token,
|
||||
@@ -53,7 +54,6 @@ app.state.config = AppConfig()
|
||||
app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
|
||||
app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
||||
|
||||
|
||||
app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API
|
||||
app.state.config.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
|
||||
app.state.config.OPENAI_API_KEYS = OPENAI_API_KEYS
|
||||
@@ -185,28 +185,41 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
||||
async def fetch_url(url, key):
|
||||
timeout = aiohttp.ClientTimeout(total=5)
|
||||
try:
|
||||
if key != "":
|
||||
headers = {"Authorization": f"Bearer {key}"}
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with session.get(url, headers=headers) as response:
|
||||
return await response.json()
|
||||
else:
|
||||
return None
|
||||
headers = {"Authorization": f"Bearer {key}"}
|
||||
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
|
||||
async with session.get(url, headers=headers) as response:
|
||||
return await response.json()
|
||||
except Exception as e:
|
||||
# Handle connection error here
|
||||
log.error(f"Connection error: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def cleanup_response(
|
||||
response: Optional[aiohttp.ClientResponse],
|
||||
session: Optional[aiohttp.ClientSession],
|
||||
):
|
||||
if response:
|
||||
response.close()
|
||||
if session:
|
||||
await session.close()
|
||||
|
||||
|
||||
def merge_models_lists(model_lists):
|
||||
log.info(f"merge_models_lists {model_lists}")
|
||||
log.debug(f"merge_models_lists {model_lists}")
|
||||
merged_list = []
|
||||
|
||||
for idx, models in enumerate(model_lists):
|
||||
if models is not None and "error" not in models:
|
||||
merged_list.extend(
|
||||
[
|
||||
{**model, "urlIdx": idx}
|
||||
{
|
||||
**model,
|
||||
"name": model.get("name", model["id"]),
|
||||
"owned_by": "openai",
|
||||
"openai": model,
|
||||
"urlIdx": idx,
|
||||
}
|
||||
for model in models
|
||||
if "api.openai.com"
|
||||
not in app.state.config.OPENAI_API_BASE_URLS[idx]
|
||||
@@ -217,7 +230,7 @@ def merge_models_lists(model_lists):
|
||||
return merged_list
|
||||
|
||||
|
||||
async def get_all_models():
|
||||
async def get_all_models(raw: bool = False):
|
||||
log.info("get_all_models()")
|
||||
|
||||
if (
|
||||
@@ -226,13 +239,37 @@ async def get_all_models():
|
||||
) or not app.state.config.ENABLE_OPENAI_API:
|
||||
models = {"data": []}
|
||||
else:
|
||||
# Check if API KEYS length is same than API URLS length
|
||||
if len(app.state.config.OPENAI_API_KEYS) != len(
|
||||
app.state.config.OPENAI_API_BASE_URLS
|
||||
):
|
||||
# if there are more keys than urls, remove the extra keys
|
||||
if len(app.state.config.OPENAI_API_KEYS) > len(
|
||||
app.state.config.OPENAI_API_BASE_URLS
|
||||
):
|
||||
app.state.config.OPENAI_API_KEYS = app.state.config.OPENAI_API_KEYS[
|
||||
: len(app.state.config.OPENAI_API_BASE_URLS)
|
||||
]
|
||||
# if there are more urls than keys, add empty keys
|
||||
else:
|
||||
app.state.config.OPENAI_API_KEYS += [
|
||||
""
|
||||
for _ in range(
|
||||
len(app.state.config.OPENAI_API_BASE_URLS)
|
||||
- len(app.state.config.OPENAI_API_KEYS)
|
||||
)
|
||||
]
|
||||
|
||||
tasks = [
|
||||
fetch_url(f"{url}/models", app.state.config.OPENAI_API_KEYS[idx])
|
||||
for idx, url in enumerate(app.state.config.OPENAI_API_BASE_URLS)
|
||||
]
|
||||
|
||||
responses = await asyncio.gather(*tasks)
|
||||
log.info(f"get_all_models:responses() {responses}")
|
||||
log.debug(f"get_all_models:responses() {responses}")
|
||||
|
||||
if raw:
|
||||
return responses
|
||||
|
||||
models = {
|
||||
"data": merge_models_lists(
|
||||
@@ -249,10 +286,10 @@ async def get_all_models():
|
||||
)
|
||||
}
|
||||
|
||||
log.info(f"models: {models}")
|
||||
log.debug(f"models: {models}")
|
||||
app.state.MODELS = {model["id"]: model for model in models["data"]}
|
||||
|
||||
return models
|
||||
return models
|
||||
|
||||
|
||||
@app.get("/models")
|
||||
@@ -272,11 +309,16 @@ async def get_models(url_idx: Optional[int] = None, user=Depends(get_current_use
|
||||
return models
|
||||
else:
|
||||
url = app.state.config.OPENAI_API_BASE_URLS[url_idx]
|
||||
key = app.state.config.OPENAI_API_KEYS[url_idx]
|
||||
|
||||
headers = {}
|
||||
headers["Authorization"] = f"Bearer {key}"
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
r = None
|
||||
|
||||
try:
|
||||
r = requests.request(method="GET", url=f"{url}/models")
|
||||
r = requests.request(method="GET", url=f"{url}/models", headers=headers)
|
||||
r.raise_for_status()
|
||||
|
||||
response_data = r.json()
|
||||
@@ -303,84 +345,208 @@ async def get_models(url_idx: Optional[int] = None, user=Depends(get_current_use
|
||||
)
|
||||
|
||||
|
||||
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
||||
async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
||||
@app.post("/chat/completions")
|
||||
@app.post("/chat/completions/{url_idx}")
|
||||
async def generate_chat_completion(
|
||||
form_data: dict,
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
idx = 0
|
||||
payload = {**form_data}
|
||||
|
||||
body = await request.body()
|
||||
# TODO: Remove below after gpt-4-vision fix from Open AI
|
||||
# Try to decode the body of the request from bytes to a UTF-8 string (Require add max_token to fix gpt-4-vision)
|
||||
try:
|
||||
body = body.decode("utf-8")
|
||||
body = json.loads(body)
|
||||
model_id = form_data.get("model")
|
||||
model_info = Models.get_model_by_id(model_id)
|
||||
|
||||
idx = app.state.MODELS[body.get("model")]["urlIdx"]
|
||||
if model_info:
|
||||
if model_info.base_model_id:
|
||||
payload["model"] = model_info.base_model_id
|
||||
|
||||
# Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000
|
||||
# This is a workaround until OpenAI fixes the issue with this model
|
||||
if body.get("model") == "gpt-4-vision-preview":
|
||||
if "max_tokens" not in body:
|
||||
body["max_tokens"] = 4000
|
||||
log.debug("Modified body_dict:", body)
|
||||
model_info.params = model_info.params.model_dump()
|
||||
|
||||
# Fix for ChatGPT calls failing because the num_ctx key is in body
|
||||
if "num_ctx" in body:
|
||||
# If 'num_ctx' is in the dictionary, delete it
|
||||
# Leaving it there generates an error with the
|
||||
# OpenAI API (Feb 2024)
|
||||
del body["num_ctx"]
|
||||
if model_info.params:
|
||||
if model_info.params.get("temperature", None) is not None:
|
||||
payload["temperature"] = float(model_info.params.get("temperature"))
|
||||
|
||||
# Convert the modified body back to JSON
|
||||
body = json.dumps(body)
|
||||
except json.JSONDecodeError as e:
|
||||
log.error("Error loading request body into a dictionary:", e)
|
||||
if model_info.params.get("top_p", None):
|
||||
payload["top_p"] = int(model_info.params.get("top_p", None))
|
||||
|
||||
if model_info.params.get("max_tokens", None):
|
||||
payload["max_tokens"] = int(model_info.params.get("max_tokens", None))
|
||||
|
||||
if model_info.params.get("frequency_penalty", None):
|
||||
payload["frequency_penalty"] = int(
|
||||
model_info.params.get("frequency_penalty", None)
|
||||
)
|
||||
|
||||
if model_info.params.get("seed", None):
|
||||
payload["seed"] = model_info.params.get("seed", None)
|
||||
|
||||
if model_info.params.get("stop", None):
|
||||
payload["stop"] = (
|
||||
[
|
||||
bytes(stop, "utf-8").decode("unicode_escape")
|
||||
for stop in model_info.params["stop"]
|
||||
]
|
||||
if model_info.params.get("stop", None)
|
||||
else None
|
||||
)
|
||||
|
||||
if model_info.params.get("system", None):
|
||||
# Check if the payload already has a system message
|
||||
# If not, add a system message to the payload
|
||||
if payload.get("messages"):
|
||||
for message in payload["messages"]:
|
||||
if message.get("role") == "system":
|
||||
message["content"] = (
|
||||
model_info.params.get("system", None) + message["content"]
|
||||
)
|
||||
break
|
||||
else:
|
||||
payload["messages"].insert(
|
||||
0,
|
||||
{
|
||||
"role": "system",
|
||||
"content": model_info.params.get("system", None),
|
||||
},
|
||||
)
|
||||
|
||||
else:
|
||||
pass
|
||||
|
||||
model = app.state.MODELS[payload.get("model")]
|
||||
idx = model["urlIdx"]
|
||||
|
||||
if "pipeline" in model and model.get("pipeline"):
|
||||
payload["user"] = {"name": user.name, "id": user.id}
|
||||
|
||||
# Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000
|
||||
# This is a workaround until OpenAI fixes the issue with this model
|
||||
if payload.get("model") == "gpt-4-vision-preview":
|
||||
if "max_tokens" not in payload:
|
||||
payload["max_tokens"] = 4000
|
||||
log.debug("Modified payload:", payload)
|
||||
|
||||
# Convert the modified body back to JSON
|
||||
payload = json.dumps(payload)
|
||||
|
||||
print(payload)
|
||||
|
||||
url = app.state.config.OPENAI_API_BASE_URLS[idx]
|
||||
key = app.state.config.OPENAI_API_KEYS[idx]
|
||||
|
||||
target_url = f"{url}/{path}"
|
||||
|
||||
if key == "":
|
||||
raise HTTPException(status_code=401, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
|
||||
print(payload)
|
||||
|
||||
headers = {}
|
||||
headers["Authorization"] = f"Bearer {key}"
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
r = None
|
||||
session = None
|
||||
streaming = False
|
||||
|
||||
try:
|
||||
r = requests.request(
|
||||
method=request.method,
|
||||
url=target_url,
|
||||
data=body,
|
||||
session = aiohttp.ClientSession(trust_env=True)
|
||||
r = await session.request(
|
||||
method="POST",
|
||||
url=f"{url}/chat/completions",
|
||||
data=payload,
|
||||
headers=headers,
|
||||
stream=True,
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
# Check if response is SSE
|
||||
if "text/event-stream" in r.headers.get("Content-Type", ""):
|
||||
streaming = True
|
||||
return StreamingResponse(
|
||||
r.iter_content(chunk_size=8192),
|
||||
status_code=r.status_code,
|
||||
r.content,
|
||||
status_code=r.status,
|
||||
headers=dict(r.headers),
|
||||
background=BackgroundTask(
|
||||
cleanup_response, response=r, session=session
|
||||
),
|
||||
)
|
||||
else:
|
||||
response_data = r.json()
|
||||
response_data = await r.json()
|
||||
return response_data
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
res = await r.json()
|
||||
print(res)
|
||||
if "error" in res:
|
||||
error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
|
||||
except:
|
||||
error_detail = f"External: {e}"
|
||||
raise HTTPException(status_code=r.status if r else 500, detail=error_detail)
|
||||
finally:
|
||||
if not streaming and session:
|
||||
if r:
|
||||
r.close()
|
||||
await session.close()
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500, detail=error_detail
|
||||
|
||||
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
||||
async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
||||
idx = 0
|
||||
|
||||
body = await request.body()
|
||||
|
||||
url = app.state.config.OPENAI_API_BASE_URLS[idx]
|
||||
key = app.state.config.OPENAI_API_KEYS[idx]
|
||||
|
||||
target_url = f"{url}/{path}"
|
||||
|
||||
headers = {}
|
||||
headers["Authorization"] = f"Bearer {key}"
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
r = None
|
||||
session = None
|
||||
streaming = False
|
||||
|
||||
try:
|
||||
session = aiohttp.ClientSession(trust_env=True)
|
||||
r = await session.request(
|
||||
method=request.method,
|
||||
url=target_url,
|
||||
data=body,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
# Check if response is SSE
|
||||
if "text/event-stream" in r.headers.get("Content-Type", ""):
|
||||
streaming = True
|
||||
return StreamingResponse(
|
||||
r.content,
|
||||
status_code=r.status,
|
||||
headers=dict(r.headers),
|
||||
background=BackgroundTask(
|
||||
cleanup_response, response=r, session=session
|
||||
),
|
||||
)
|
||||
else:
|
||||
response_data = await r.json()
|
||||
return response_data
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = await r.json()
|
||||
print(res)
|
||||
if "error" in res:
|
||||
error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
|
||||
except:
|
||||
error_detail = f"External: {e}"
|
||||
raise HTTPException(status_code=r.status if r else 500, detail=error_detail)
|
||||
finally:
|
||||
if not streaming and session:
|
||||
if r:
|
||||
r.close()
|
||||
await session.close()
|
||||
|
||||
+357
-55
@@ -8,12 +8,15 @@ from fastapi import (
|
||||
Form,
|
||||
)
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
import requests
|
||||
import os, shutil, logging, re
|
||||
from datetime import datetime
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
from typing import List, Union, Sequence, Iterator, Any
|
||||
|
||||
from chromadb.utils.batch_utils import create_batches
|
||||
from langchain_core.documents import Document
|
||||
|
||||
from langchain_community.document_loaders import (
|
||||
WebBaseLoader,
|
||||
@@ -28,7 +31,9 @@ from langchain_community.document_loaders import (
|
||||
UnstructuredXMLLoader,
|
||||
UnstructuredRSTLoader,
|
||||
UnstructuredExcelLoader,
|
||||
UnstructuredPowerPointLoader,
|
||||
YoutubeLoader,
|
||||
OutlookMessageLoader,
|
||||
)
|
||||
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
|
||||
@@ -45,7 +50,7 @@ import json
|
||||
|
||||
import sentence_transformers
|
||||
|
||||
from apps.web.models.documents import (
|
||||
from apps.webui.models.documents import (
|
||||
Documents,
|
||||
DocumentForm,
|
||||
DocumentResponse,
|
||||
@@ -60,6 +65,15 @@ from apps.rag.utils import (
|
||||
query_collection_with_hybrid_search,
|
||||
)
|
||||
|
||||
from apps.rag.search.brave import search_brave
|
||||
from apps.rag.search.google_pse import search_google_pse
|
||||
from apps.rag.search.main import SearchResult
|
||||
from apps.rag.search.searxng import search_searxng
|
||||
from apps.rag.search.serper import search_serper
|
||||
from apps.rag.search.serpstack import search_serpstack
|
||||
from apps.rag.search.serply import search_serply
|
||||
from apps.rag.search.duckduckgo import search_duckduckgo
|
||||
|
||||
from utils.misc import (
|
||||
calculate_sha256,
|
||||
calculate_sha256_string,
|
||||
@@ -69,6 +83,7 @@ from utils.misc import (
|
||||
from utils.utils import get_current_user, get_admin_user
|
||||
|
||||
from config import (
|
||||
AppConfig,
|
||||
ENV,
|
||||
SRC_LOG_LEVELS,
|
||||
UPLOAD_DIR,
|
||||
@@ -94,7 +109,19 @@ from config import (
|
||||
RAG_TEMPLATE,
|
||||
ENABLE_RAG_LOCAL_WEB_FETCH,
|
||||
YOUTUBE_LOADER_LANGUAGE,
|
||||
AppConfig,
|
||||
ENABLE_RAG_WEB_SEARCH,
|
||||
RAG_WEB_SEARCH_ENGINE,
|
||||
SEARXNG_QUERY_URL,
|
||||
GOOGLE_PSE_API_KEY,
|
||||
GOOGLE_PSE_ENGINE_ID,
|
||||
BRAVE_SEARCH_API_KEY,
|
||||
SERPSTACK_API_KEY,
|
||||
SERPSTACK_HTTPS,
|
||||
SERPER_API_KEY,
|
||||
SERPLY_API_KEY,
|
||||
RAG_WEB_SEARCH_RESULT_COUNT,
|
||||
RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
||||
RAG_EMBEDDING_OPENAI_BATCH_SIZE,
|
||||
)
|
||||
|
||||
from constants import ERROR_MESSAGES
|
||||
@@ -119,6 +146,7 @@ app.state.config.CHUNK_OVERLAP = CHUNK_OVERLAP
|
||||
|
||||
app.state.config.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE
|
||||
app.state.config.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
|
||||
app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE = RAG_EMBEDDING_OPENAI_BATCH_SIZE
|
||||
app.state.config.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL
|
||||
app.state.config.RAG_TEMPLATE = RAG_TEMPLATE
|
||||
|
||||
@@ -133,6 +161,21 @@ app.state.config.YOUTUBE_LOADER_LANGUAGE = YOUTUBE_LOADER_LANGUAGE
|
||||
app.state.YOUTUBE_LOADER_TRANSLATION = None
|
||||
|
||||
|
||||
app.state.config.ENABLE_RAG_WEB_SEARCH = ENABLE_RAG_WEB_SEARCH
|
||||
app.state.config.RAG_WEB_SEARCH_ENGINE = RAG_WEB_SEARCH_ENGINE
|
||||
|
||||
app.state.config.SEARXNG_QUERY_URL = SEARXNG_QUERY_URL
|
||||
app.state.config.GOOGLE_PSE_API_KEY = GOOGLE_PSE_API_KEY
|
||||
app.state.config.GOOGLE_PSE_ENGINE_ID = GOOGLE_PSE_ENGINE_ID
|
||||
app.state.config.BRAVE_SEARCH_API_KEY = BRAVE_SEARCH_API_KEY
|
||||
app.state.config.SERPSTACK_API_KEY = SERPSTACK_API_KEY
|
||||
app.state.config.SERPSTACK_HTTPS = SERPSTACK_HTTPS
|
||||
app.state.config.SERPER_API_KEY = SERPER_API_KEY
|
||||
app.state.config.SERPLY_API_KEY = SERPLY_API_KEY
|
||||
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = RAG_WEB_SEARCH_RESULT_COUNT
|
||||
app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = RAG_WEB_SEARCH_CONCURRENT_REQUESTS
|
||||
|
||||
|
||||
def update_embedding_model(
|
||||
embedding_model: str,
|
||||
update_model: bool = False,
|
||||
@@ -178,6 +221,7 @@ app.state.EMBEDDING_FUNCTION = get_embedding_function(
|
||||
app.state.sentence_transformer_ef,
|
||||
app.state.config.OPENAI_API_KEY,
|
||||
app.state.config.OPENAI_API_BASE_URL,
|
||||
app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
|
||||
)
|
||||
|
||||
origins = ["*"]
|
||||
@@ -200,6 +244,10 @@ class UrlForm(CollectionNameForm):
|
||||
url: str
|
||||
|
||||
|
||||
class SearchForm(CollectionNameForm):
|
||||
query: str
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def get_status():
|
||||
return {
|
||||
@@ -210,6 +258,7 @@ async def get_status():
|
||||
"embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
|
||||
"embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
|
||||
"reranking_model": app.state.config.RAG_RERANKING_MODEL,
|
||||
"openai_batch_size": app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
|
||||
}
|
||||
|
||||
|
||||
@@ -222,6 +271,7 @@ async def get_embedding_config(user=Depends(get_admin_user)):
|
||||
"openai_config": {
|
||||
"url": app.state.config.OPENAI_API_BASE_URL,
|
||||
"key": app.state.config.OPENAI_API_KEY,
|
||||
"batch_size": app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -237,6 +287,7 @@ async def get_reraanking_config(user=Depends(get_admin_user)):
|
||||
class OpenAIConfigForm(BaseModel):
|
||||
url: str
|
||||
key: str
|
||||
batch_size: Optional[int] = None
|
||||
|
||||
|
||||
class EmbeddingModelUpdateForm(BaseModel):
|
||||
@@ -257,9 +308,14 @@ async def update_embedding_config(
|
||||
app.state.config.RAG_EMBEDDING_MODEL = form_data.embedding_model
|
||||
|
||||
if app.state.config.RAG_EMBEDDING_ENGINE in ["ollama", "openai"]:
|
||||
if form_data.openai_config != None:
|
||||
if form_data.openai_config is not None:
|
||||
app.state.config.OPENAI_API_BASE_URL = form_data.openai_config.url
|
||||
app.state.config.OPENAI_API_KEY = form_data.openai_config.key
|
||||
app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE = (
|
||||
form_data.openai_config.batch_size
|
||||
if form_data.openai_config.batch_size
|
||||
else 1
|
||||
)
|
||||
|
||||
update_embedding_model(app.state.config.RAG_EMBEDDING_MODEL)
|
||||
|
||||
@@ -269,6 +325,7 @@ async def update_embedding_config(
|
||||
app.state.sentence_transformer_ef,
|
||||
app.state.config.OPENAI_API_KEY,
|
||||
app.state.config.OPENAI_API_BASE_URL,
|
||||
app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
|
||||
)
|
||||
|
||||
return {
|
||||
@@ -278,6 +335,7 @@ async def update_embedding_config(
|
||||
"openai_config": {
|
||||
"url": app.state.config.OPENAI_API_BASE_URL,
|
||||
"key": app.state.config.OPENAI_API_KEY,
|
||||
"batch_size": app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
|
||||
},
|
||||
}
|
||||
except Exception as e:
|
||||
@@ -325,11 +383,27 @@ async def get_rag_config(user=Depends(get_admin_user)):
|
||||
"chunk_size": app.state.config.CHUNK_SIZE,
|
||||
"chunk_overlap": app.state.config.CHUNK_OVERLAP,
|
||||
},
|
||||
"web_loader_ssl_verification": app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
|
||||
"youtube": {
|
||||
"language": app.state.config.YOUTUBE_LOADER_LANGUAGE,
|
||||
"translation": app.state.YOUTUBE_LOADER_TRANSLATION,
|
||||
},
|
||||
"web": {
|
||||
"ssl_verification": app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
|
||||
"search": {
|
||||
"enabled": app.state.config.ENABLE_RAG_WEB_SEARCH,
|
||||
"engine": app.state.config.RAG_WEB_SEARCH_ENGINE,
|
||||
"searxng_query_url": app.state.config.SEARXNG_QUERY_URL,
|
||||
"google_pse_api_key": app.state.config.GOOGLE_PSE_API_KEY,
|
||||
"google_pse_engine_id": app.state.config.GOOGLE_PSE_ENGINE_ID,
|
||||
"brave_search_api_key": app.state.config.BRAVE_SEARCH_API_KEY,
|
||||
"serpstack_api_key": app.state.config.SERPSTACK_API_KEY,
|
||||
"serpstack_https": app.state.config.SERPSTACK_HTTPS,
|
||||
"serper_api_key": app.state.config.SERPER_API_KEY,
|
||||
"serply_api_key": app.state.config.SERPLY_API_KEY,
|
||||
"result_count": app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
||||
"concurrent_requests": app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -343,11 +417,31 @@ class YoutubeLoaderConfig(BaseModel):
|
||||
translation: Optional[str] = None
|
||||
|
||||
|
||||
class WebSearchConfig(BaseModel):
|
||||
enabled: bool
|
||||
engine: Optional[str] = None
|
||||
searxng_query_url: Optional[str] = None
|
||||
google_pse_api_key: Optional[str] = None
|
||||
google_pse_engine_id: Optional[str] = None
|
||||
brave_search_api_key: Optional[str] = None
|
||||
serpstack_api_key: Optional[str] = None
|
||||
serpstack_https: Optional[bool] = None
|
||||
serper_api_key: Optional[str] = None
|
||||
serply_api_key: Optional[str] = None
|
||||
result_count: Optional[int] = None
|
||||
concurrent_requests: Optional[int] = None
|
||||
|
||||
|
||||
class WebConfig(BaseModel):
|
||||
search: WebSearchConfig
|
||||
web_loader_ssl_verification: Optional[bool] = None
|
||||
|
||||
|
||||
class ConfigUpdateForm(BaseModel):
|
||||
pdf_extract_images: Optional[bool] = None
|
||||
chunk: Optional[ChunkParamUpdateForm] = None
|
||||
web_loader_ssl_verification: Optional[bool] = None
|
||||
youtube: Optional[YoutubeLoaderConfig] = None
|
||||
web: Optional[WebConfig] = None
|
||||
|
||||
|
||||
@app.post("/config/update")
|
||||
@@ -358,35 +452,37 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_
|
||||
else app.state.config.PDF_EXTRACT_IMAGES
|
||||
)
|
||||
|
||||
app.state.config.CHUNK_SIZE = (
|
||||
form_data.chunk.chunk_size
|
||||
if form_data.chunk is not None
|
||||
else app.state.config.CHUNK_SIZE
|
||||
)
|
||||
if form_data.chunk is not None:
|
||||
app.state.config.CHUNK_SIZE = form_data.chunk.chunk_size
|
||||
app.state.config.CHUNK_OVERLAP = form_data.chunk.chunk_overlap
|
||||
|
||||
app.state.config.CHUNK_OVERLAP = (
|
||||
form_data.chunk.chunk_overlap
|
||||
if form_data.chunk is not None
|
||||
else app.state.config.CHUNK_OVERLAP
|
||||
)
|
||||
if form_data.youtube is not None:
|
||||
app.state.config.YOUTUBE_LOADER_LANGUAGE = form_data.youtube.language
|
||||
app.state.YOUTUBE_LOADER_TRANSLATION = form_data.youtube.translation
|
||||
|
||||
app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
|
||||
form_data.web_loader_ssl_verification
|
||||
if form_data.web_loader_ssl_verification != None
|
||||
else app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
|
||||
)
|
||||
if form_data.web is not None:
|
||||
app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
|
||||
form_data.web.web_loader_ssl_verification
|
||||
)
|
||||
|
||||
app.state.config.YOUTUBE_LOADER_LANGUAGE = (
|
||||
form_data.youtube.language
|
||||
if form_data.youtube is not None
|
||||
else app.state.config.YOUTUBE_LOADER_LANGUAGE
|
||||
)
|
||||
|
||||
app.state.YOUTUBE_LOADER_TRANSLATION = (
|
||||
form_data.youtube.translation
|
||||
if form_data.youtube is not None
|
||||
else app.state.YOUTUBE_LOADER_TRANSLATION
|
||||
)
|
||||
app.state.config.ENABLE_RAG_WEB_SEARCH = form_data.web.search.enabled
|
||||
app.state.config.RAG_WEB_SEARCH_ENGINE = form_data.web.search.engine
|
||||
app.state.config.SEARXNG_QUERY_URL = form_data.web.search.searxng_query_url
|
||||
app.state.config.GOOGLE_PSE_API_KEY = form_data.web.search.google_pse_api_key
|
||||
app.state.config.GOOGLE_PSE_ENGINE_ID = (
|
||||
form_data.web.search.google_pse_engine_id
|
||||
)
|
||||
app.state.config.BRAVE_SEARCH_API_KEY = (
|
||||
form_data.web.search.brave_search_api_key
|
||||
)
|
||||
app.state.config.SERPSTACK_API_KEY = form_data.web.search.serpstack_api_key
|
||||
app.state.config.SERPSTACK_HTTPS = form_data.web.search.serpstack_https
|
||||
app.state.config.SERPER_API_KEY = form_data.web.search.serper_api_key
|
||||
app.state.config.SERPLY_API_KEY = form_data.web.search.serply_api_key
|
||||
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = form_data.web.search.result_count
|
||||
app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = (
|
||||
form_data.web.search.concurrent_requests
|
||||
)
|
||||
|
||||
return {
|
||||
"status": True,
|
||||
@@ -395,11 +491,27 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_
|
||||
"chunk_size": app.state.config.CHUNK_SIZE,
|
||||
"chunk_overlap": app.state.config.CHUNK_OVERLAP,
|
||||
},
|
||||
"web_loader_ssl_verification": app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
|
||||
"youtube": {
|
||||
"language": app.state.config.YOUTUBE_LOADER_LANGUAGE,
|
||||
"translation": app.state.YOUTUBE_LOADER_TRANSLATION,
|
||||
},
|
||||
"web": {
|
||||
"ssl_verification": app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
|
||||
"search": {
|
||||
"enabled": app.state.config.ENABLE_RAG_WEB_SEARCH,
|
||||
"engine": app.state.config.RAG_WEB_SEARCH_ENGINE,
|
||||
"searxng_query_url": app.state.config.SEARXNG_QUERY_URL,
|
||||
"google_pse_api_key": app.state.config.GOOGLE_PSE_API_KEY,
|
||||
"google_pse_engine_id": app.state.config.GOOGLE_PSE_ENGINE_ID,
|
||||
"brave_search_api_key": app.state.config.BRAVE_SEARCH_API_KEY,
|
||||
"serpstack_api_key": app.state.config.SERPSTACK_API_KEY,
|
||||
"serpstack_https": app.state.config.SERPSTACK_HTTPS,
|
||||
"serper_api_key": app.state.config.SERPER_API_KEY,
|
||||
"serply_api_key": app.state.config.SERPLY_API_KEY,
|
||||
"result_count": app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
||||
"concurrent_requests": app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -588,35 +700,158 @@ def store_web(form_data: UrlForm, user=Depends(get_current_user)):
|
||||
)
|
||||
|
||||
|
||||
def get_web_loader(url: str, verify_ssl: bool = True):
|
||||
def get_web_loader(url: Union[str, Sequence[str]], verify_ssl: bool = True):
|
||||
# Check if the URL is valid
|
||||
if isinstance(validators.url(url), validators.ValidationError):
|
||||
if not validate_url(url):
|
||||
raise ValueError(ERROR_MESSAGES.INVALID_URL)
|
||||
if not ENABLE_RAG_LOCAL_WEB_FETCH:
|
||||
# Local web fetch is disabled, filter out any URLs that resolve to private IP addresses
|
||||
parsed_url = urllib.parse.urlparse(url)
|
||||
# Get IPv4 and IPv6 addresses
|
||||
ipv4_addresses, ipv6_addresses = resolve_hostname(parsed_url.hostname)
|
||||
# Check if any of the resolved addresses are private
|
||||
# This is technically still vulnerable to DNS rebinding attacks, as we don't control WebBaseLoader
|
||||
for ip in ipv4_addresses:
|
||||
if validators.ipv4(ip, private=True):
|
||||
return SafeWebBaseLoader(
|
||||
url,
|
||||
verify_ssl=verify_ssl,
|
||||
requests_per_second=RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
||||
continue_on_failure=True,
|
||||
)
|
||||
|
||||
|
||||
def validate_url(url: Union[str, Sequence[str]]):
|
||||
if isinstance(url, str):
|
||||
if isinstance(validators.url(url), validators.ValidationError):
|
||||
raise ValueError(ERROR_MESSAGES.INVALID_URL)
|
||||
if not ENABLE_RAG_LOCAL_WEB_FETCH:
|
||||
# Check if the URL exists by making a HEAD request
|
||||
try:
|
||||
response = requests.head(url, allow_redirects=True)
|
||||
if response.status_code != 200:
|
||||
raise ValueError(ERROR_MESSAGES.INVALID_URL)
|
||||
except requests.exceptions.RequestException:
|
||||
raise ValueError(ERROR_MESSAGES.INVALID_URL)
|
||||
for ip in ipv6_addresses:
|
||||
if validators.ipv6(ip, private=True):
|
||||
raise ValueError(ERROR_MESSAGES.INVALID_URL)
|
||||
return WebBaseLoader(url, verify_ssl=verify_ssl)
|
||||
return 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)
|
||||
def search_web(engine: str, query: str) -> list[SearchResult]:
|
||||
"""Search the web using a search engine and return the results as a list of SearchResult objects.
|
||||
Will look for a search engine API key in environment variables in the following order:
|
||||
- SEARXNG_QUERY_URL
|
||||
- GOOGLE_PSE_API_KEY + GOOGLE_PSE_ENGINE_ID
|
||||
- BRAVE_SEARCH_API_KEY
|
||||
- SERPSTACK_API_KEY
|
||||
- SERPER_API_KEY
|
||||
- SERPLY_API_KEY
|
||||
|
||||
# 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]
|
||||
Args:
|
||||
query (str): The query to search for
|
||||
"""
|
||||
|
||||
return ipv4_addresses, ipv6_addresses
|
||||
# TODO: add playwright to search the web
|
||||
if engine == "searxng":
|
||||
if app.state.config.SEARXNG_QUERY_URL:
|
||||
return search_searxng(
|
||||
app.state.config.SEARXNG_QUERY_URL,
|
||||
query,
|
||||
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
||||
)
|
||||
else:
|
||||
raise Exception("No SEARXNG_QUERY_URL found in environment variables")
|
||||
elif engine == "google_pse":
|
||||
if (
|
||||
app.state.config.GOOGLE_PSE_API_KEY
|
||||
and app.state.config.GOOGLE_PSE_ENGINE_ID
|
||||
):
|
||||
return search_google_pse(
|
||||
app.state.config.GOOGLE_PSE_API_KEY,
|
||||
app.state.config.GOOGLE_PSE_ENGINE_ID,
|
||||
query,
|
||||
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
||||
)
|
||||
else:
|
||||
raise Exception(
|
||||
"No GOOGLE_PSE_API_KEY or GOOGLE_PSE_ENGINE_ID found in environment variables"
|
||||
)
|
||||
elif engine == "brave":
|
||||
if app.state.config.BRAVE_SEARCH_API_KEY:
|
||||
return search_brave(
|
||||
app.state.config.BRAVE_SEARCH_API_KEY,
|
||||
query,
|
||||
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
||||
)
|
||||
else:
|
||||
raise Exception("No BRAVE_SEARCH_API_KEY found in environment variables")
|
||||
elif engine == "serpstack":
|
||||
if app.state.config.SERPSTACK_API_KEY:
|
||||
return search_serpstack(
|
||||
app.state.config.SERPSTACK_API_KEY,
|
||||
query,
|
||||
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
||||
https_enabled=app.state.config.SERPSTACK_HTTPS,
|
||||
)
|
||||
else:
|
||||
raise Exception("No SERPSTACK_API_KEY found in environment variables")
|
||||
elif engine == "serper":
|
||||
if app.state.config.SERPER_API_KEY:
|
||||
return search_serper(
|
||||
app.state.config.SERPER_API_KEY,
|
||||
query,
|
||||
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
||||
)
|
||||
else:
|
||||
raise Exception("No SERPER_API_KEY found in environment variables")
|
||||
elif engine == "serply":
|
||||
if app.state.config.SERPLY_API_KEY:
|
||||
return search_serply(
|
||||
app.state.config.SERPLY_API_KEY,
|
||||
query,
|
||||
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
||||
)
|
||||
else:
|
||||
raise Exception("No SERPLY_API_KEY found in environment variables")
|
||||
elif engine == "duckduckgo":
|
||||
return search_duckduckgo(query, app.state.config.RAG_WEB_SEARCH_RESULT_COUNT)
|
||||
else:
|
||||
raise Exception("No search engine API key found in environment variables")
|
||||
|
||||
|
||||
@app.post("/web/search")
|
||||
def store_web_search(form_data: SearchForm, user=Depends(get_current_user)):
|
||||
try:
|
||||
logging.info(
|
||||
f"trying to web search with {app.state.config.RAG_WEB_SEARCH_ENGINE, form_data.query}"
|
||||
)
|
||||
web_results = search_web(
|
||||
app.state.config.RAG_WEB_SEARCH_ENGINE, form_data.query
|
||||
)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
||||
print(e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.WEB_SEARCH_ERROR(e),
|
||||
)
|
||||
|
||||
try:
|
||||
urls = [result.link for result in web_results]
|
||||
loader = get_web_loader(urls)
|
||||
data = loader.load()
|
||||
|
||||
collection_name = form_data.collection_name
|
||||
if collection_name == "":
|
||||
collection_name = calculate_sha256_string(form_data.query)[:63]
|
||||
|
||||
store_data_in_vector_db(data, collection_name, overwrite=True)
|
||||
return {
|
||||
"status": True,
|
||||
"collection_name": collection_name,
|
||||
"filenames": urls,
|
||||
}
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||
)
|
||||
|
||||
|
||||
def store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> bool:
|
||||
@@ -654,6 +889,13 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b
|
||||
texts = [doc.page_content for doc in docs]
|
||||
metadatas = [doc.metadata for doc in docs]
|
||||
|
||||
# ChromaDB does not like datetime formats
|
||||
# for meta-data so convert them to string.
|
||||
for metadata in metadatas:
|
||||
for key, value in metadata.items():
|
||||
if isinstance(value, datetime):
|
||||
metadata[key] = str(value)
|
||||
|
||||
try:
|
||||
if overwrite:
|
||||
for collection in CHROMA_CLIENT.list_collections():
|
||||
@@ -669,6 +911,7 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b
|
||||
app.state.sentence_transformer_ef,
|
||||
app.state.config.OPENAI_API_KEY,
|
||||
app.state.config.OPENAI_API_BASE_URL,
|
||||
app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
|
||||
)
|
||||
|
||||
embedding_texts = list(map(lambda x: x.replace("\n", " "), texts))
|
||||
@@ -739,6 +982,7 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
|
||||
"swift",
|
||||
"vue",
|
||||
"svelte",
|
||||
"msg",
|
||||
]
|
||||
|
||||
if file_ext == "pdf":
|
||||
@@ -768,6 +1012,13 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
|
||||
"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
|
||||
):
|
||||
@@ -933,6 +1184,30 @@ def reset_vector_db(user=Depends(get_admin_user)):
|
||||
CHROMA_CLIENT.reset()
|
||||
|
||||
|
||||
@app.get("/reset/uploads")
|
||||
def reset_upload_dir(user=Depends(get_admin_user)) -> bool:
|
||||
folder = f"{UPLOAD_DIR}"
|
||||
try:
|
||||
# Check if the directory exists
|
||||
if os.path.exists(folder):
|
||||
# Iterate over all the files and directories in the specified directory
|
||||
for filename in os.listdir(folder):
|
||||
file_path = os.path.join(folder, filename)
|
||||
try:
|
||||
if os.path.isfile(file_path) or os.path.islink(file_path):
|
||||
os.unlink(file_path) # Remove the file or link
|
||||
elif os.path.isdir(file_path):
|
||||
shutil.rmtree(file_path) # Remove the directory
|
||||
except Exception as e:
|
||||
print(f"Failed to delete {file_path}. Reason: {e}")
|
||||
else:
|
||||
print(f"The directory {folder} does not exist")
|
||||
except Exception as e:
|
||||
print(f"Failed to process the directory {folder}. Reason: {e}")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@app.get("/reset")
|
||||
def reset(user=Depends(get_admin_user)) -> bool:
|
||||
folder = f"{UPLOAD_DIR}"
|
||||
@@ -954,6 +1229,33 @@ def reset(user=Depends(get_admin_user)) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class SafeWebBaseLoader(WebBaseLoader):
|
||||
"""WebBaseLoader with enhanced error handling for URLs."""
|
||||
|
||||
def lazy_load(self) -> Iterator[Document]:
|
||||
"""Lazy load text from the url(s) in web_path with error handling."""
|
||||
for path in self.web_paths:
|
||||
try:
|
||||
soup = self._scrape(path, bs_kwargs=self.bs_kwargs)
|
||||
text = soup.get_text(**self.bs_get_text_kwargs)
|
||||
|
||||
# Build metadata
|
||||
metadata = {"source": path}
|
||||
if title := soup.find("title"):
|
||||
metadata["title"] = title.get_text()
|
||||
if description := soup.find("meta", attrs={"name": "description"}):
|
||||
metadata["description"] = description.get(
|
||||
"content", "No description found."
|
||||
)
|
||||
if html := soup.find("html"):
|
||||
metadata["language"] = html.get("lang", "No language found.")
|
||||
|
||||
yield Document(page_content=text, metadata=metadata)
|
||||
except Exception as e:
|
||||
# Log the error and continue with the next URL
|
||||
log.error(f"Error loading {path}: {e}")
|
||||
|
||||
|
||||
if ENV == "dev":
|
||||
|
||||
@app.get("/ef")
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from apps.rag.search.main import SearchResult
|
||||
from config import SRC_LOG_LEVELS
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||
|
||||
|
||||
def search_brave(api_key: str, query: str, count: int) -> list[SearchResult]:
|
||||
"""Search using Brave's Search API and return the results as a list of SearchResult objects.
|
||||
|
||||
Args:
|
||||
api_key (str): A Brave Search API key
|
||||
query (str): The query to search for
|
||||
"""
|
||||
url = "https://api.search.brave.com/res/v1/web/search"
|
||||
headers = {
|
||||
"Accept": "application/json",
|
||||
"Accept-Encoding": "gzip",
|
||||
"X-Subscription-Token": api_key,
|
||||
}
|
||||
params = {"q": query, "count": count}
|
||||
|
||||
response = requests.get(url, headers=headers, params=params)
|
||||
response.raise_for_status()
|
||||
|
||||
json_response = response.json()
|
||||
results = json_response.get("web", {}).get("results", [])
|
||||
return [
|
||||
SearchResult(
|
||||
link=result["url"], title=result.get("title"), snippet=result.get("snippet")
|
||||
)
|
||||
for result in results[:count]
|
||||
]
|
||||
@@ -0,0 +1,46 @@
|
||||
import logging
|
||||
|
||||
from apps.rag.search.main import SearchResult
|
||||
from duckduckgo_search import DDGS
|
||||
from config import SRC_LOG_LEVELS
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||
|
||||
|
||||
def search_duckduckgo(query: str, count: int) -> list[SearchResult]:
|
||||
"""
|
||||
Search using DuckDuckGo's Search API and return the results as a list of SearchResult objects.
|
||||
Args:
|
||||
query (str): The query to search for
|
||||
count (int): The number of results to return
|
||||
|
||||
Returns:
|
||||
List[SearchResult]: A list of search results
|
||||
"""
|
||||
# Use the DDGS context manager to create a DDGS object
|
||||
with DDGS() as ddgs:
|
||||
# Use the ddgs.text() method to perform the search
|
||||
ddgs_gen = ddgs.text(
|
||||
query, safesearch="moderate", max_results=count, backend="api"
|
||||
)
|
||||
# Check if there are search results
|
||||
if ddgs_gen:
|
||||
# Convert the search results into a list
|
||||
search_results = [r for r in ddgs_gen]
|
||||
|
||||
# Create an empty list to store the SearchResult objects
|
||||
results = []
|
||||
# Iterate over each search result
|
||||
for result in search_results:
|
||||
# Create a SearchResult object and append it to the results list
|
||||
results.append(
|
||||
SearchResult(
|
||||
link=result["href"],
|
||||
title=result.get("title"),
|
||||
snippet=result.get("body"),
|
||||
)
|
||||
)
|
||||
print(results)
|
||||
# Return the list of search results
|
||||
return results
|
||||
@@ -0,0 +1,45 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from apps.rag.search.main import SearchResult
|
||||
from config import SRC_LOG_LEVELS
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||
|
||||
|
||||
def search_google_pse(
|
||||
api_key: str, search_engine_id: str, query: str, count: int
|
||||
) -> list[SearchResult]:
|
||||
"""Search using Google's Programmable Search Engine API and return the results as a list of SearchResult objects.
|
||||
|
||||
Args:
|
||||
api_key (str): A Programmable Search Engine API key
|
||||
search_engine_id (str): A Programmable Search Engine ID
|
||||
query (str): The query to search for
|
||||
"""
|
||||
url = "https://www.googleapis.com/customsearch/v1"
|
||||
|
||||
headers = {"Content-Type": "application/json"}
|
||||
params = {
|
||||
"cx": search_engine_id,
|
||||
"q": query,
|
||||
"key": api_key,
|
||||
"num": count,
|
||||
}
|
||||
|
||||
response = requests.request("GET", url, headers=headers, params=params)
|
||||
response.raise_for_status()
|
||||
|
||||
json_response = response.json()
|
||||
results = json_response.get("items", [])
|
||||
return [
|
||||
SearchResult(
|
||||
link=result["link"],
|
||||
title=result.get("title"),
|
||||
snippet=result.get("snippet"),
|
||||
)
|
||||
for result in results
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class SearchResult(BaseModel):
|
||||
link: str
|
||||
title: Optional[str]
|
||||
snippet: Optional[str]
|
||||
@@ -0,0 +1,83 @@
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from typing import List
|
||||
|
||||
from apps.rag.search.main import SearchResult
|
||||
from config import SRC_LOG_LEVELS
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||
|
||||
|
||||
def search_searxng(
|
||||
query_url: str, query: str, count: int, **kwargs
|
||||
) -> List[SearchResult]:
|
||||
"""
|
||||
Search a SearXNG instance for a given query and return the results as a list of SearchResult objects.
|
||||
|
||||
The function allows passing additional parameters such as language or time_range to tailor the search result.
|
||||
|
||||
Args:
|
||||
query_url (str): The base URL of the SearXNG server.
|
||||
query (str): The search term or question to find in the SearXNG database.
|
||||
count (int): The maximum number of results to retrieve from the search.
|
||||
|
||||
Keyword Args:
|
||||
language (str): Language filter for the search results; e.g., "en-US". Defaults to an empty string.
|
||||
time_range (str): Time range for filtering results by date; e.g., "2023-04-05..today" or "all-time". Defaults to ''.
|
||||
categories: (Optional[List[str]]): Specific categories within which the search should be performed, defaulting to an empty string if not provided.
|
||||
|
||||
Returns:
|
||||
List[SearchResult]: A list of SearchResults sorted by relevance score in descending order.
|
||||
|
||||
Raise:
|
||||
requests.exceptions.RequestException: If a request error occurs during the search process.
|
||||
"""
|
||||
|
||||
# Default values for optional parameters are provided as empty strings or None when not specified.
|
||||
language = kwargs.get("language", "en-US")
|
||||
time_range = kwargs.get("time_range", "")
|
||||
categories = "".join(kwargs.get("categories", []))
|
||||
|
||||
params = {
|
||||
"q": query,
|
||||
"format": "json",
|
||||
"pageno": 1,
|
||||
"language": language,
|
||||
"time_range": time_range,
|
||||
"categories": categories,
|
||||
"theme": "simple",
|
||||
"image_proxy": 0,
|
||||
}
|
||||
|
||||
# Legacy query format
|
||||
if "<query>" in query_url:
|
||||
# Strip all query parameters from the URL
|
||||
query_url = query_url.split("?")[0]
|
||||
|
||||
log.debug(f"searching {query_url}")
|
||||
|
||||
response = requests.get(
|
||||
query_url,
|
||||
headers={
|
||||
"User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) RAG Bot",
|
||||
"Accept": "text/html",
|
||||
"Accept-Encoding": "gzip, deflate",
|
||||
"Accept-Language": "en-US,en;q=0.5",
|
||||
"Connection": "keep-alive",
|
||||
},
|
||||
params=params,
|
||||
)
|
||||
|
||||
response.raise_for_status() # Raise an exception for HTTP errors.
|
||||
|
||||
json_response = response.json()
|
||||
results = json_response.get("results", [])
|
||||
sorted_results = sorted(results, key=lambda x: x.get("score", 0), reverse=True)
|
||||
return [
|
||||
SearchResult(
|
||||
link=result["url"], title=result.get("title"), snippet=result.get("content")
|
||||
)
|
||||
for result in sorted_results[:count]
|
||||
]
|
||||
@@ -0,0 +1,39 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from apps.rag.search.main import SearchResult
|
||||
from config import SRC_LOG_LEVELS
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||
|
||||
|
||||
def search_serper(api_key: str, query: str, count: int) -> list[SearchResult]:
|
||||
"""Search using serper.dev's API and return the results as a list of SearchResult objects.
|
||||
|
||||
Args:
|
||||
api_key (str): A serper.dev API key
|
||||
query (str): The query to search for
|
||||
"""
|
||||
url = "https://google.serper.dev/search"
|
||||
|
||||
payload = json.dumps({"q": query})
|
||||
headers = {"X-API-KEY": api_key, "Content-Type": "application/json"}
|
||||
|
||||
response = requests.request("POST", url, headers=headers, data=payload)
|
||||
response.raise_for_status()
|
||||
|
||||
json_response = response.json()
|
||||
results = sorted(
|
||||
json_response.get("organic", []), key=lambda x: x.get("position", 0)
|
||||
)
|
||||
return [
|
||||
SearchResult(
|
||||
link=result["link"],
|
||||
title=result.get("title"),
|
||||
snippet=result.get("description"),
|
||||
)
|
||||
for result in results[:count]
|
||||
]
|
||||
@@ -0,0 +1,68 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
import requests
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from apps.rag.search.main import SearchResult
|
||||
from config import SRC_LOG_LEVELS
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||
|
||||
|
||||
def search_serply(
|
||||
api_key: str,
|
||||
query: str,
|
||||
count: int,
|
||||
hl: str = "us",
|
||||
limit: int = 10,
|
||||
device_type: str = "desktop",
|
||||
proxy_location: str = "US",
|
||||
) -> list[SearchResult]:
|
||||
"""Search using serper.dev's API and return the results as a list of SearchResult objects.
|
||||
|
||||
Args:
|
||||
api_key (str): A serply.io API key
|
||||
query (str): The query to search for
|
||||
hl (str): Host Language code to display results in (reference https://developers.google.com/custom-search/docs/xml_results?hl=en#wsInterfaceLanguages)
|
||||
limit (int): The maximum number of results to return [10-100, defaults to 10]
|
||||
"""
|
||||
log.info("Searching with Serply")
|
||||
|
||||
url = "https://api.serply.io/v1/search/"
|
||||
|
||||
query_payload = {
|
||||
"q": query,
|
||||
"language": "en",
|
||||
"num": limit,
|
||||
"gl": proxy_location.upper(),
|
||||
"hl": hl.lower(),
|
||||
}
|
||||
|
||||
url = f"{url}{urlencode(query_payload)}"
|
||||
headers = {
|
||||
"X-API-KEY": api_key,
|
||||
"X-User-Agent": device_type,
|
||||
"User-Agent": "open-webui",
|
||||
"X-Proxy-Location": proxy_location,
|
||||
}
|
||||
|
||||
response = requests.request("GET", url, headers=headers)
|
||||
response.raise_for_status()
|
||||
|
||||
json_response = response.json()
|
||||
log.info(f"results from serply search: {json_response}")
|
||||
|
||||
results = sorted(
|
||||
json_response.get("results", []), key=lambda x: x.get("realPosition", 0)
|
||||
)
|
||||
|
||||
return [
|
||||
SearchResult(
|
||||
link=result["link"],
|
||||
title=result.get("title"),
|
||||
snippet=result.get("description"),
|
||||
)
|
||||
for result in results[:count]
|
||||
]
|
||||
@@ -0,0 +1,43 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from apps.rag.search.main import SearchResult
|
||||
from config import SRC_LOG_LEVELS
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||
|
||||
|
||||
def search_serpstack(
|
||||
api_key: str, query: str, count: int, https_enabled: bool = True
|
||||
) -> list[SearchResult]:
|
||||
"""Search using serpstack.com's and return the results as a list of SearchResult objects.
|
||||
|
||||
Args:
|
||||
api_key (str): A serpstack.com API key
|
||||
query (str): The query to search for
|
||||
https_enabled (bool): Whether to use HTTPS or HTTP for the API request
|
||||
"""
|
||||
url = f"{'https' if https_enabled else 'http'}://api.serpstack.com/search"
|
||||
|
||||
headers = {"Content-Type": "application/json"}
|
||||
params = {
|
||||
"access_key": api_key,
|
||||
"query": query,
|
||||
}
|
||||
|
||||
response = requests.request("POST", url, headers=headers, params=params)
|
||||
response.raise_for_status()
|
||||
|
||||
json_response = response.json()
|
||||
results = sorted(
|
||||
json_response.get("organic_results", []), key=lambda x: x.get("position", 0)
|
||||
)
|
||||
return [
|
||||
SearchResult(
|
||||
link=result["url"], title=result.get("title"), snippet=result.get("snippet")
|
||||
)
|
||||
for result in results[:count]
|
||||
]
|
||||
+998
@@ -0,0 +1,998 @@
|
||||
{
|
||||
"query": {
|
||||
"original": "python",
|
||||
"show_strict_warning": false,
|
||||
"is_navigational": true,
|
||||
"is_news_breaking": false,
|
||||
"spellcheck_off": true,
|
||||
"country": "us",
|
||||
"bad_results": false,
|
||||
"should_fallback": false,
|
||||
"postal_code": "",
|
||||
"city": "",
|
||||
"header_country": "",
|
||||
"more_results_available": true,
|
||||
"state": ""
|
||||
},
|
||||
"mixed": {
|
||||
"type": "mixed",
|
||||
"main": [
|
||||
{
|
||||
"type": "web",
|
||||
"index": 0,
|
||||
"all": false
|
||||
},
|
||||
{
|
||||
"type": "web",
|
||||
"index": 1,
|
||||
"all": false
|
||||
},
|
||||
{
|
||||
"type": "news",
|
||||
"all": true
|
||||
},
|
||||
{
|
||||
"type": "web",
|
||||
"index": 2,
|
||||
"all": false
|
||||
},
|
||||
{
|
||||
"type": "videos",
|
||||
"all": true
|
||||
},
|
||||
{
|
||||
"type": "web",
|
||||
"index": 3,
|
||||
"all": false
|
||||
},
|
||||
{
|
||||
"type": "web",
|
||||
"index": 4,
|
||||
"all": false
|
||||
},
|
||||
{
|
||||
"type": "web",
|
||||
"index": 5,
|
||||
"all": false
|
||||
},
|
||||
{
|
||||
"type": "web",
|
||||
"index": 6,
|
||||
"all": false
|
||||
},
|
||||
{
|
||||
"type": "web",
|
||||
"index": 7,
|
||||
"all": false
|
||||
},
|
||||
{
|
||||
"type": "web",
|
||||
"index": 8,
|
||||
"all": false
|
||||
},
|
||||
{
|
||||
"type": "web",
|
||||
"index": 9,
|
||||
"all": false
|
||||
},
|
||||
{
|
||||
"type": "web",
|
||||
"index": 10,
|
||||
"all": false
|
||||
},
|
||||
{
|
||||
"type": "web",
|
||||
"index": 11,
|
||||
"all": false
|
||||
},
|
||||
{
|
||||
"type": "web",
|
||||
"index": 12,
|
||||
"all": false
|
||||
},
|
||||
{
|
||||
"type": "web",
|
||||
"index": 13,
|
||||
"all": false
|
||||
},
|
||||
{
|
||||
"type": "web",
|
||||
"index": 14,
|
||||
"all": false
|
||||
},
|
||||
{
|
||||
"type": "web",
|
||||
"index": 15,
|
||||
"all": false
|
||||
},
|
||||
{
|
||||
"type": "web",
|
||||
"index": 16,
|
||||
"all": false
|
||||
},
|
||||
{
|
||||
"type": "web",
|
||||
"index": 17,
|
||||
"all": false
|
||||
},
|
||||
{
|
||||
"type": "web",
|
||||
"index": 18,
|
||||
"all": false
|
||||
},
|
||||
{
|
||||
"type": "web",
|
||||
"index": 19,
|
||||
"all": false
|
||||
}
|
||||
],
|
||||
"top": [],
|
||||
"side": []
|
||||
},
|
||||
"news": {
|
||||
"type": "news",
|
||||
"results": [
|
||||
{
|
||||
"title": "Google lays off staff from Flutter, Dart and Python teams weeks before its developer conference | TechCrunch",
|
||||
"url": "https://techcrunch.com/2024/05/01/google-lays-off-staff-from-flutter-dart-python-weeks-before-its-developer-conference/",
|
||||
"is_source_local": false,
|
||||
"is_source_both": false,
|
||||
"description": "Google told TechCrunch that Flutter will have new updates to share at I/O this year.",
|
||||
"page_age": "2024-05-02T17:40:05",
|
||||
"family_friendly": true,
|
||||
"meta_url": {
|
||||
"scheme": "https",
|
||||
"netloc": "techcrunch.com",
|
||||
"hostname": "techcrunch.com",
|
||||
"favicon": "https://imgs.search.brave.com/N6VSEVahheQOb7lqfb47dhUOB4XD-6sfQOP94sCe3Oo/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZGI5Njk0Yzlk/YWM3ZWMwZjg1MTM1/NmIyMWEyNzBjZDZj/ZDQyNmFlNGU0NDRi/MDgyYjQwOGU0Y2Qy/ZWMwNWQ2ZC90ZWNo/Y3J1bmNoLmNvbS8",
|
||||
"path": "› 2024 › 05 › 01 › google-lays-off-staff-from-flutter-dart-python-weeks-before-its-developer-conference"
|
||||
},
|
||||
"breaking": false,
|
||||
"thumbnail": {
|
||||
"src": "https://imgs.search.brave.com/gCI5UG8muOEOZDAx9vpu6L6r6R00mD7jOF08-biFoyQ/rs:fit:200:200:1/g:ce/aHR0cHM6Ly90ZWNo/Y3J1bmNoLmNvbS93/cC1jb250ZW50L3Vw/bG9hZHMvMjAxOC8x/MS9HZXR0eUltYWdl/cy0xMDAyNDg0NzQ2/LmpwZz9yZXNpemU9/MTIwMCw4MDA"
|
||||
},
|
||||
"age": "3 days ago",
|
||||
"extra_snippets": [
|
||||
"Ahead of Google’s annual I/O developer conference in May, the tech giant has laid off staff across key teams like Flutter, Dart, Python and others, according to reports from affected employees shared on social media. Google confirmed the layoffs to TechCrunch, but not the specific teams, roles or how many people were let go.",
|
||||
"In a separate post on Reddit, another commenter noted the Python team affected by the layoffs were those who managed the internal Python runtimes and toolchains and worked with OSS Python. Included in this group were “multiple current and former core devs and steering council members,” they said.",
|
||||
"Meanwhile, others shared on Y Combinator’s Hacker News, where a Python team member detailed their specific duties on the technical front and noted that, for years, much of the work was done with fewer than 10 people. Another Hacker News commenter said their early years on the Python team were spent paying down internal technical debt accumulated from not having a strong Python strategy.",
|
||||
"CNBC reports that a total of 200 people were let go across Google’s “Core” teams, which included those working on Python, app platforms, and other engineering roles. Some jobs were being shifted to India and Mexico, it said, citing internal documents."
|
||||
]
|
||||
}
|
||||
],
|
||||
"mutated_by_goggles": false
|
||||
},
|
||||
"type": "search",
|
||||
"videos": {
|
||||
"type": "videos",
|
||||
"results": [
|
||||
{
|
||||
"type": "video_result",
|
||||
"url": "https://www.youtube.com/watch?v=b093aqAZiPU",
|
||||
"title": "👩💻 Python for Beginners Tutorial - YouTube",
|
||||
"description": "In this step-by-step Python for beginner's tutorial, learn how you can get started programming in Python. In this video, I assume that you are completely new...",
|
||||
"age": "March 25, 2021",
|
||||
"page_age": "2021-03-25T10:00:08",
|
||||
"video": {},
|
||||
"meta_url": {
|
||||
"scheme": "https",
|
||||
"netloc": "youtube.com",
|
||||
"hostname": "www.youtube.com",
|
||||
"favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v",
|
||||
"path": "› watch"
|
||||
},
|
||||
"thumbnail": {
|
||||
"src": "https://imgs.search.brave.com/tZI4Do4_EYcTCsD_MvE3Jx8FzjIXwIJ5ZuKhwiWTyZs/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9i/MDkzYXFBWmlQVS9t/YXhyZXNkZWZhdWx0/LmpwZw"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "video_result",
|
||||
"url": "https://www.youtube.com/watch?v=rfscVS0vtbw",
|
||||
"title": "Learn Python - Full Course for Beginners [Tutorial] - YouTube",
|
||||
"description": "This course will give you a full introduction into all of the core concepts in python. Follow along with the videos and you'll be a python programmer in no t...",
|
||||
"age": "July 11, 2018",
|
||||
"page_age": "2018-07-11T18:00:42",
|
||||
"video": {},
|
||||
"meta_url": {
|
||||
"scheme": "https",
|
||||
"netloc": "youtube.com",
|
||||
"hostname": "www.youtube.com",
|
||||
"favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v",
|
||||
"path": "› watch"
|
||||
},
|
||||
"thumbnail": {
|
||||
"src": "https://imgs.search.brave.com/65zkx_kPU_zJb-4nmvvY-q5-ZZwzceChz-N00V8cqvk/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9y/ZnNjVlMwdnRidy9t/YXhyZXNkZWZhdWx0/LmpwZw"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "video_result",
|
||||
"url": "https://www.youtube.com/watch?v=_uQrJ0TkZlc",
|
||||
"title": "Python Tutorial - Python Full Course for Beginners - YouTube",
|
||||
"description": "Become a Python pro! 🚀 This comprehensive tutorial takes you from beginner to hero, covering the basics, machine learning, and web development projects.🚀 W...",
|
||||
"age": "February 18, 2019",
|
||||
"page_age": "2019-02-18T15:00:08",
|
||||
"video": {},
|
||||
"meta_url": {
|
||||
"scheme": "https",
|
||||
"netloc": "youtube.com",
|
||||
"hostname": "www.youtube.com",
|
||||
"favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v",
|
||||
"path": "› watch"
|
||||
},
|
||||
"thumbnail": {
|
||||
"src": "https://imgs.search.brave.com/Djiv1pXLq1ClqBSE_86jQnEYR8bW8UJP6Cs7LrgyQzQ/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9f/dVFySjBUa1psYy9t/YXhyZXNkZWZhdWx0/LmpwZw"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "video_result",
|
||||
"url": "https://www.youtube.com/watch?v=wRKgzC-MhIc",
|
||||
"title": "[] and {} vs list() and dict(), which is better?",
|
||||
"description": "Enjoy the videos and music you love, upload original content, and share it all with friends, family, and the world on YouTube.",
|
||||
"video": {},
|
||||
"meta_url": {
|
||||
"scheme": "https",
|
||||
"netloc": "youtube.com",
|
||||
"hostname": "www.youtube.com",
|
||||
"favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v",
|
||||
"path": "› watch"
|
||||
},
|
||||
"thumbnail": {
|
||||
"src": "https://imgs.search.brave.com/Hw9ep2Pio13X1VZjRw_h9R2VH_XvZFOuGlQJVnVkeq0/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS93/UktnekMtTWhJYy9o/cWRlZmF1bHQuanBn"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "video_result",
|
||||
"url": "https://www.youtube.com/watch?v=LWdsF79H1Pg",
|
||||
"title": "print() vs. return in Python Functions - YouTube",
|
||||
"description": "In this video, you will learn the differences between the return statement and the print function when they are used inside Python functions. We will see an ...",
|
||||
"age": "June 11, 2022",
|
||||
"page_age": "2022-06-11T21:33:26",
|
||||
"video": {},
|
||||
"meta_url": {
|
||||
"scheme": "https",
|
||||
"netloc": "youtube.com",
|
||||
"hostname": "www.youtube.com",
|
||||
"favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v",
|
||||
"path": "› watch"
|
||||
},
|
||||
"thumbnail": {
|
||||
"src": "https://imgs.search.brave.com/ebglnr5_jwHHpvon3WU-5hzt0eHdTZSVGg3Ts6R38xY/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9M/V2RzRjc5SDFQZy9t/YXhyZXNkZWZhdWx0/LmpwZw"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "video_result",
|
||||
"url": "https://www.youtube.com/watch?v=AovxLr8jUH4",
|
||||
"title": "Python Tutorial for Beginners 5 - Python print() and input() Function ...",
|
||||
"description": "In this Video I am going to show How to use print() Function and input() Function in Python. In python The print() function is used to print the specified ...",
|
||||
"age": "August 28, 2018",
|
||||
"page_age": "2018-08-28T20:11:09",
|
||||
"video": {},
|
||||
"meta_url": {
|
||||
"scheme": "https",
|
||||
"netloc": "youtube.com",
|
||||
"hostname": "www.youtube.com",
|
||||
"favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v",
|
||||
"path": "› watch"
|
||||
},
|
||||
"thumbnail": {
|
||||
"src": "https://imgs.search.brave.com/nCoLEcWkKtiecprWbS6nufwGCaSbPH7o0-sMeIkFmjI/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9B/b3Z4THI4alVINC9o/cWRlZmF1bHQuanBn"
|
||||
}
|
||||
}
|
||||
],
|
||||
"mutated_by_goggles": false
|
||||
},
|
||||
"web": {
|
||||
"type": "search",
|
||||
"results": [
|
||||
{
|
||||
"title": "Welcome to Python.org",
|
||||
"url": "https://www.python.org",
|
||||
"is_source_local": false,
|
||||
"is_source_both": false,
|
||||
"description": "The official home of the <strong>Python</strong> Programming Language",
|
||||
"page_age": "2023-09-09T15:55:05",
|
||||
"profile": {
|
||||
"name": "Python",
|
||||
"url": "https://www.python.org",
|
||||
"long_name": "python.org",
|
||||
"img": "https://imgs.search.brave.com/vBaRH-v6oPS4csO4cdvuKhZ7-xDVvydin3oe3zXYxAI/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNTJjMzZjNDBj/MmIzODgwMGUyOTRj/Y2E5MjM3YjRkYTZj/YWI1Yzk1NTlmYTgw/ZDBjNzM0MGMxZjQz/YWFjNTczYy93d3cu/cHl0aG9uLm9yZy8"
|
||||
},
|
||||
"language": "en",
|
||||
"family_friendly": true,
|
||||
"type": "search_result",
|
||||
"subtype": "generic",
|
||||
"meta_url": {
|
||||
"scheme": "https",
|
||||
"netloc": "python.org",
|
||||
"hostname": "www.python.org",
|
||||
"favicon": "https://imgs.search.brave.com/vBaRH-v6oPS4csO4cdvuKhZ7-xDVvydin3oe3zXYxAI/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNTJjMzZjNDBj/MmIzODgwMGUyOTRj/Y2E5MjM3YjRkYTZj/YWI1Yzk1NTlmYTgw/ZDBjNzM0MGMxZjQz/YWFjNTczYy93d3cu/cHl0aG9uLm9yZy8",
|
||||
"path": ""
|
||||
},
|
||||
"thumbnail": {
|
||||
"src": "https://imgs.search.brave.com/GGfNfe5rxJ8QWEoxXniSLc0-POLU3qPyTIpuqPdbmXk/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/cHl0aG9uLm9yZy9z/dGF0aWMvb3Blbmdy/YXBoLWljb24tMjAw/eDIwMC5wbmc",
|
||||
"original": "https://www.python.org/static/opengraph-icon-200x200.png",
|
||||
"logo": false
|
||||
},
|
||||
"age": "September 9, 2023",
|
||||
"cluster_type": "generic",
|
||||
"cluster": [
|
||||
{
|
||||
"title": "Downloads",
|
||||
"url": "https://www.python.org/downloads/",
|
||||
"is_source_local": false,
|
||||
"is_source_both": false,
|
||||
"description": "The official home of the <strong>Python</strong> Programming Language",
|
||||
"family_friendly": true
|
||||
},
|
||||
{
|
||||
"title": "Macos",
|
||||
"url": "https://www.python.org/downloads/macos/",
|
||||
"is_source_local": false,
|
||||
"is_source_both": false,
|
||||
"description": "The official home of the <strong>Python</strong> Programming Language",
|
||||
"family_friendly": true
|
||||
},
|
||||
{
|
||||
"title": "Windows",
|
||||
"url": "https://www.python.org/downloads/windows/",
|
||||
"is_source_local": false,
|
||||
"is_source_both": false,
|
||||
"description": "The official home of the <strong>Python</strong> Programming Language",
|
||||
"family_friendly": true
|
||||
},
|
||||
{
|
||||
"title": "Getting Started",
|
||||
"url": "https://www.python.org/about/gettingstarted/",
|
||||
"is_source_local": false,
|
||||
"is_source_both": false,
|
||||
"description": "The official home of the <strong>Python</strong> Programming Language",
|
||||
"family_friendly": true
|
||||
}
|
||||
],
|
||||
"extra_snippets": [
|
||||
"Calculations are simple with Python, and expression syntax is straightforward: the operators +, -, * and / work as expected; parentheses () can be used for grouping. More about simple math functions in Python 3.",
|
||||
"The core of extensible programming is defining functions. Python allows mandatory and optional arguments, keyword arguments, and even arbitrary argument lists. More about defining functions in Python 3",
|
||||
"Lists (known as arrays in other languages) are one of the compound data types that Python understands. Lists can be indexed, sliced and manipulated with other built-in functions. More about lists in Python 3",
|
||||
"# Python 3: Simple output (with Unicode) >>> print(\"Hello, I'm Python!\") Hello, I'm Python! # Input, assignment >>> name = input('What is your name?\\n') >>> print('Hi, %s.' % name) What is your name? Python Hi, Python."
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Python (programming language) - Wikipedia",
|
||||
"url": "https://en.wikipedia.org/wiki/Python_(programming_language)",
|
||||
"is_source_local": false,
|
||||
"is_source_both": false,
|
||||
"description": "<strong>Python</strong> is a high-level, general-purpose programming language. Its design philosophy emphasizes code readability with the use of significant indentation. <strong>Python</strong> is dynamically typed and garbage-collected. It supports multiple programming paradigms, including structured (particularly procedural), ...",
|
||||
"page_age": "2024-05-01T12:54:03",
|
||||
"profile": {
|
||||
"name": "Wikipedia",
|
||||
"url": "https://en.wikipedia.org/wiki/Python_(programming_language)",
|
||||
"long_name": "en.wikipedia.org",
|
||||
"img": "https://imgs.search.brave.com/0kxnVOiqv-faZvOJc7zpym4Zin1CTs1f1svfNZSzmfU/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNjQwNGZhZWY0/ZTQ1YWUzYzQ3MDUw/MmMzMGY3NTQ0ZjNj/NDUwMDk5ZTI3MWRk/NWYyNTM4N2UwOTE0/NTI3ZDQzNy9lbi53/aWtpcGVkaWEub3Jn/Lw"
|
||||
},
|
||||
"language": "en",
|
||||
"family_friendly": true,
|
||||
"type": "search_result",
|
||||
"subtype": "generic",
|
||||
"meta_url": {
|
||||
"scheme": "https",
|
||||
"netloc": "en.wikipedia.org",
|
||||
"hostname": "en.wikipedia.org",
|
||||
"favicon": "https://imgs.search.brave.com/0kxnVOiqv-faZvOJc7zpym4Zin1CTs1f1svfNZSzmfU/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNjQwNGZhZWY0/ZTQ1YWUzYzQ3MDUw/MmMzMGY3NTQ0ZjNj/NDUwMDk5ZTI3MWRk/NWYyNTM4N2UwOTE0/NTI3ZDQzNy9lbi53/aWtpcGVkaWEub3Jn/Lw",
|
||||
"path": "› wiki › Python_(programming_language)"
|
||||
},
|
||||
"age": "4 days ago",
|
||||
"extra_snippets": [
|
||||
"Python is dynamically typed and garbage-collected. It supports multiple programming paradigms, including structured (particularly procedural), object-oriented and functional programming. It is often described as a \"batteries included\" language due to its comprehensive standard library.",
|
||||
"Guido van Rossum began working on Python in the late 1980s as a successor to the ABC programming language and first released it in 1991 as Python 0.9.0. Python 2.0 was released in 2000. Python 3.0, released in 2008, was a major revision not completely backward-compatible with earlier versions. Python 2.7.18, released in 2020, was the last release of Python 2.",
|
||||
"Python was invented in the late 1980s by Guido van Rossum at Centrum Wiskunde & Informatica (CWI) in the Netherlands as a successor to the ABC programming language, which was inspired by SETL, capable of exception handling and interfacing with the Amoeba operating system.",
|
||||
"Python consistently ranks as one of the most popular programming languages, and has gained widespread use in the machine learning community."
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Python Tutorial",
|
||||
"url": "https://www.w3schools.com/python/",
|
||||
"is_source_local": false,
|
||||
"is_source_both": false,
|
||||
"description": "W3Schools offers free online tutorials, references and exercises in all the major languages of the web. Covering popular subjects like HTML, CSS, JavaScript, <strong>Python</strong>, SQL, Java, and many, many more.",
|
||||
"page_age": "2017-12-07T00:00:00",
|
||||
"profile": {
|
||||
"name": "W3Schools",
|
||||
"url": "https://www.w3schools.com/python/",
|
||||
"long_name": "w3schools.com",
|
||||
"img": "https://imgs.search.brave.com/JwO5r7z3HTBkU29vgNH_4rrSWLf2M4-8FMWNvbxrKX8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjVlMGVkZDVj/ZGMyZWRmMzAwODRi/ZDAwZGE4NWI3NmU4/MjRhNjEzOGFhZWY3/ZGViMjY1OWY2ZDYw/YTZiOGUyZS93d3cu/dzNzY2hvb2xzLmNv/bS8"
|
||||
},
|
||||
"language": "en",
|
||||
"family_friendly": true,
|
||||
"type": "search_result",
|
||||
"subtype": "generic",
|
||||
"meta_url": {
|
||||
"scheme": "https",
|
||||
"netloc": "w3schools.com",
|
||||
"hostname": "www.w3schools.com",
|
||||
"favicon": "https://imgs.search.brave.com/JwO5r7z3HTBkU29vgNH_4rrSWLf2M4-8FMWNvbxrKX8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjVlMGVkZDVj/ZGMyZWRmMzAwODRi/ZDAwZGE4NWI3NmU4/MjRhNjEzOGFhZWY3/ZGViMjY1OWY2ZDYw/YTZiOGUyZS93d3cu/dzNzY2hvb2xzLmNv/bS8",
|
||||
"path": "› python"
|
||||
},
|
||||
"thumbnail": {
|
||||
"src": "https://imgs.search.brave.com/EMfp8dodbJehmj0yCJh8317RHuaumsddnHI4bujvFcg/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/dzNzY2hvb2xzLmNv/bS9pbWFnZXMvdzNz/Y2hvb2xzX2xvZ29f/NDM2XzIucG5n",
|
||||
"original": "https://www.w3schools.com/images/w3schools_logo_436_2.png",
|
||||
"logo": true
|
||||
},
|
||||
"age": "December 7, 2017",
|
||||
"extra_snippets": [
|
||||
"Well organized and easy to understand Web building tutorials with lots of examples of how to use HTML, CSS, JavaScript, SQL, Python, PHP, Bootstrap, Java, XML and more.",
|
||||
"HTML CSS JAVASCRIPT SQL PYTHON JAVA PHP HOW TO W3.CSS C C++ C# BOOTSTRAP REACT MYSQL JQUERY EXCEL XML DJANGO NUMPY PANDAS NODEJS R TYPESCRIPT ANGULAR GIT POSTGRESQL MONGODB ASP AI GO KOTLIN SASS VUE DSA GEN AI SCIPY AWS CYBERSECURITY DATA SCIENCE",
|
||||
"Python Variables Variable Names Assign Multiple Values Output Variables Global Variables Variable Exercises Python Data Types Python Numbers Python Casting Python Strings",
|
||||
"Python Strings Slicing Strings Modify Strings Concatenate Strings Format Strings Escape Characters String Methods String Exercises Python Booleans Python Operators Python Lists"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Online Python - IDE, Editor, Compiler, Interpreter",
|
||||
"url": "https://www.online-python.com/",
|
||||
"is_source_local": false,
|
||||
"is_source_both": false,
|
||||
"description": "Build and Run your <strong>Python</strong> code instantly. Online-<strong>Python</strong> is a quick and easy tool that helps you to build, compile, test your <strong>python</strong> programs.",
|
||||
"profile": {
|
||||
"name": "Online-python",
|
||||
"url": "https://www.online-python.com/",
|
||||
"long_name": "online-python.com",
|
||||
"img": "https://imgs.search.brave.com/kfaEvapwHxSsRObO52-I-otYFPHpG1h7UXJyUqDM2Ec/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZGYxODdjNWQ0/NjZjZTNiMjk5NDY1/MWI5MTgyYjU3Y2Q3/MTI3NGM5MjUzY2Fi/OGQ3MTQ4MmIxMTQx/ZTcxNWFhMC93d3cu/b25saW5lLXB5dGhv/bi5jb20v"
|
||||
},
|
||||
"language": "en",
|
||||
"family_friendly": true,
|
||||
"type": "search_result",
|
||||
"subtype": "generic",
|
||||
"meta_url": {
|
||||
"scheme": "https",
|
||||
"netloc": "online-python.com",
|
||||
"hostname": "www.online-python.com",
|
||||
"favicon": "https://imgs.search.brave.com/kfaEvapwHxSsRObO52-I-otYFPHpG1h7UXJyUqDM2Ec/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZGYxODdjNWQ0/NjZjZTNiMjk5NDY1/MWI5MTgyYjU3Y2Q3/MTI3NGM5MjUzY2Fi/OGQ3MTQ4MmIxMTQx/ZTcxNWFhMC93d3cu/b25saW5lLXB5dGhv/bi5jb20v",
|
||||
"path": ""
|
||||
},
|
||||
"extra_snippets": [
|
||||
"Build, run, and share Python code online for free with the help of online-integrated python's development environment (IDE). It is one of the most efficient, dependable, and potent online compilers for the Python programming language. It is not necessary for you to bother about establishing a Python environment in your local.",
|
||||
"It is one of the most efficient, dependable, and potent online compilers for the Python programming language. It is not necessary for you to bother about establishing a Python environment in your local. Now You can immediately execute the Python code in the web browser of your choice.",
|
||||
"It is not necessary for you to bother about establishing a Python environment in your local. Now You can immediately execute the Python code in the web browser of your choice. Using this Python editor is simple and quick to get up and running with. Simply type in the programme, and then press the RUN button!",
|
||||
"Now You can immediately execute the Python code in the web browser of your choice. Using this Python editor is simple and quick to get up and running with. Simply type in the programme, and then press the RUN button! The code can be saved online by choosing the SHARE option, which also gives you the ability to access your code from any location providing you have internet access."
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Python · GitHub",
|
||||
"url": "https://github.com/python",
|
||||
"is_source_local": false,
|
||||
"is_source_both": false,
|
||||
"description": "Repositories related to the <strong>Python</strong> Programming language - <strong>Python</strong>",
|
||||
"page_age": "2023-03-06T00:00:00",
|
||||
"profile": {
|
||||
"name": "GitHub",
|
||||
"url": "https://github.com/python",
|
||||
"long_name": "github.com",
|
||||
"img": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw"
|
||||
},
|
||||
"language": "en",
|
||||
"family_friendly": true,
|
||||
"type": "search_result",
|
||||
"subtype": "generic",
|
||||
"meta_url": {
|
||||
"scheme": "https",
|
||||
"netloc": "github.com",
|
||||
"hostname": "github.com",
|
||||
"favicon": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw",
|
||||
"path": "› python"
|
||||
},
|
||||
"thumbnail": {
|
||||
"src": "https://imgs.search.brave.com/POoaRfu_7gfp-D_O3qMNJrwDqJNbiDu1HuBpNJ_MpVQ/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9hdmF0/YXJzLmdpdGh1YnVz/ZXJjb250ZW50LmNv/bS91LzE1MjU5ODE_/cz0yMDAmYW1wO3Y9/NA",
|
||||
"original": "https://avatars.githubusercontent.com/u/1525981?s=200&v=4",
|
||||
"logo": false
|
||||
},
|
||||
"age": "March 6, 2023",
|
||||
"extra_snippets": ["Configuration for Python planets (e.g. http://planetpython.org)"]
|
||||
},
|
||||
{
|
||||
"title": "Online Python Compiler (Interpreter)",
|
||||
"url": "https://www.programiz.com/python-programming/online-compiler/",
|
||||
"is_source_local": false,
|
||||
"is_source_both": false,
|
||||
"description": "Write and run <strong>Python</strong> code using our online compiler (interpreter). You can use <strong>Python</strong> Shell like IDLE, and take inputs from the user in our <strong>Python</strong> compiler.",
|
||||
"page_age": "2020-06-02T00:00:00",
|
||||
"profile": {
|
||||
"name": "Programiz",
|
||||
"url": "https://www.programiz.com/python-programming/online-compiler/",
|
||||
"long_name": "programiz.com",
|
||||
"img": "https://imgs.search.brave.com/ozj4JFayZ3Fs5c9eTp7M5g12azQ_Hblgu4dpTuHRz6U/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMGJlN2U1YjVi/Y2M3ZDU5OGMwMWNi/M2Q3YjhjOTM1ZTFk/Y2NkZjE4NGQwOGIx/MTQ4NjI2YmNhODVj/MzFkMmJhYy93d3cu/cHJvZ3JhbWl6LmNv/bS8"
|
||||
},
|
||||
"language": "en",
|
||||
"family_friendly": true,
|
||||
"type": "search_result",
|
||||
"subtype": "generic",
|
||||
"meta_url": {
|
||||
"scheme": "https",
|
||||
"netloc": "programiz.com",
|
||||
"hostname": "www.programiz.com",
|
||||
"favicon": "https://imgs.search.brave.com/ozj4JFayZ3Fs5c9eTp7M5g12azQ_Hblgu4dpTuHRz6U/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMGJlN2U1YjVi/Y2M3ZDU5OGMwMWNi/M2Q3YjhjOTM1ZTFk/Y2NkZjE4NGQwOGIx/MTQ4NjI2YmNhODVj/MzFkMmJhYy93d3cu/cHJvZ3JhbWl6LmNv/bS8",
|
||||
"path": "› python-programming › online-compiler"
|
||||
},
|
||||
"age": "June 2, 2020",
|
||||
"extra_snippets": [
|
||||
"Python Online Compiler Online R Compiler SQL Online Editor Online HTML/CSS Editor Online Java Compiler C Online Compiler C++ Online Compiler C# Online Compiler JavaScript Online Compiler Online GoLang Compiler Online PHP Compiler Online Swift Compiler Online Rust Compiler",
|
||||
"# Online Python compiler (interpreter) to run Python online. # Write Python 3 code in this online editor and run it. print(\"Try programiz.pro\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Python Developer",
|
||||
"url": "https://twitter.com/Python_Dv/status/1786763460992544791",
|
||||
"is_source_local": false,
|
||||
"is_source_both": false,
|
||||
"description": "<strong>Python</strong> Developer",
|
||||
"page_age": "2024-05-04T14:30:03",
|
||||
"profile": {
|
||||
"name": "X",
|
||||
"url": "https://twitter.com/Python_Dv/status/1786763460992544791",
|
||||
"long_name": "twitter.com",
|
||||
"img": "https://imgs.search.brave.com/Zq483bGX0GnSgym-1P7iyOyEDX3PkDZSNT8m56F862A/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvN2MxOTUxNzhj/OTY1ZTQ3N2I0MjJk/MTY5NGM0MTRlYWVi/MjU1YWE2NDUwYmQ2/YTA2MDFhMDlkZDEx/NTAzZGNiNi90d2l0/dGVyLmNvbS8"
|
||||
},
|
||||
"language": "en",
|
||||
"family_friendly": true,
|
||||
"type": "search_result",
|
||||
"subtype": "generic",
|
||||
"meta_url": {
|
||||
"scheme": "https",
|
||||
"netloc": "twitter.com",
|
||||
"hostname": "twitter.com",
|
||||
"favicon": "https://imgs.search.brave.com/Zq483bGX0GnSgym-1P7iyOyEDX3PkDZSNT8m56F862A/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvN2MxOTUxNzhj/OTY1ZTQ3N2I0MjJk/MTY5NGM0MTRlYWVi/MjU1YWE2NDUwYmQ2/YTA2MDFhMDlkZDEx/NTAzZGNiNi90d2l0/dGVyLmNvbS8",
|
||||
"path": "› Python_Dv › status › 1786763460992544791"
|
||||
},
|
||||
"age": "20 hours ago"
|
||||
},
|
||||
{
|
||||
"title": "input table name? - python script - KNIME Extensions - KNIME Community Forum",
|
||||
"url": "https://forum.knime.com/t/input-table-name-python-script/78978",
|
||||
"is_source_local": false,
|
||||
"is_source_both": false,
|
||||
"description": "Hi, when running a <strong>python</strong> script node, I get the error seen on the screenshot Same happens with this code too: The script input is output from the csv reader node. How can I get the right name for that table? Best wishes, Dario",
|
||||
"page_age": "2024-05-04T09:20:44",
|
||||
"profile": {
|
||||
"name": "Knime",
|
||||
"url": "https://forum.knime.com/t/input-table-name-python-script/78978",
|
||||
"long_name": "forum.knime.com",
|
||||
"img": "https://imgs.search.brave.com/WQoOhAD5i6uEhJ-qXvlWMJwbGA52f2Ycc_ns36EK698/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTAxNzMxNjFl/MzJjNzU5NzRkOTMz/Mjg4NDU2OWUxM2Rj/YzVkOGM3MzIwNzI2/YTY1NzYxNzA1MDE5/NzQzOWU3NC9mb3J1/bS5rbmltZS5jb20v"
|
||||
},
|
||||
"language": "en",
|
||||
"family_friendly": true,
|
||||
"type": "search_result",
|
||||
"subtype": "article",
|
||||
"meta_url": {
|
||||
"scheme": "https",
|
||||
"netloc": "forum.knime.com",
|
||||
"hostname": "forum.knime.com",
|
||||
"favicon": "https://imgs.search.brave.com/WQoOhAD5i6uEhJ-qXvlWMJwbGA52f2Ycc_ns36EK698/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTAxNzMxNjFl/MzJjNzU5NzRkOTMz/Mjg4NDU2OWUxM2Rj/YzVkOGM3MzIwNzI2/YTY1NzYxNzA1MDE5/NzQzOWU3NC9mb3J1/bS5rbmltZS5jb20v",
|
||||
"path": " › knime extensions"
|
||||
},
|
||||
"thumbnail": {
|
||||
"src": "https://imgs.search.brave.com/DtEl38dcvuM1kGfhN0T5HfOrsMJcztWNyriLvtDJmKI/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9mb3J1/bS1jZG4ua25pbWUu/Y29tL3VwbG9hZHMv/ZGVmYXVsdC9vcmln/aW5hbC8zWC9lLzYv/ZTY0M2M2NzFlNzAz/MDg2MjkwMWY2YzJh/OWFjOWI5ZmEwM2M3/ZjMwZi5wbmc",
|
||||
"original": "https://forum-cdn.knime.com/uploads/default/original/3X/e/6/e643c671e7030862901f6c2a9ac9b9fa03c7f30f.png",
|
||||
"logo": false
|
||||
},
|
||||
"age": "1 day ago",
|
||||
"extra_snippets": [
|
||||
"Hi, when running a python script node, I get the error seen on the screenshot Same happens with this code too: The script input is output from the csv reader node. How can I get the right name for that table? …"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "What does the Double Star operator mean in Python? - GeeksforGeeks",
|
||||
"url": "https://www.geeksforgeeks.org/what-does-the-double-star-operator-mean-in-python/",
|
||||
"is_source_local": false,
|
||||
"is_source_both": false,
|
||||
"description": "A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.",
|
||||
"page_age": "2023-03-14T17:15:04",
|
||||
"profile": {
|
||||
"name": "GeeksforGeeks",
|
||||
"url": "https://www.geeksforgeeks.org/what-does-the-double-star-operator-mean-in-python/",
|
||||
"long_name": "geeksforgeeks.org",
|
||||
"img": "https://imgs.search.brave.com/fhzcfv5xltx6-YBvJI9RZgS7xZo0dPNaASsrB8YOsCs/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjBhOGQ3MmNi/ZWE5N2EwMmZjYzA1/ZTI0ZTFhMGUyMTE0/MGM0ZTBmMWZlM2Y2/Yzk2ODMxZTRhYTBi/NDdjYTE0OS93d3cu/Z2Vla3Nmb3JnZWVr/cy5vcmcv"
|
||||
},
|
||||
"language": "en",
|
||||
"family_friendly": true,
|
||||
"type": "search_result",
|
||||
"subtype": "article",
|
||||
"meta_url": {
|
||||
"scheme": "https",
|
||||
"netloc": "geeksforgeeks.org",
|
||||
"hostname": "www.geeksforgeeks.org",
|
||||
"favicon": "https://imgs.search.brave.com/fhzcfv5xltx6-YBvJI9RZgS7xZo0dPNaASsrB8YOsCs/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjBhOGQ3MmNi/ZWE5N2EwMmZjYzA1/ZTI0ZTFhMGUyMTE0/MGM0ZTBmMWZlM2Y2/Yzk2ODMxZTRhYTBi/NDdjYTE0OS93d3cu/Z2Vla3Nmb3JnZWVr/cy5vcmcv",
|
||||
"path": "› what-does-the-double-star-operator-mean-in-python"
|
||||
},
|
||||
"thumbnail": {
|
||||
"src": "https://imgs.search.brave.com/GcR-j_dLbyHkbHEI3ffLMi6xpXGhF_2Z8POIoqtokhM/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9tZWRp/YS5nZWVrc2Zvcmdl/ZWtzLm9yZy93cC1j/b250ZW50L3VwbG9h/ZHMvZ2ZnXzIwMFgy/MDAtMTAweDEwMC5w/bmc",
|
||||
"original": "https://media.geeksforgeeks.org/wp-content/uploads/gfg_200X200-100x100.png",
|
||||
"logo": false
|
||||
},
|
||||
"age": "March 14, 2023",
|
||||
"extra_snippets": [
|
||||
"Difference between / vs. // operator in Python",
|
||||
"Double Star or (**) is one of the Arithmetic Operator (Like +, -, *, **, /, //, %) in Python Language. It is also known as Power Operator.",
|
||||
"The time complexity of the given Python program is O(n), where n is the number of key-value pairs in the input dictionary.",
|
||||
"Inplace Operators in Python | Set 2 (ixor(), iand(), ipow(),…)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "r/Python",
|
||||
"url": "https://www.reddit.com/r/Python/",
|
||||
"is_source_local": false,
|
||||
"is_source_both": false,
|
||||
"description": "The official <strong>Python</strong> community for Reddit! Stay up to date with the latest news, packages, and meta information relating to the <strong>Python</strong> programming language. --- If you have questions or are new to <strong>Python</strong> use r/LearnPython",
|
||||
"page_age": "2022-12-30T16:25:02",
|
||||
"profile": {
|
||||
"name": "Reddit",
|
||||
"url": "https://www.reddit.com/r/Python/",
|
||||
"long_name": "reddit.com",
|
||||
"img": "https://imgs.search.brave.com/mAZYEK9Wi13WLDUge7XZ8YuDTwm6DP6gBjvz1GdYZVY/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvN2ZiNTU0M2Nj/MTFhZjRiYWViZDlk/MjJiMjBjMzFjMDRk/Y2IzYWI0MGI0MjVk/OGY5NzQzOGQ5NzQ5/NWJhMWI0NC93d3cu/cmVkZGl0LmNvbS8"
|
||||
},
|
||||
"language": "en",
|
||||
"family_friendly": true,
|
||||
"type": "search_result",
|
||||
"subtype": "generic",
|
||||
"meta_url": {
|
||||
"scheme": "https",
|
||||
"netloc": "reddit.com",
|
||||
"hostname": "www.reddit.com",
|
||||
"favicon": "https://imgs.search.brave.com/mAZYEK9Wi13WLDUge7XZ8YuDTwm6DP6gBjvz1GdYZVY/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvN2ZiNTU0M2Nj/MTFhZjRiYWViZDlk/MjJiMjBjMzFjMDRk/Y2IzYWI0MGI0MjVk/OGY5NzQzOGQ5NzQ5/NWJhMWI0NC93d3cu/cmVkZGl0LmNvbS8",
|
||||
"path": "› r › Python"
|
||||
},
|
||||
"thumbnail": {
|
||||
"src": "https://imgs.search.brave.com/zWd10t3zg34ciHiAB-K5WWK3h_H4LedeDot9BVX7Ydo/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9zdHls/ZXMucmVkZGl0bWVk/aWEuY29tL3Q1XzJx/aDB5L3N0eWxlcy9j/b21tdW5pdHlJY29u/X2NpZmVobDR4dDdu/YzEucG5n",
|
||||
"original": "https://styles.redditmedia.com/t5_2qh0y/styles/communityIcon_cifehl4xt7nc1.png",
|
||||
"logo": false
|
||||
},
|
||||
"age": "December 30, 2022",
|
||||
"extra_snippets": [
|
||||
"r/Python: The official Python community for Reddit! Stay up to date with the latest news, packages, and meta information relating to the Python…",
|
||||
"By default, Python allows you to import and use anything, anywhere. Over time, this results in modules that were intended to be separate getting tightly coupled together, and domain boundaries breaking down. We experienced this first-hand at a unicorn startup, where the eng team paused development for over a year in an attempt to split up packages into independent services.",
|
||||
"Hello r/Python! It's time to share what you've been working on! Whether it's a work-in-progress, a completed masterpiece, or just a rough idea, let us know what you're up to!",
|
||||
"Whether it's your job, your hobby, or your passion project, all Python-related work is welcome here."
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "GitHub - python/cpython: The Python programming language",
|
||||
"url": "https://github.com/python/cpython",
|
||||
"is_source_local": false,
|
||||
"is_source_both": false,
|
||||
"description": "The <strong>Python</strong> programming language. Contribute to <strong>python</strong>/cpython development by creating an account on GitHub.",
|
||||
"page_age": "2022-10-29T00:00:00",
|
||||
"profile": {
|
||||
"name": "GitHub",
|
||||
"url": "https://github.com/python/cpython",
|
||||
"long_name": "github.com",
|
||||
"img": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw"
|
||||
},
|
||||
"language": "en",
|
||||
"family_friendly": true,
|
||||
"type": "search_result",
|
||||
"subtype": "software",
|
||||
"meta_url": {
|
||||
"scheme": "https",
|
||||
"netloc": "github.com",
|
||||
"hostname": "github.com",
|
||||
"favicon": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw",
|
||||
"path": "› python › cpython"
|
||||
},
|
||||
"thumbnail": {
|
||||
"src": "https://imgs.search.brave.com/BJbWFRUqgP-tKIyGK9ByXjuYjHO2mtYigUOEFNz_gXk/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9vcGVu/Z3JhcGguZ2l0aHVi/YXNzZXRzLmNvbS82/MTY5YmJkNTQ0YzAy/NDg0MGU4NDdjYTU1/YTU3ZGZmMDA2ZDAw/YWQ1NDIzOTFmYTQ3/YmJjODg3OWM0NWYw/MTZhL3B5dGhvbi9j/cHl0aG9u",
|
||||
"original": "https://opengraph.githubassets.com/6169bbd544c024840e847ca55a57dff006d00ad542391fa47bbc8879c45f016a/python/cpython",
|
||||
"logo": false
|
||||
},
|
||||
"age": "October 29, 2022",
|
||||
"extra_snippets": [
|
||||
"You can pass many options to the configure script; run ./configure --help to find out more. On macOS case-insensitive file systems and on Cygwin, the executable is called python.exe; elsewhere it's just python.",
|
||||
"Building a complete Python installation requires the use of various additional third-party libraries, depending on your build platform and configure options. Not all standard library modules are buildable or useable on all platforms. Refer to the Install dependencies section of the Developer Guide for current detailed information on dependencies for various Linux distributions and macOS.",
|
||||
"To get an optimized build of Python, configure --enable-optimizations before you run make. This sets the default make targets up to enable Profile Guided Optimization (PGO) and may be used to auto-enable Link Time Optimization (LTO) on some platforms. For more details, see the sections below.",
|
||||
"Copyright © 2001-2024 Python Software Foundation. All rights reserved."
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "5. Data Structures — Python 3.12.3 documentation",
|
||||
"url": "https://docs.python.org/3/tutorial/datastructures.html",
|
||||
"is_source_local": false,
|
||||
"is_source_both": false,
|
||||
"description": "This chapter describes some things you’ve learned about already in more detail, and adds some new things as well. More on Lists: The list data type has some more methods. Here are all of the method...",
|
||||
"page_age": "2023-07-04T00:00:00",
|
||||
"profile": {
|
||||
"name": "Python documentation",
|
||||
"url": "https://docs.python.org/3/tutorial/datastructures.html",
|
||||
"long_name": "docs.python.org",
|
||||
"img": "https://imgs.search.brave.com/F5Ym7eSElhGdGUFKLRxDj9Z_tc180ldpeMvQ2Q6ARbA/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMTUzOTFjOGVi/YTcyOTVmODA3ODIy/YjE2NzFjY2ViMjhl/NzRlY2JhYTc5YjNm/ZjhmODAyZWI2OGUw/ZjU4NDVlNy9kb2Nz/LnB5dGhvbi5vcmcv"
|
||||
},
|
||||
"language": "en",
|
||||
"family_friendly": true,
|
||||
"type": "search_result",
|
||||
"subtype": "generic",
|
||||
"meta_url": {
|
||||
"scheme": "https",
|
||||
"netloc": "docs.python.org",
|
||||
"hostname": "docs.python.org",
|
||||
"favicon": "https://imgs.search.brave.com/F5Ym7eSElhGdGUFKLRxDj9Z_tc180ldpeMvQ2Q6ARbA/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMTUzOTFjOGVi/YTcyOTVmODA3ODIy/YjE2NzFjY2ViMjhl/NzRlY2JhYTc5YjNm/ZjhmODAyZWI2OGUw/ZjU4NDVlNy9kb2Nz/LnB5dGhvbi5vcmcv",
|
||||
"path": "› 3 › tutorial › datastructures.html"
|
||||
},
|
||||
"thumbnail": {
|
||||
"src": "https://imgs.search.brave.com/Y7GrMRF8WorDIMLuOl97XC8ltYpoOCqNwWF2pQIIKls/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9kb2Nz/LnB5dGhvbi5vcmcv/My9fc3RhdGljL29n/LWltYWdlLnBuZw",
|
||||
"original": "https://docs.python.org/3/_static/og-image.png",
|
||||
"logo": false
|
||||
},
|
||||
"age": "July 4, 2023",
|
||||
"extra_snippets": [
|
||||
"You might have noticed that methods like insert, remove or sort that only modify the list have no return value printed – they return the default None. [1] This is a design principle for all mutable data structures in Python.",
|
||||
"We saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types — list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.",
|
||||
"Python also includes a data type for sets. A set is an unordered collection with no duplicate elements. Basic uses include membership testing and eliminating duplicate entries. Set objects also support mathematical operations like union, intersection, difference, and symmetric difference.",
|
||||
"Another useful data type built into Python is the dictionary (see Mapping Types — dict). Dictionaries are sometimes found in other languages as “associative memories” or “associative arrays”. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys."
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Something wrong with python packages / AUR Issues, Discussion & PKGBUILD Requests / Arch Linux Forums",
|
||||
"url": "https://bbs.archlinux.org/viewtopic.php?id=295466",
|
||||
"is_source_local": false,
|
||||
"is_source_both": false,
|
||||
"description": "Big <strong>Python</strong> updates require <strong>Python</strong> packages to be rebuild. For some reason they didn't think a bump that made it necessary to rebuild half the official repo was a news post.",
|
||||
"page_age": "2024-05-04T08:30:02",
|
||||
"profile": {
|
||||
"name": "Archlinux",
|
||||
"url": "https://bbs.archlinux.org/viewtopic.php?id=295466",
|
||||
"long_name": "bbs.archlinux.org",
|
||||
"img": "https://imgs.search.brave.com/3au9oqkzSri_aLEec3jo-0bFgLuICkydrWfjFcC8lkI/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNWNkODM1MWJl/ZmJhMzkzNzYzMDkz/NmEyMWMxNjI5MjNk/NGJmZjFhNTBlZDNl/Mzk5MzJjOGZkYjZl/MjNmY2IzNS9iYnMu/YXJjaGxpbnV4Lm9y/Zy8"
|
||||
},
|
||||
"language": "en",
|
||||
"family_friendly": true,
|
||||
"type": "search_result",
|
||||
"subtype": "generic",
|
||||
"meta_url": {
|
||||
"scheme": "https",
|
||||
"netloc": "bbs.archlinux.org",
|
||||
"hostname": "bbs.archlinux.org",
|
||||
"favicon": "https://imgs.search.brave.com/3au9oqkzSri_aLEec3jo-0bFgLuICkydrWfjFcC8lkI/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNWNkODM1MWJl/ZmJhMzkzNzYzMDkz/NmEyMWMxNjI5MjNk/NGJmZjFhNTBlZDNl/Mzk5MzJjOGZkYjZl/MjNmY2IzNS9iYnMu/YXJjaGxpbnV4Lm9y/Zy8",
|
||||
"path": "› viewtopic.php"
|
||||
},
|
||||
"age": "1 day ago",
|
||||
"extra_snippets": [
|
||||
"Traceback (most recent call last): File \"/usr/lib/python3.12/importlib/metadata/__init__.py\", line 397, in from_name return next(cls.discover(name=name)) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ StopIteration During handling of the above exception, another exception occurred: Traceback (most recent call last): File \"/usr/bin/informant\", line 33, in <module> sys.exit(load_entry_point('informant==0.5.0', 'console_scripts', 'informant')()) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File \"/usr/bin/informant\", line 22, in importlib_load_entry_point for entry_point in distribution(dis"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Introduction to Python",
|
||||
"url": "https://www.w3schools.com/python/python_intro.asp",
|
||||
"is_source_local": false,
|
||||
"is_source_both": false,
|
||||
"description": "W3Schools offers free online tutorials, references and exercises in all the major languages of the web. Covering popular subjects like HTML, CSS, JavaScript, <strong>Python</strong>, SQL, Java, and many, many more.",
|
||||
"profile": {
|
||||
"name": "W3Schools",
|
||||
"url": "https://www.w3schools.com/python/python_intro.asp",
|
||||
"long_name": "w3schools.com",
|
||||
"img": "https://imgs.search.brave.com/JwO5r7z3HTBkU29vgNH_4rrSWLf2M4-8FMWNvbxrKX8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjVlMGVkZDVj/ZGMyZWRmMzAwODRi/ZDAwZGE4NWI3NmU4/MjRhNjEzOGFhZWY3/ZGViMjY1OWY2ZDYw/YTZiOGUyZS93d3cu/dzNzY2hvb2xzLmNv/bS8"
|
||||
},
|
||||
"language": "en",
|
||||
"family_friendly": true,
|
||||
"type": "search_result",
|
||||
"subtype": "generic",
|
||||
"meta_url": {
|
||||
"scheme": "https",
|
||||
"netloc": "w3schools.com",
|
||||
"hostname": "www.w3schools.com",
|
||||
"favicon": "https://imgs.search.brave.com/JwO5r7z3HTBkU29vgNH_4rrSWLf2M4-8FMWNvbxrKX8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjVlMGVkZDVj/ZGMyZWRmMzAwODRi/ZDAwZGE4NWI3NmU4/MjRhNjEzOGFhZWY3/ZGViMjY1OWY2ZDYw/YTZiOGUyZS93d3cu/dzNzY2hvb2xzLmNv/bS8",
|
||||
"path": "› python › python_intro.asp"
|
||||
},
|
||||
"thumbnail": {
|
||||
"src": "https://imgs.search.brave.com/EMfp8dodbJehmj0yCJh8317RHuaumsddnHI4bujvFcg/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/dzNzY2hvb2xzLmNv/bS9pbWFnZXMvdzNz/Y2hvb2xzX2xvZ29f/NDM2XzIucG5n",
|
||||
"original": "https://www.w3schools.com/images/w3schools_logo_436_2.png",
|
||||
"logo": true
|
||||
},
|
||||
"extra_snippets": [
|
||||
"Well organized and easy to understand Web building tutorials with lots of examples of how to use HTML, CSS, JavaScript, SQL, Python, PHP, Bootstrap, Java, XML and more.",
|
||||
"HTML CSS JAVASCRIPT SQL PYTHON JAVA PHP HOW TO W3.CSS C C++ C# BOOTSTRAP REACT MYSQL JQUERY EXCEL XML DJANGO NUMPY PANDAS NODEJS R TYPESCRIPT ANGULAR GIT POSTGRESQL MONGODB ASP AI GO KOTLIN SASS VUE DSA GEN AI SCIPY AWS CYBERSECURITY DATA SCIENCE",
|
||||
"Python Variables Variable Names Assign Multiple Values Output Variables Global Variables Variable Exercises Python Data Types Python Numbers Python Casting Python Strings",
|
||||
"Python Strings Slicing Strings Modify Strings Concatenate Strings Format Strings Escape Characters String Methods String Exercises Python Booleans Python Operators Python Lists"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "bug: AUR package wants to use python but does not find any preset version · Issue #1740 · asdf-vm/asdf",
|
||||
"url": "https://github.com/asdf-vm/asdf/issues/1740",
|
||||
"is_source_local": false,
|
||||
"is_source_both": false,
|
||||
"description": "Describe the Bug I am not sure why this is happening, I am trying to install tlpui from AUR and it fails, here are some logs to help: ==> Making package: tlpui 2:1.6.5-1 (Mi 10 apr 2024 23:19:15 +0...",
|
||||
"page_age": "2024-05-04T06:45:04",
|
||||
"profile": {
|
||||
"name": "GitHub",
|
||||
"url": "https://github.com/asdf-vm/asdf/issues/1740",
|
||||
"long_name": "github.com",
|
||||
"img": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw"
|
||||
},
|
||||
"language": "en",
|
||||
"family_friendly": true,
|
||||
"type": "search_result",
|
||||
"subtype": "software",
|
||||
"meta_url": {
|
||||
"scheme": "https",
|
||||
"netloc": "github.com",
|
||||
"hostname": "github.com",
|
||||
"favicon": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw",
|
||||
"path": "› asdf-vm › asdf › issues › 1740"
|
||||
},
|
||||
"thumbnail": {
|
||||
"src": "https://imgs.search.brave.com/KrLW5s_2n4jyP8XLbc3ZPVBaLD963tQgWzG9EWPZlQs/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9vcGVu/Z3JhcGguZ2l0aHVi/YXNzZXRzLmNvbS81/MTE0ZTdkOGIwODM2/YmQ2MTY3NzQ1ZGI4/MmZjMGE3OGUyMjcw/MGFlY2ZjMWZkODBl/MDYzZTNiN2ZjOWNj/NzYyL2FzZGYtdm0v/YXNkZi9pc3N1ZXMv/MTc0MA",
|
||||
"original": "https://opengraph.githubassets.com/5114e7d8b0836bd6167745db82fc0a78e22700aecfc1fd80e063e3b7fc9cc762/asdf-vm/asdf/issues/1740",
|
||||
"logo": false
|
||||
},
|
||||
"age": "1 day ago",
|
||||
"extra_snippets": [
|
||||
"==> Starting build()... No preset version installed for command python Please install a version by running one of the following: asdf install python 3.8 or add one of the following versions in your config file at /home/ferret/.tool-versions python 3.11.0 python 3.12.1 python 3.12.3 ==> ERROR: A failure occurred in build(). Aborting...",
|
||||
"-> error making: tlpui-exit status 4 -> Failed to install the following packages. Manual intervention is required: tlpui - exit status 4 ferret@FX505DT in ~ $ cat /home/ferret/.tool-versions nodejs 21.6.0 python 3.12.3 ferret@FX505DT in ~ $ python -V Python 3.12.3 ferret@FX505DT in ~ $ which python /home/ferret/.asdf/shims/python",
|
||||
"Describe the Bug I am not sure why this is happening, I am trying to install tlpui from AUR and it fails, here are some logs to help: ==> Making package: tlpui 2:1.6.5-1 (Mi 10 apr 2024 23:19:15 +0300) ==> Retrieving sources... -> Found ..."
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "What are python.exe and python3.exe, and why do they appear to point to App Installer? | Windows 11 Forum",
|
||||
"url": "https://www.elevenforum.com/t/what-are-python-exe-and-python3-exe-and-why-do-they-appear-to-point-to-app-installer.24886/",
|
||||
"is_source_local": false,
|
||||
"is_source_both": false,
|
||||
"description": "I was looking at App execution aliases (Settings > Apps > Advanced app settings > App execution aliases) on my new computer -- my first Windows 11 computer. Why are <strong>python</strong>.exe and python3.exe listed as App Installer? I assume that App Installer refers to installation of Microsoft Store / UWP...",
|
||||
"page_age": "2024-05-03T17:30:04",
|
||||
"profile": {
|
||||
"name": "Windows 11 Forum",
|
||||
"url": "https://www.elevenforum.com/t/what-are-python-exe-and-python3-exe-and-why-do-they-appear-to-point-to-app-installer.24886/",
|
||||
"long_name": "elevenforum.com",
|
||||
"img": "https://imgs.search.brave.com/XVRAYMEj6Im8i7jV5RxeTwpiRPtY9IWg4wRIuh-WhEw/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZjk5MDZkMDIw/M2U1OWIwNjM5Y2U1/M2U2NzNiNzVkNTA5/NzA5OTI1ZTFmOTc4/MzU3OTlhYzU5OTVi/ZGNjNTY4MS93d3cu/ZWxldmVuZm9ydW0u/Y29tLw"
|
||||
},
|
||||
"language": "en",
|
||||
"family_friendly": true,
|
||||
"type": "search_result",
|
||||
"subtype": "generic",
|
||||
"meta_url": {
|
||||
"scheme": "https",
|
||||
"netloc": "elevenforum.com",
|
||||
"hostname": "www.elevenforum.com",
|
||||
"favicon": "https://imgs.search.brave.com/XVRAYMEj6Im8i7jV5RxeTwpiRPtY9IWg4wRIuh-WhEw/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZjk5MDZkMDIw/M2U1OWIwNjM5Y2U1/M2U2NzNiNzVkNTA5/NzA5OTI1ZTFmOTc4/MzU3OTlhYzU5OTVi/ZGNjNTY4MS93d3cu/ZWxldmVuZm9ydW0u/Y29tLw",
|
||||
"path": " › windows support forums › apps and software"
|
||||
},
|
||||
"thumbnail": {
|
||||
"src": "https://imgs.search.brave.com/DVoFcE6d_-lx3BVGNS-RZK_lZzxQ8VhwZVf3AVqEJFA/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/ZWxldmVuZm9ydW0u/Y29tL2RhdGEvYXNz/ZXRzL2xvZ28vbWV0/YTEtMjAxLnBuZw",
|
||||
"original": "https://www.elevenforum.com/data/assets/logo/meta1-201.png",
|
||||
"logo": true
|
||||
},
|
||||
"age": "2 days ago",
|
||||
"extra_snippets": [
|
||||
"Why are python.exe and python3.exe listed as App Installer? I assume that App Installer refers to installation of Microsoft Store / UWP apps, but if that's the case, then why are they called python.exe and python3.exe? Or are python.exe and python3.exe simply serving as aliases / pointers pointing to App Installer, which is itself a Microsoft Store App?",
|
||||
"Or are python.exe and python3.exe simply serving as aliases / pointers pointing to App Installer, which is itself a Microsoft Store App? I wish to soon install Python, along with an integrated development editor (IDE), on my machine, so that I can code in Python.",
|
||||
"I wish to soon install Python, along with an integrated development editor (IDE), on my machine, so that I can code in Python. But is a Python interpreter already on my computer as suggested, if obliquely, by the presence of python.exe and python3.exe? I kind of doubt it."
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "How to Watermark Your Images Using Python OpenCV in ...",
|
||||
"url": "https://medium.com/@daily_data_prep/how-to-watermark-your-images-using-python-opencv-in-bulk-e472085389a1",
|
||||
"is_source_local": false,
|
||||
"is_source_both": false,
|
||||
"description": "Medium is an open platform where readers find dynamic thinking, and where expert and undiscovered voices can share their writing on any topic.",
|
||||
"page_age": "2024-05-03T14:05:06",
|
||||
"profile": {
|
||||
"name": "Medium",
|
||||
"url": "https://medium.com/@daily_data_prep/how-to-watermark-your-images-using-python-opencv-in-bulk-e472085389a1",
|
||||
"long_name": "medium.com",
|
||||
"img": "https://imgs.search.brave.com/qvE2kIQCiAsnPv2C6P9xM5J2VVWdm55g-A-2Q_yIJ0g/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTZhYmQ1N2Q4/NDg4ZDcyODIyMDZi/MzFmOWNhNjE3Y2E4/Y2YzMThjNjljNDIx/ZjllZmNhYTcwODhl/YTcwNDEzYy9tZWRp/dW0uY29tLw"
|
||||
},
|
||||
"language": "en",
|
||||
"family_friendly": true,
|
||||
"type": "search_result",
|
||||
"subtype": "generic",
|
||||
"meta_url": {
|
||||
"scheme": "https",
|
||||
"netloc": "medium.com",
|
||||
"hostname": "medium.com",
|
||||
"favicon": "https://imgs.search.brave.com/qvE2kIQCiAsnPv2C6P9xM5J2VVWdm55g-A-2Q_yIJ0g/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTZhYmQ1N2Q4/NDg4ZDcyODIyMDZi/MzFmOWNhNjE3Y2E4/Y2YzMThjNjljNDIx/ZjllZmNhYTcwODhl/YTcwNDEzYy9tZWRp/dW0uY29tLw",
|
||||
"path": "› @daily_data_prep › how-to-watermark-your-images-using-python-opencv-in-bulk-e472085389a1"
|
||||
},
|
||||
"age": "2 days ago"
|
||||
},
|
||||
{
|
||||
"title": "Increment and Decrement Operators in Python?",
|
||||
"url": "https://www.tutorialspoint.com/increment-and-decrement-operators-in-python",
|
||||
"is_source_local": false,
|
||||
"is_source_both": false,
|
||||
"description": "Increment and Decrement Operators in <strong>Python</strong> - <strong>Python</strong> does not have unary increment/decrement operator (++/--). Instead to increment a value, usea += 1to decrement a value, use −a -= 1Example>>> a = 0 >>> >>> #Increment >>> a +=1 >>> >>> #Decrement >>> a -= 1 >>> >>> #value of a >>> a 0Python ...",
|
||||
"page_age": "2023-08-23T00:00:00",
|
||||
"profile": {
|
||||
"name": "Tutorialspoint",
|
||||
"url": "https://www.tutorialspoint.com/increment-and-decrement-operators-in-python",
|
||||
"long_name": "tutorialspoint.com",
|
||||
"img": "https://imgs.search.brave.com/Wt8BSkivPlFwcU5yBtf7YzuvTuRExyd_502cdABCS5c/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjcyYjAzYmVl/ODU4MzZiMjJiYTFh/MjJhZDNmNWE4YzA5/MDgyYTZhMDg3NTYw/M2NiY2NiZTUxN2I5/MjU1MWFmMS93d3cu/dHV0b3JpYWxzcG9p/bnQuY29tLw"
|
||||
},
|
||||
"language": "en",
|
||||
"family_friendly": true,
|
||||
"type": "search_result",
|
||||
"subtype": "generic",
|
||||
"meta_url": {
|
||||
"scheme": "https",
|
||||
"netloc": "tutorialspoint.com",
|
||||
"hostname": "www.tutorialspoint.com",
|
||||
"favicon": "https://imgs.search.brave.com/Wt8BSkivPlFwcU5yBtf7YzuvTuRExyd_502cdABCS5c/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjcyYjAzYmVl/ODU4MzZiMjJiYTFh/MjJhZDNmNWE4YzA5/MDgyYTZhMDg3NTYw/M2NiY2NiZTUxN2I5/MjU1MWFmMS93d3cu/dHV0b3JpYWxzcG9p/bnQuY29tLw",
|
||||
"path": "› increment-and-decrement-operators-in-python"
|
||||
},
|
||||
"thumbnail": {
|
||||
"src": "https://imgs.search.brave.com/ddG5vyZGLVudvecEbQJPeG8tGuaZ7g3Xz6Gyjdl5WA8/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/dHV0b3JpYWxzcG9p/bnQuY29tL2ltYWdl/cy90cF9sb2dvXzQz/Ni5wbmc",
|
||||
"original": "https://www.tutorialspoint.com/images/tp_logo_436.png",
|
||||
"logo": true
|
||||
},
|
||||
"age": "August 23, 2023",
|
||||
"extra_snippets": [
|
||||
"Increment and Decrement Operators in Python - Python does not have unary increment/decrement operator (++/--). Instead to increment a value, usea += 1to decrement a value, use −a -= 1Example>>> a = 0 >>> >>> #Increment >>> a +=1 >>> >>> #Decrement >>> a -= 1 >>> >>> #value of a >>> a 0Python does not provide multiple ways to do the same thing",
|
||||
"So what above statement means in python is: create an object of type int having value 1 and give the name a to it. The object is an instance of int having value 1 and the name a refers to it. The assigned name a and the object to which it refers are distinct.",
|
||||
"Python does not provide multiple ways to do the same thing .",
|
||||
"However, be careful if you are coming from a language like C, Python doesn’t have \"variables\" in the sense that C does, instead python uses names and objects and in python integers (int’s) are immutable."
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Gumroad – How not to suck at Python / SideFX Houdini | CG Persia",
|
||||
"url": "https://cgpersia.com/2024/05/gumroad-how-not-to-suck-at-python-sidefx-houdini-195370.html",
|
||||
"is_source_local": false,
|
||||
"is_source_both": false,
|
||||
"description": "Info: This course is made for artists or TD (technical director) willing to learn <strong>Python</strong> to improve their workflows inside SideFX Houdini, get faster in production and develop all the tools you always wished you had.",
|
||||
"page_age": "2024-05-03T08:35:03",
|
||||
"profile": {
|
||||
"name": "Cgpersia",
|
||||
"url": "https://cgpersia.com/2024/05/gumroad-how-not-to-suck-at-python-sidefx-houdini-195370.html",
|
||||
"long_name": "cgpersia.com",
|
||||
"img": "https://imgs.search.brave.com/VjyaopAm-M9sWvM7n-KnGZ3T5swIOwwE80iF5QVqQPg/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYmE0MzQ4NmI2/NjFhMTA1ZDBiN2Iw/ZWNiNDUxNjUwYjdh/MGE5ZjQ0ZjIxNzll/NmVkZDE2YzYyMDBh/NDNiMDgwMy9jZ3Bl/cnNpYS5jb20v"
|
||||
},
|
||||
"language": "en",
|
||||
"family_friendly": true,
|
||||
"type": "search_result",
|
||||
"subtype": "generic",
|
||||
"meta_url": {
|
||||
"scheme": "https",
|
||||
"netloc": "cgpersia.com",
|
||||
"hostname": "cgpersia.com",
|
||||
"favicon": "https://imgs.search.brave.com/VjyaopAm-M9sWvM7n-KnGZ3T5swIOwwE80iF5QVqQPg/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYmE0MzQ4NmI2/NjFhMTA1ZDBiN2Iw/ZWNiNDUxNjUwYjdh/MGE5ZjQ0ZjIxNzll/NmVkZDE2YzYyMDBh/NDNiMDgwMy9jZ3Bl/cnNpYS5jb20v",
|
||||
"path": "› 2024 › 05 › gumroad-how-not-to-suck-at-python-sidefx-houdini-195370.html"
|
||||
},
|
||||
"age": "2 days ago",
|
||||
"extra_snippets": [
|
||||
"Posted in: 2D, CG Releases, Downloads, Learning, Tutorials, Videos. Tagged: Gumroad, Python, Sidefx. Leave a Comment",
|
||||
"01 – Python – Fundamentals Get the Fundamentals of python before starting the fun stuff ! 02 – Python Construction Part02 digging further into python concepts 03 – Houdini – Python Basics Applying some basic python in Houdini and starting to make tools !",
|
||||
"02 – Python Construction Part02 digging further into python concepts 03 – Houdini – Python Basics Applying some basic python in Houdini and starting to make tools ! 04 – Houdini – Python Intermediate Applying some more advanced python in Houdini to make tools ! 05 – Houdini – Python Expert Using QtDesigner in combinaison with Houdini Python/Pyside to create advanced tools."
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "How to install Python: The complete Python programmer’s guide",
|
||||
"url": "https://www.pluralsight.com/resources/blog/software-development/python-installation-guide",
|
||||
"is_source_local": false,
|
||||
"is_source_both": false,
|
||||
"description": "An easy guide on how set up your operating system so you can program in <strong>Python</strong>, and how to update or uninstall it. For Linux, Windows, and macOS.",
|
||||
"page_age": "2024-05-02T07:30:02",
|
||||
"profile": {
|
||||
"name": "Pluralsight",
|
||||
"url": "https://www.pluralsight.com/resources/blog/software-development/python-installation-guide",
|
||||
"long_name": "pluralsight.com",
|
||||
"img": "https://imgs.search.brave.com/zvwQNSVu9-jR2CRlNcsTzxjaXKPlXNuh-Jo9-0yA1OE/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMTNkNWQyNjk3/M2Q0NzYyMmUyNDc3/ZjYwMWFlZDI5YTI4/ODhmYzc2MDkzMjAy/MjNkMWY1MDE3NTQw/MzI5NWVkZS93d3cu/cGx1cmFsc2lnaHQu/Y29tLw"
|
||||
},
|
||||
"language": "en",
|
||||
"family_friendly": true,
|
||||
"type": "search_result",
|
||||
"subtype": "generic",
|
||||
"meta_url": {
|
||||
"scheme": "https",
|
||||
"netloc": "pluralsight.com",
|
||||
"hostname": "www.pluralsight.com",
|
||||
"favicon": "https://imgs.search.brave.com/zvwQNSVu9-jR2CRlNcsTzxjaXKPlXNuh-Jo9-0yA1OE/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMTNkNWQyNjk3/M2Q0NzYyMmUyNDc3/ZjYwMWFlZDI5YTI4/ODhmYzc2MDkzMjAy/MjNkMWY1MDE3NTQw/MzI5NWVkZS93d3cu/cGx1cmFsc2lnaHQu/Y29tLw",
|
||||
"path": " › blog › blog"
|
||||
},
|
||||
"thumbnail": {
|
||||
"src": "https://imgs.search.brave.com/xrv5PHH2Bzmq2rcIYzk__8h5RqCj6kS3I6SGCNw5dZM/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/cGx1cmFsc2lnaHQu/Y29tL2NvbnRlbnQv/ZGFtL3BzL2ltYWdl/cy9yZXNvdXJjZS1j/ZW50ZXIvYmxvZy9o/ZWFkZXItaGVyby1p/bWFnZXMvUHl0aG9u/LndlYnA",
|
||||
"original": "https://www.pluralsight.com/content/dam/ps/images/resource-center/blog/header-hero-images/Python.webp",
|
||||
"logo": false
|
||||
},
|
||||
"age": "3 days ago",
|
||||
"extra_snippets": [
|
||||
"Whether it’s your first time programming or you’re a seasoned programmer, you’ll have to install or update Python every now and then --- or if necessary, uninstall it. In this article, you'll learn how to do just that.",
|
||||
"Some systems come with Python, so to start off, we’ll first check to see if it’s installed on your system before we proceed. To do that, we’ll need to open a terminal. Since you might be new to programming, let’s go over how to open a terminal for Linux, Windows, and macOS.",
|
||||
"Before we dive into setting up your system so you can program in Python, let’s talk terminal basics and benefits.",
|
||||
"However, let’s focus on why we need it for working with Python. We use a terminal, or command line, to:"
|
||||
]
|
||||
}
|
||||
],
|
||||
"family_friendly": true
|
||||
}
|
||||
}
|
||||
+442
@@ -0,0 +1,442 @@
|
||||
{
|
||||
"kind": "customsearch#search",
|
||||
"url": {
|
||||
"type": "application/json",
|
||||
"template": "https://www.googleapis.com/customsearch/v1?q={searchTerms}&num={count?}&start={startIndex?}&lr={language?}&safe={safe?}&cx={cx?}&sort={sort?}&filter={filter?}&gl={gl?}&cr={cr?}&googlehost={googleHost?}&c2coff={disableCnTwTranslation?}&hq={hq?}&hl={hl?}&siteSearch={siteSearch?}&siteSearchFilter={siteSearchFilter?}&exactTerms={exactTerms?}&excludeTerms={excludeTerms?}&linkSite={linkSite?}&orTerms={orTerms?}&dateRestrict={dateRestrict?}&lowRange={lowRange?}&highRange={highRange?}&searchType={searchType}&fileType={fileType?}&rights={rights?}&imgSize={imgSize?}&imgType={imgType?}&imgColorType={imgColorType?}&imgDominantColor={imgDominantColor?}&alt=json"
|
||||
},
|
||||
"queries": {
|
||||
"request": [
|
||||
{
|
||||
"title": "Google Custom Search - lectures",
|
||||
"totalResults": "2450000000",
|
||||
"searchTerms": "lectures",
|
||||
"count": 10,
|
||||
"startIndex": 1,
|
||||
"inputEncoding": "utf8",
|
||||
"outputEncoding": "utf8",
|
||||
"safe": "off",
|
||||
"cx": "0473ef98502d44e18"
|
||||
}
|
||||
],
|
||||
"nextPage": [
|
||||
{
|
||||
"title": "Google Custom Search - lectures",
|
||||
"totalResults": "2450000000",
|
||||
"searchTerms": "lectures",
|
||||
"count": 10,
|
||||
"startIndex": 11,
|
||||
"inputEncoding": "utf8",
|
||||
"outputEncoding": "utf8",
|
||||
"safe": "off",
|
||||
"cx": "0473ef98502d44e18"
|
||||
}
|
||||
]
|
||||
},
|
||||
"context": {
|
||||
"title": "LLM Search"
|
||||
},
|
||||
"searchInformation": {
|
||||
"searchTime": 0.445959,
|
||||
"formattedSearchTime": "0.45",
|
||||
"totalResults": "2450000000",
|
||||
"formattedTotalResults": "2,450,000,000"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"kind": "customsearch#result",
|
||||
"title": "The Feynman Lectures on Physics",
|
||||
"htmlTitle": "The Feynman \u003cb\u003eLectures\u003c/b\u003e on Physics",
|
||||
"link": "https://www.feynmanlectures.caltech.edu/",
|
||||
"displayLink": "www.feynmanlectures.caltech.edu",
|
||||
"snippet": "This edition has been designed for ease of reading on devices of any size or shape; text, figures and equations can all be zoomed without degradation.",
|
||||
"htmlSnippet": "This edition has been designed for ease of reading on devices of any size or shape; text, figures and equations can all be zoomed without degradation.",
|
||||
"cacheId": "CyXMWYWs9UEJ",
|
||||
"formattedUrl": "https://www.feynmanlectures.caltech.edu/",
|
||||
"htmlFormattedUrl": "https://www.feynman\u003cb\u003electures\u003c/b\u003e.caltech.edu/",
|
||||
"pagemap": {
|
||||
"metatags": [
|
||||
{
|
||||
"viewport": "width=device-width, initial-scale=1.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "customsearch#result",
|
||||
"title": "Video Lectures",
|
||||
"htmlTitle": "Video \u003cb\u003eLectures\u003c/b\u003e",
|
||||
"link": "https://www.reddit.com/r/lectures/",
|
||||
"displayLink": "www.reddit.com",
|
||||
"snippet": "r/lectures: This subreddit is all about video lectures, talks and interesting public speeches. The topics include mathematics, physics, computer…",
|
||||
"htmlSnippet": "r/\u003cb\u003electures\u003c/b\u003e: This subreddit is all about video \u003cb\u003electures\u003c/b\u003e, talks and interesting public speeches. The topics include mathematics, physics, computer…",
|
||||
"formattedUrl": "https://www.reddit.com/r/lectures/",
|
||||
"htmlFormattedUrl": "https://www.reddit.com/r/\u003cb\u003electures\u003c/b\u003e/",
|
||||
"pagemap": {
|
||||
"cse_thumbnail": [
|
||||
{
|
||||
"src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTZtOjhfkgUKQbL3DZxe5F6OVsgeDNffleObjJ7n9RllKQTSsimax7VIaY&s",
|
||||
"width": "192",
|
||||
"height": "192"
|
||||
}
|
||||
],
|
||||
"metatags": [
|
||||
{
|
||||
"og:image": "https://www.redditstatic.com/shreddit/assets/favicon/192x192.png",
|
||||
"theme-color": "#000000",
|
||||
"og:image:width": "256",
|
||||
"og:type": "website",
|
||||
"twitter:card": "summary",
|
||||
"twitter:title": "r/lectures",
|
||||
"og:site_name": "Reddit",
|
||||
"og:title": "r/lectures",
|
||||
"og:image:height": "256",
|
||||
"bingbot": "noarchive",
|
||||
"msapplication-navbutton-color": "#000000",
|
||||
"og:description": "This subreddit is all about video lectures, talks and interesting public speeches.\n\nThe topics include mathematics, physics, computer science, programming, engineering, biology, medicine, economics, politics, social sciences, and any other subjects!",
|
||||
"twitter:image": "https://www.redditstatic.com/shreddit/assets/favicon/192x192.png",
|
||||
"apple-mobile-web-app-status-bar-style": "black",
|
||||
"twitter:site": "@reddit",
|
||||
"viewport": "width=device-width, initial-scale=1, viewport-fit=cover",
|
||||
"apple-mobile-web-app-capable": "yes",
|
||||
"og:ttl": "600",
|
||||
"og:url": "https://www.reddit.com/r/lectures/"
|
||||
}
|
||||
],
|
||||
"cse_image": [
|
||||
{
|
||||
"src": "https://www.redditstatic.com/shreddit/assets/favicon/192x192.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "customsearch#result",
|
||||
"title": "Lectures & Discussions | Flint Institute of Arts",
|
||||
"htmlTitle": "\u003cb\u003eLectures\u003c/b\u003e & Discussions | Flint Institute of Arts",
|
||||
"link": "https://flintarts.org/events/lectures",
|
||||
"displayLink": "flintarts.org",
|
||||
"snippet": "It will trace the intricate relationship between jewelry, attire, and the expression of personal identity, social hierarchy, and spiritual belief systems that ...",
|
||||
"htmlSnippet": "It will trace the intricate relationship between jewelry, attire, and the expression of personal identity, social hierarchy, and spiritual belief systems that ...",
|
||||
"cacheId": "jvpb9DxrfxoJ",
|
||||
"formattedUrl": "https://flintarts.org/events/lectures",
|
||||
"htmlFormattedUrl": "https://flintarts.org/events/\u003cb\u003electures\u003c/b\u003e",
|
||||
"pagemap": {
|
||||
"cse_thumbnail": [
|
||||
{
|
||||
"src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS23tMtAeNhJbOWdGxShYsmnyzFdzOC9Hb7lRykA9Pw72z1IlKTkjTdZw&s",
|
||||
"width": "447",
|
||||
"height": "113"
|
||||
}
|
||||
],
|
||||
"metatags": [
|
||||
{
|
||||
"og:image": "https://flintarts.org/uploads/images/page-headers/_headerImage/nightshot.jpg",
|
||||
"og:type": "website",
|
||||
"viewport": "width=device-width, initial-scale=1",
|
||||
"og:title": "Lectures & Discussions | Flint Institute of Arts",
|
||||
"og:description": "The Flint Institute of Arts is the second largest art museum in Michigan and one of the largest museum art schools in the nation."
|
||||
}
|
||||
],
|
||||
"cse_image": [
|
||||
{
|
||||
"src": "https://flintarts.org/uploads/images/page-headers/_headerImage/nightshot.jpg"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "customsearch#result",
|
||||
"title": "Mandel Lectures | Mandel Center for the Humanities ... - Waltham",
|
||||
"htmlTitle": "Mandel \u003cb\u003eLectures\u003c/b\u003e | Mandel Center for the Humanities ... - Waltham",
|
||||
"link": "https://www.brandeis.edu/mandel-center-humanities/mandel-lectures.html",
|
||||
"displayLink": "www.brandeis.edu",
|
||||
"snippet": "Past Lectures · Lecture 1: \"Invisible Music: The Sonic Idea of Black Revolution From Captivity to Reconstruction\" · Lecture 2: \"Solidarity in Sound: Grassroots ...",
|
||||
"htmlSnippet": "Past \u003cb\u003eLectures\u003c/b\u003e · \u003cb\u003eLecture\u003c/b\u003e 1: "Invisible Music: The Sonic Idea of Black Revolution From Captivity to Reconstruction" · \u003cb\u003eLecture\u003c/b\u003e 2: "Solidarity in Sound: Grassroots ...",
|
||||
"cacheId": "cQLOZr0kgEEJ",
|
||||
"formattedUrl": "https://www.brandeis.edu/mandel-center-humanities/mandel-lectures.html",
|
||||
"htmlFormattedUrl": "https://www.brandeis.edu/mandel-center-humanities/mandel-\u003cb\u003electures\u003c/b\u003e.html",
|
||||
"pagemap": {
|
||||
"cse_thumbnail": [
|
||||
{
|
||||
"src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQWlU7bcJ5pIHk7RBCk2QKE-48ejF7hyPV0pr-20_cBt2BGdfKtiYXBuyw&s",
|
||||
"width": "275",
|
||||
"height": "183"
|
||||
}
|
||||
],
|
||||
"metatags": [
|
||||
{
|
||||
"og:image": "https://www.brandeis.edu/mandel-center-humanities/events/events-images/mlhzumba",
|
||||
"twitter:card": "summary_large_image",
|
||||
"viewport": "width=device-width,initial-scale=1,minimum-scale=1",
|
||||
"og:title": "Mandel Lectures in the Humanities",
|
||||
"og:url": "https://www.brandeis.edu/mandel-center-humanities/mandel-lectures.html",
|
||||
"og:description": "Annual Lecture Series",
|
||||
"twitter:image": "https://www.brandeis.edu/mandel-center-humanities/events/events-images/mlhzumba"
|
||||
}
|
||||
],
|
||||
"cse_image": [
|
||||
{
|
||||
"src": "https://www.brandeis.edu/mandel-center-humanities/events/events-images/mlhzumba"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "customsearch#result",
|
||||
"title": "Brian Douglas - YouTube",
|
||||
"htmlTitle": "Brian Douglas - YouTube",
|
||||
"link": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
|
||||
"displayLink": "www.youtube.com",
|
||||
"snippet": "Welcome to Control Systems Lectures! This collection of videos is intended to supplement a first year controls class, not replace it.",
|
||||
"htmlSnippet": "Welcome to Control Systems \u003cb\u003eLectures\u003c/b\u003e! This collection of videos is intended to supplement a first year controls class, not replace it.",
|
||||
"cacheId": "NEROyBHolL0J",
|
||||
"formattedUrl": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
|
||||
"htmlFormattedUrl": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
|
||||
"pagemap": {
|
||||
"hcard": [
|
||||
{
|
||||
"fn": "Brian Douglas",
|
||||
"url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg"
|
||||
}
|
||||
],
|
||||
"cse_thumbnail": [
|
||||
{
|
||||
"src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR7G0CeCBz_wVTZgjnhEr2QbiKP7f3uYzKitZYn74Mi32cDmVxvsegJoLI&s",
|
||||
"width": "225",
|
||||
"height": "225"
|
||||
}
|
||||
],
|
||||
"imageobject": [
|
||||
{
|
||||
"width": "900",
|
||||
"url": "https://yt3.googleusercontent.com/ytc/AIdro_nLo68wetImbwGUYP3stve_iKmAEccjhqB-q4o79xdInN4=s900-c-k-c0x00ffffff-no-rj",
|
||||
"height": "900"
|
||||
}
|
||||
],
|
||||
"person": [
|
||||
{
|
||||
"name": "Brian Douglas",
|
||||
"url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg"
|
||||
}
|
||||
],
|
||||
"metatags": [
|
||||
{
|
||||
"apple-itunes-app": "app-id=544007664, app-argument=https://m.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg?referring_app=com.apple.mobilesafari-smartbanner, affiliate-data=ct=smart_app_banner_polymer&pt=9008",
|
||||
"og:image": "https://yt3.googleusercontent.com/ytc/AIdro_nLo68wetImbwGUYP3stve_iKmAEccjhqB-q4o79xdInN4=s900-c-k-c0x00ffffff-no-rj",
|
||||
"twitter:app:url:iphone": "vnd.youtube://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
|
||||
"twitter:app:id:googleplay": "com.google.android.youtube",
|
||||
"theme-color": "rgb(255, 255, 255)",
|
||||
"og:image:width": "900",
|
||||
"twitter:card": "summary",
|
||||
"og:site_name": "YouTube",
|
||||
"twitter:url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
|
||||
"twitter:app:url:ipad": "vnd.youtube://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
|
||||
"al:android:package": "com.google.android.youtube",
|
||||
"twitter:app:name:googleplay": "YouTube",
|
||||
"al:ios:url": "vnd.youtube://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
|
||||
"twitter:app:id:iphone": "544007664",
|
||||
"og:description": "Welcome to Control Systems Lectures! This collection of videos is intended to supplement a first year controls class, not replace it. My goal is to take specific concepts in controls and expand on them in order to provide an intuitive understanding which will ultimately make you a better controls engineer. \n\nI'm glad you made it to my channel and I hope you find it useful.\n\nShoot me a message at controlsystemlectures@gmail.com, leave a comment or question and I'll get back to you if I can. Don't forget to subscribe!\n \nTwitter: @BrianBDouglas for engineering tweets and announcement of new videos.\nWebpage: http://engineeringmedia.com\n\nHere is the hardware/software I use: http://www.youtube.com/watch?v=m-M5_mIyHe4\n\nHere's a list of my favorite references: http://bit.ly/2skvmWd\n\n--Brian",
|
||||
"al:ios:app_store_id": "544007664",
|
||||
"twitter:image": "https://yt3.googleusercontent.com/ytc/AIdro_nLo68wetImbwGUYP3stve_iKmAEccjhqB-q4o79xdInN4=s900-c-k-c0x00ffffff-no-rj",
|
||||
"twitter:site": "@youtube",
|
||||
"og:type": "profile",
|
||||
"twitter:title": "Brian Douglas",
|
||||
"al:ios:app_name": "YouTube",
|
||||
"og:title": "Brian Douglas",
|
||||
"og:image:height": "900",
|
||||
"twitter:app:id:ipad": "544007664",
|
||||
"al:web:url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg?feature=applinks",
|
||||
"al:android:url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg?feature=applinks",
|
||||
"fb:app_id": "87741124305",
|
||||
"twitter:app:url:googleplay": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
|
||||
"twitter:app:name:ipad": "YouTube",
|
||||
"viewport": "width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no,",
|
||||
"twitter:description": "Welcome to Control Systems Lectures! This collection of videos is intended to supplement a first year controls class, not replace it. My goal is to take specific concepts in controls and expand on them in order to provide an intuitive understanding which will ultimately make you a better controls engineer. \n\nI'm glad you made it to my channel and I hope you find it useful.\n\nShoot me a message at controlsystemlectures@gmail.com, leave a comment or question and I'll get back to you if I can. Don't forget to subscribe!\n \nTwitter: @BrianBDouglas for engineering tweets and announcement of new videos.\nWebpage: http://engineeringmedia.com\n\nHere is the hardware/software I use: http://www.youtube.com/watch?v=m-M5_mIyHe4\n\nHere's a list of my favorite references: http://bit.ly/2skvmWd\n\n--Brian",
|
||||
"og:url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
|
||||
"al:android:app_name": "YouTube",
|
||||
"twitter:app:name:iphone": "YouTube"
|
||||
}
|
||||
],
|
||||
"cse_image": [
|
||||
{
|
||||
"src": "https://yt3.googleusercontent.com/ytc/AIdro_nLo68wetImbwGUYP3stve_iKmAEccjhqB-q4o79xdInN4=s900-c-k-c0x00ffffff-no-rj"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "customsearch#result",
|
||||
"title": "Lecture - Wikipedia",
|
||||
"htmlTitle": "\u003cb\u003eLecture\u003c/b\u003e - Wikipedia",
|
||||
"link": "https://en.wikipedia.org/wiki/Lecture",
|
||||
"displayLink": "en.wikipedia.org",
|
||||
"snippet": "Lecture ... For the academic rank, see Lecturer. A lecture (from Latin: lēctūra 'reading') is an oral presentation intended to present information or teach people ...",
|
||||
"htmlSnippet": "\u003cb\u003eLecture\u003c/b\u003e ... For the academic rank, see \u003cb\u003eLecturer\u003c/b\u003e. A \u003cb\u003electure\u003c/b\u003e (from Latin: lēctūra 'reading') is an oral presentation intended to present information or teach people ...",
|
||||
"cacheId": "d9Pjta02fmgJ",
|
||||
"formattedUrl": "https://en.wikipedia.org/wiki/Lecture",
|
||||
"htmlFormattedUrl": "https://en.wikipedia.org/wiki/Lecture",
|
||||
"pagemap": {
|
||||
"metatags": [
|
||||
{
|
||||
"referrer": "origin",
|
||||
"og:image": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/26/ADFA_Lecture_Theatres.jpg/1200px-ADFA_Lecture_Theatres.jpg",
|
||||
"theme-color": "#eaecf0",
|
||||
"og:image:width": "1200",
|
||||
"og:type": "website",
|
||||
"viewport": "width=device-width, initial-scale=1.0, user-scalable=yes, minimum-scale=0.25, maximum-scale=5.0",
|
||||
"og:title": "Lecture - Wikipedia",
|
||||
"og:image:height": "799",
|
||||
"format-detection": "telephone=no"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "customsearch#result",
|
||||
"title": "Mount Wilson Observatory | Lectures",
|
||||
"htmlTitle": "Mount Wilson Observatory | \u003cb\u003eLectures\u003c/b\u003e",
|
||||
"link": "https://www.mtwilson.edu/lectures/",
|
||||
"displayLink": "www.mtwilson.edu",
|
||||
"snippet": "Talks & Telescopes: August 24, 2024 – Panel: The Triumph of Hubble ... Compelling talks followed by picnicking and convivial stargazing through both the big ...",
|
||||
"htmlSnippet": "Talks & Telescopes: August 24, 2024 – Panel: The Triumph of Hubble ... Compelling talks followed by picnicking and convivial stargazing through both the big ...",
|
||||
"cacheId": "wdXI0azqx5UJ",
|
||||
"formattedUrl": "https://www.mtwilson.edu/lectures/",
|
||||
"htmlFormattedUrl": "https://www.mtwilson.edu/\u003cb\u003electures\u003c/b\u003e/",
|
||||
"pagemap": {
|
||||
"metatags": [
|
||||
{
|
||||
"viewport": "width=device-width,initial-scale=1,user-scalable=no"
|
||||
}
|
||||
],
|
||||
"webpage": [
|
||||
{
|
||||
"image": "http://www.mtwilson.edu/wp-content/uploads/2016/09/Logo.jpg",
|
||||
"url": "https://www.facebook.com/WilsonObs"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "customsearch#result",
|
||||
"title": "Lectures | NBER",
|
||||
"htmlTitle": "\u003cb\u003eLectures\u003c/b\u003e | NBER",
|
||||
"link": "https://www.nber.org/research/lectures",
|
||||
"displayLink": "www.nber.org",
|
||||
"snippet": "Results 1 - 50 of 354 ... Among featured events at the NBER Summer Institute are the Martin Feldstein Lecture, which examines a current issue involving economic ...",
|
||||
"htmlSnippet": "Results 1 - 50 of 354 \u003cb\u003e...\u003c/b\u003e Among featured events at the NBER Summer Institute are the Martin Feldstein \u003cb\u003eLecture\u003c/b\u003e, which examines a current issue involving economic ...",
|
||||
"cacheId": "CvvP3U3nb44J",
|
||||
"formattedUrl": "https://www.nber.org/research/lectures",
|
||||
"htmlFormattedUrl": "https://www.nber.org/research/\u003cb\u003electures\u003c/b\u003e",
|
||||
"pagemap": {
|
||||
"cse_thumbnail": [
|
||||
{
|
||||
"src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTmeViEZyV1YmFEFLhcA6WdgAG3v3RV6tB93ncyxSJ5JPst_p2aWrL7D1k&s",
|
||||
"width": "310",
|
||||
"height": "163"
|
||||
}
|
||||
],
|
||||
"metatags": [
|
||||
{
|
||||
"og:image": "https://www.nber.org/sites/default/files/2022-06/NBER-FB-Share-Tile-1200.jpg",
|
||||
"og:site_name": "NBER",
|
||||
"handheldfriendly": "true",
|
||||
"viewport": "width=device-width, initial-scale=1.0",
|
||||
"og:title": "Lectures",
|
||||
"mobileoptimized": "width",
|
||||
"og:url": "https://www.nber.org/research/lectures"
|
||||
}
|
||||
],
|
||||
"cse_image": [
|
||||
{
|
||||
"src": "https://www.nber.org/sites/default/files/2022-06/NBER-FB-Share-Tile-1200.jpg"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "customsearch#result",
|
||||
"title": "STUDENTS CANNOT ACCESS RECORDED LECTURES ... - Solved",
|
||||
"htmlTitle": "STUDENTS CANNOT ACCESS RECORDED LECTURES ... - Solved",
|
||||
"link": "https://community.canvaslms.com/t5/Canvas-Question-Forum/STUDENTS-CANNOT-ACCESS-RECORDED-LECTURES/td-p/190358",
|
||||
"displayLink": "community.canvaslms.com",
|
||||
"snippet": "Mar 19, 2020 ... I believe the issue is that students were not invited. Are you trying to capture your screen? If not, there is an option to just record your web ...",
|
||||
"htmlSnippet": "Mar 19, 2020 \u003cb\u003e...\u003c/b\u003e I believe the issue is that students were not invited. Are you trying to capture your screen? If not, there is an option to just record your web ...",
|
||||
"cacheId": "wqrynQXX61sJ",
|
||||
"formattedUrl": "https://community.canvaslms.com/t5/Canvas...LECTURES/td-p/190358",
|
||||
"htmlFormattedUrl": "https://community.canvaslms.com/t5/Canvas...\u003cb\u003eLECTURES\u003c/b\u003e/td-p/190358",
|
||||
"pagemap": {
|
||||
"cse_thumbnail": [
|
||||
{
|
||||
"src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRUqXau3N8LfKgSD7OJOvV7xzGarLKRU-ckWXy1ZQ1p4CLPsedvLKmLMhk&s",
|
||||
"width": "310",
|
||||
"height": "163"
|
||||
}
|
||||
],
|
||||
"metatags": [
|
||||
{
|
||||
"og:image": "https://community.canvaslms.com/html/@6A1FDD4D5FF35E4BBB4083A1022FA0DB/assets/CommunityPreview23.png",
|
||||
"og:type": "article",
|
||||
"article:section": "Canvas Question Forum",
|
||||
"article:published_time": "2020-03-19T15:50:03.409Z",
|
||||
"og:site_name": "Instructure Community",
|
||||
"article:modified_time": "2020-03-19T13:55:53-07:00",
|
||||
"viewport": "width=device-width, initial-scale=1.0, user-scalable=yes",
|
||||
"og:title": "STUDENTS CANNOT ACCESS RECORDED LECTURES",
|
||||
"og:url": "https://community.canvaslms.com/t5/Canvas-Question-Forum/STUDENTS-CANNOT-ACCESS-RECORDED-LECTURES/m-p/190358#M93667",
|
||||
"og:description": "I can access and see my recorded lectures but my students can't. They have an error message when they try to open the recorded presentation or notes.",
|
||||
"article:author": "https://community.canvaslms.com/t5/user/viewprofilepage/user-id/794287",
|
||||
"twitter:image": "https://community.canvaslms.com/html/@6A1FDD4D5FF35E4BBB4083A1022FA0DB/assets/CommunityPreview23.png"
|
||||
}
|
||||
],
|
||||
"cse_image": [
|
||||
{
|
||||
"src": "https://community.canvaslms.com/html/@6A1FDD4D5FF35E4BBB4083A1022FA0DB/assets/CommunityPreview23.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "customsearch#result",
|
||||
"title": "Public Lecture Series - Sam Fox School of Design & Visual Arts",
|
||||
"htmlTitle": "Public \u003cb\u003eLecture\u003c/b\u003e Series - Sam Fox School of Design & Visual Arts",
|
||||
"link": "https://samfoxschool.wustl.edu/calendar/series/2-public-lecture-series",
|
||||
"displayLink": "samfoxschool.wustl.edu",
|
||||
"snippet": "The Sam Fox School's Spring 2024 Public Lecture Series highlights design and art as catalysts for change. Renowned speakers will delve into themes like ...",
|
||||
"htmlSnippet": "The Sam Fox School's Spring 2024 Public \u003cb\u003eLecture\u003c/b\u003e Series highlights design and art as catalysts for change. Renowned speakers will delve into themes like ...",
|
||||
"cacheId": "B-cgQG0j6tUJ",
|
||||
"formattedUrl": "https://samfoxschool.wustl.edu/calendar/series/2-public-lecture-series",
|
||||
"htmlFormattedUrl": "https://samfoxschool.wustl.edu/calendar/series/2-public-lecture-series",
|
||||
"pagemap": {
|
||||
"cse_thumbnail": [
|
||||
{
|
||||
"src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQSmHaGianm-64m-qauYjkPK_Q0JKWe-7yom4m1ogFYTmpWArA7k6dmk0sR&s",
|
||||
"width": "307",
|
||||
"height": "164"
|
||||
}
|
||||
],
|
||||
"website": [
|
||||
{
|
||||
"name": "Public Lecture Series - Sam Fox School of Design & Visual Arts — Washington University in St. Louis"
|
||||
}
|
||||
],
|
||||
"metatags": [
|
||||
{
|
||||
"og:image": "https://dvsp0hlm0xrn3.cloudfront.net/assets/default_og_image-44e73dee4b9d1e2c6a6295901371270c8ec5899eaed48ee8167a9b12f1b0f8b3.jpg",
|
||||
"og:type": "website",
|
||||
"og:site_name": "Sam Fox School of Design & Visual Arts — Washington University in St. Louis",
|
||||
"viewport": "width=device-width, initial-scale=1.0",
|
||||
"og:title": "Public Lecture Series - Sam Fox School of Design & Visual Arts — Washington University in St. Louis",
|
||||
"csrf-token": "jBQsfZGY3RH8NVs0-KVDBYB-2N2kib4UYZHYdrShfTdLkvzfSvGeOaMrRKTRdYBPRKzdcGIuP7zwm9etqX_uvg",
|
||||
"csrf-param": "authenticity_token",
|
||||
"og:description": "The Sam Fox School's Spring 2024 Public Lecture Series highlights design and art as catalysts for change. Renowned speakers will delve into themes like social equity, resilient cities, and the impact of emerging technologies on contemporary life. Speakers include artists, architects, designers, and critics of the highest caliber, widely recognized for their research-based practices and multidisciplinary approaches to their fields."
|
||||
}
|
||||
],
|
||||
"cse_image": [
|
||||
{
|
||||
"src": "https://dvsp0hlm0xrn3.cloudfront.net/assets/default_og_image-44e73dee4b9d1e2c6a6295901371270c8ec5899eaed48ee8167a9b12f1b0f8b3.jpg"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+476
@@ -0,0 +1,476 @@
|
||||
{
|
||||
"query": "python",
|
||||
"number_of_results": 116000000,
|
||||
"results": [
|
||||
{
|
||||
"url": "https://www.python.org/",
|
||||
"title": "Welcome to Python.org",
|
||||
"content": "Python is a versatile and powerful language that lets you work quickly and integrate systems more effectively. Learn how to get started, download the latest version, access documentation, find jobs, and join the Python community.",
|
||||
"engine": "bing",
|
||||
"parsed_url": ["https", "www.python.org", "/", "", "", ""],
|
||||
"template": "default.html",
|
||||
"engines": ["bing", "qwant", "duckduckgo"],
|
||||
"positions": [1, 1, 1],
|
||||
"score": 9.0,
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"url": "https://wiki.nerdvpn.de/wiki/Python_(programming_language)",
|
||||
"title": "Python (programming language) - Wikipedia",
|
||||
"content": "Python is a high-level, general-purpose programming language. Its design philosophy emphasizes code readability with the use of significant indentation. Python is dynamically typed and garbage-collected. It supports multiple programming paradigms, including structured (particularly procedural), object-oriented and functional programming.",
|
||||
"engine": "bing",
|
||||
"parsed_url": ["https", "wiki.nerdvpn.de", "/wiki/Python_(programming_language)", "", "", ""],
|
||||
"template": "default.html",
|
||||
"engines": ["bing", "qwant", "duckduckgo"],
|
||||
"positions": [4, 3, 2],
|
||||
"score": 3.25,
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"url": "https://docs.python.org/3/tutorial/index.html",
|
||||
"title": "The Python Tutorial \u2014 Python 3.12.3 documentation",
|
||||
"content": "3 days ago \u00b7 Python is an easy to learn, powerful programming language. It has efficient high-level data structures and a simple but effective approach to object-oriented programming. Python\u2019s elegant syntax and dynamic typing, together with its interpreted nature, make it an ideal language for scripting and rapid application development in many \u2026",
|
||||
"engine": "bing",
|
||||
"parsed_url": ["https", "docs.python.org", "/3/tutorial/index.html", "", "", ""],
|
||||
"template": "default.html",
|
||||
"engines": ["bing", "qwant", "duckduckgo"],
|
||||
"positions": [5, 5, 3],
|
||||
"score": 2.2,
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"url": "https://www.python.org/downloads/",
|
||||
"title": "Download Python | Python.org",
|
||||
"content": "Python is a popular programming language for various purposes. Find the latest version of Python for different operating systems, download release notes, and learn about the development process.",
|
||||
"engine": "bing",
|
||||
"parsed_url": ["https", "www.python.org", "/downloads/", "", "", ""],
|
||||
"template": "default.html",
|
||||
"engines": ["bing", "duckduckgo"],
|
||||
"positions": [2, 2],
|
||||
"score": 2.0,
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"url": "https://www.python.org/about/gettingstarted/",
|
||||
"title": "Python For Beginners | Python.org",
|
||||
"content": "Learn the basics of Python, a popular and easy-to-use programming language, from installing it to using it for various purposes. Find out how to access online documentation, tutorials, books, code samples, and more resources to help you get started with Python.",
|
||||
"engine": "bing",
|
||||
"parsed_url": ["https", "www.python.org", "/about/gettingstarted/", "", "", ""],
|
||||
"template": "default.html",
|
||||
"engines": ["bing", "qwant", "duckduckgo"],
|
||||
"positions": [9, 4, 4],
|
||||
"score": 1.8333333333333333,
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"url": "https://www.python.org/shell/",
|
||||
"title": "Welcome to Python.org",
|
||||
"content": "Python is a versatile and easy-to-use programming language that lets you work quickly. Learn more about Python, download the latest version, access documentation, find jobs, and join the community.",
|
||||
"engine": "bing",
|
||||
"parsed_url": ["https", "www.python.org", "/shell/", "", "", ""],
|
||||
"template": "default.html",
|
||||
"engines": ["bing", "qwant", "duckduckgo"],
|
||||
"positions": [3, 10, 8],
|
||||
"score": 1.675,
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"url": "https://realpython.com/",
|
||||
"title": "Python Tutorials \u2013 Real Python",
|
||||
"content": "Real Python offers comprehensive and up-to-date tutorials, books, and courses for Python developers of all skill levels. Whether you want to learn Python basics, web development, data science, machine learning, or more, you can find clear and practical guides and code examples here.",
|
||||
"engine": "bing",
|
||||
"parsed_url": ["https", "realpython.com", "/", "", "", ""],
|
||||
"template": "default.html",
|
||||
"engines": ["bing", "qwant", "duckduckgo"],
|
||||
"positions": [6, 6, 5],
|
||||
"score": 1.6,
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"url": "https://wiki.nerdvpn.de/wiki/Python",
|
||||
"title": "Python",
|
||||
"content": "Topics referred to by the same term",
|
||||
"engine": "wikipedia",
|
||||
"parsed_url": ["https", "wiki.nerdvpn.de", "/wiki/Python", "", "", ""],
|
||||
"template": "default.html",
|
||||
"engines": ["wikipedia"],
|
||||
"positions": [1],
|
||||
"score": 1.0,
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"title": "Online Python - IDE, Editor, Compiler, Interpreter",
|
||||
"content": "Online Python IDE is a free online tool that lets you write, execute, and share Python code in the web browser. Learn about Python, its features, and its popularity as a general-purpose programming language for web development, data science, and more.",
|
||||
"url": "https://www.online-python.com/",
|
||||
"engine": "duckduckgo",
|
||||
"parsed_url": ["https", "www.online-python.com", "/", "", "", ""],
|
||||
"template": "default.html",
|
||||
"engines": ["qwant", "duckduckgo"],
|
||||
"positions": [8, 6],
|
||||
"score": 0.5833333333333333,
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"url": "https://micropython.org/",
|
||||
"title": "MicroPython - Python for microcontrollers",
|
||||
"content": "MicroPython is a full Python compiler and runtime that runs on the bare-metal. You get an interactive prompt (the REPL) to execute commands immediately, along ...",
|
||||
"img_src": null,
|
||||
"engine": "google",
|
||||
"parsed_url": ["https", "micropython.org", "/", "", "", ""],
|
||||
"template": "default.html",
|
||||
"engines": ["google"],
|
||||
"positions": [1],
|
||||
"score": 1.0,
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"url": "https://dictionary.cambridge.org/uk/dictionary/english/python",
|
||||
"title": "PYTHON | \u0417\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0432 \u0430\u043d\u0433\u043b\u0456\u0439\u0441\u044c\u043a\u0456\u0439 \u043c\u043e\u0432\u0456 - Cambridge Dictionary",
|
||||
"content": "Apr 17, 2024 \u2014 \u0412\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f PYTHON: 1. a very large snake that kills animals for food by wrapping itself around them and crushing them\u2026. \u0414\u0456\u0437\u043d\u0430\u0439\u0442\u0435\u0441\u044f \u0431\u0456\u043b\u044c\u0448\u0435.",
|
||||
"img_src": null,
|
||||
"engine": "google",
|
||||
"parsed_url": [
|
||||
"https",
|
||||
"dictionary.cambridge.org",
|
||||
"/uk/dictionary/english/python",
|
||||
"",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"template": "default.html",
|
||||
"engines": ["google"],
|
||||
"positions": [2],
|
||||
"score": 0.5,
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"url": "https://www.codetoday.co.uk/code",
|
||||
"title": "Web-based Python Editor (with Turtle graphics)",
|
||||
"content": "Quick way of starting to write Python code, including drawing with Turtle, provided by CodeToday using Trinket.io Ideal for young children to start ...",
|
||||
"img_src": null,
|
||||
"engine": "google",
|
||||
"parsed_url": ["https", "www.codetoday.co.uk", "/code", "", "", ""],
|
||||
"template": "default.html",
|
||||
"engines": ["google"],
|
||||
"positions": [3],
|
||||
"score": 0.3333333333333333,
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"url": "https://snapcraft.io/docs/python-plugin",
|
||||
"title": "The python plugin | Snapcraft documentation",
|
||||
"content": "The python plugin can be used by either Python 2 or Python 3 based parts using a setup.py script for building the project, or using a package published to ...",
|
||||
"img_src": null,
|
||||
"engine": "google",
|
||||
"parsed_url": ["https", "snapcraft.io", "/docs/python-plugin", "", "", ""],
|
||||
"template": "default.html",
|
||||
"engines": ["google"],
|
||||
"positions": [4],
|
||||
"score": 0.25,
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"url": "https://www.developer-tech.com/categories/developer-languages/developer-languages-python/",
|
||||
"title": "Latest Python Developer News",
|
||||
"content": "Python's status as the primary language for AI and machine learning projects, from its extensive data-handling capabilities to its flexibility and ...",
|
||||
"img_src": null,
|
||||
"engine": "google",
|
||||
"parsed_url": [
|
||||
"https",
|
||||
"www.developer-tech.com",
|
||||
"/categories/developer-languages/developer-languages-python/",
|
||||
"",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"template": "default.html",
|
||||
"engines": ["google"],
|
||||
"positions": [5],
|
||||
"score": 0.2,
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"url": "https://subjectguides.york.ac.uk/coding/python",
|
||||
"title": "Coding: a Practical Guide - Python - Subject Guides",
|
||||
"content": "Python is a coding language used for a wide range of things, including working with data, building systems and software, and even creating games.",
|
||||
"img_src": null,
|
||||
"engine": "google",
|
||||
"parsed_url": ["https", "subjectguides.york.ac.uk", "/coding/python", "", "", ""],
|
||||
"template": "default.html",
|
||||
"engines": ["google"],
|
||||
"positions": [6],
|
||||
"score": 0.16666666666666666,
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"url": "https://hub.salford.ac.uk/psytech/python/getting-started-python/",
|
||||
"title": "Getting Started - Python - Salford PsyTech Home - The Hub",
|
||||
"content": "Python in itself is a very friendly programming language, when we get to grips with writing code, once you grasp the logic, it will become very intuitive.",
|
||||
"img_src": null,
|
||||
"engine": "google",
|
||||
"parsed_url": [
|
||||
"https",
|
||||
"hub.salford.ac.uk",
|
||||
"/psytech/python/getting-started-python/",
|
||||
"",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"template": "default.html",
|
||||
"engines": ["google"],
|
||||
"positions": [7],
|
||||
"score": 0.14285714285714285,
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"url": "https://snapcraft.io/docs/python-apps",
|
||||
"title": "Python apps | Snapcraft documentation",
|
||||
"content": "Snapcraft can be used to package and distribute Python applications in a way that enables convenient installation by users. The process of creating a snap ...",
|
||||
"img_src": null,
|
||||
"engine": "google",
|
||||
"parsed_url": ["https", "snapcraft.io", "/docs/python-apps", "", "", ""],
|
||||
"template": "default.html",
|
||||
"engines": ["google"],
|
||||
"positions": [8],
|
||||
"score": 0.125,
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"url": "https://anvil.works/",
|
||||
"title": "Anvil | Build Web Apps with Nothing but Python",
|
||||
"content": "Anvil is a free Python-based drag-and-drop web app builder.\u200eSign Up \u00b7 \u200eSign in \u00b7 \u200ePricing \u00b7 \u200eForum",
|
||||
"img_src": null,
|
||||
"engine": "google",
|
||||
"parsed_url": ["https", "anvil.works", "/", "", "", ""],
|
||||
"template": "default.html",
|
||||
"engines": ["google"],
|
||||
"positions": [9],
|
||||
"score": 0.1111111111111111,
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"url": "https://docs.python.org/",
|
||||
"title": "Python 3.12.3 documentation",
|
||||
"content": "3 days ago \u00b7 This is the official documentation for Python 3.12.3. Documentation sections: What's new in Python 3.12? Or all \"What's new\" documents since Python 2.0. Tutorial. Start here: a tour of Python's syntax and features. Library reference. Standard library and builtins. Language reference.",
|
||||
"engine": "bing",
|
||||
"parsed_url": ["https", "docs.python.org", "/", "", "", ""],
|
||||
"template": "default.html",
|
||||
"engines": ["bing", "duckduckgo"],
|
||||
"positions": [7, 13],
|
||||
"score": 0.43956043956043955,
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"title": "How to Use Python: Your First Steps - Real Python",
|
||||
"content": "Learn the basics of Python syntax, installation, error handling, and more in this tutorial. You'll also code your first Python program and test your knowledge with a quiz.",
|
||||
"url": "https://realpython.com/python-first-steps/",
|
||||
"engine": "duckduckgo",
|
||||
"parsed_url": ["https", "realpython.com", "/python-first-steps/", "", "", ""],
|
||||
"template": "default.html",
|
||||
"engines": ["qwant", "duckduckgo"],
|
||||
"positions": [14, 7],
|
||||
"score": 0.42857142857142855,
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"title": "The Python Tutorial \u2014 Python 3.11.8 documentation",
|
||||
"content": "This tutorial introduces the reader informally to the basic concepts and features of the Python language and system. It helps to have a Python interpreter handy for hands-on experience, but all examples are self-contained, so the tutorial can be read off-line as well. For a description of standard objects and modules, see The Python Standard ...",
|
||||
"url": "https://docs.python.org/3.11/tutorial/",
|
||||
"engine": "duckduckgo",
|
||||
"parsed_url": ["https", "docs.python.org", "/3.11/tutorial/", "", "", ""],
|
||||
"template": "default.html",
|
||||
"engines": ["duckduckgo"],
|
||||
"positions": [7],
|
||||
"score": 0.14285714285714285,
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"url": "https://realpython.com/python-introduction/",
|
||||
"title": "Introduction to Python 3 \u2013 Real Python",
|
||||
"content": "Python programming language, including a brief history of the development of Python and reasons why you might select Python as your language of choice.",
|
||||
"engine": "bing",
|
||||
"parsed_url": ["https", "realpython.com", "/python-introduction/", "", "", ""],
|
||||
"template": "default.html",
|
||||
"engines": ["bing"],
|
||||
"positions": [8],
|
||||
"score": 0.125,
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"title": "Our Documentation | Python.org",
|
||||
"content": "Find online or download Python's documentation, tutorials, and guides for beginners and advanced users. Learn how to port from Python 2 to Python 3, contribute to Python, and access Python videos and books.",
|
||||
"url": "https://www.python.org/doc/",
|
||||
"engine": "duckduckgo",
|
||||
"parsed_url": ["https", "www.python.org", "/doc/", "", "", ""],
|
||||
"template": "default.html",
|
||||
"engines": ["duckduckgo"],
|
||||
"positions": [9],
|
||||
"score": 0.1111111111111111,
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"title": "Welcome to Python.org",
|
||||
"url": "http://www.get-python.org/shell/",
|
||||
"content": "The mission of the Python Software Foundation is to promote, protect, and advance the Python programming language, and to support and facilitate the growth of a diverse and international community of Python programmers. Learn more. Become a Member Donate to the PSF.",
|
||||
"engine": "qwant",
|
||||
"parsed_url": ["http", "www.get-python.org", "/shell/", "", "", ""],
|
||||
"template": "default.html",
|
||||
"engines": ["qwant"],
|
||||
"positions": [9],
|
||||
"score": 0.1111111111111111,
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"title": "About Python\u2122 | Python.org",
|
||||
"content": "Python is a powerful, fast, and versatile programming language that runs on various platforms and is easy to learn. Learn how to get started, explore the applications, and join the community of Python programmers and users.",
|
||||
"url": "https://www.python.org/about/",
|
||||
"engine": "duckduckgo",
|
||||
"parsed_url": ["https", "www.python.org", "/about/", "", "", ""],
|
||||
"template": "default.html",
|
||||
"engines": ["duckduckgo"],
|
||||
"positions": [11],
|
||||
"score": 0.09090909090909091,
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"title": "Online Python Compiler (Interpreter) - Programiz",
|
||||
"content": "Write and run Python code using this online tool. You can use Python Shell like IDLE, and take inputs from the user in our Python compiler.",
|
||||
"url": "https://www.programiz.com/python-programming/online-compiler/",
|
||||
"engine": "duckduckgo",
|
||||
"parsed_url": [
|
||||
"https",
|
||||
"www.programiz.com",
|
||||
"/python-programming/online-compiler/",
|
||||
"",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"template": "default.html",
|
||||
"engines": ["duckduckgo"],
|
||||
"positions": [12],
|
||||
"score": 0.08333333333333333,
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"title": "Welcome to Python.org",
|
||||
"content": "Python is a versatile and powerful language that lets you work quickly and integrate systems more effectively. Download the latest version, read the documentation, find jobs, events, success stories, and more on Python.org.",
|
||||
"url": "https://www.python.org/?downloads",
|
||||
"engine": "duckduckgo",
|
||||
"parsed_url": ["https", "www.python.org", "/", "", "downloads", ""],
|
||||
"template": "default.html",
|
||||
"engines": ["duckduckgo"],
|
||||
"positions": [15],
|
||||
"score": 0.06666666666666667,
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"url": "https://www.matillion.com/blog/the-importance-of-python-and-its-growing-influence-on-data-productivty-a-matillion-perspective",
|
||||
"title": "The Importance of Python and its Growing Influence on ...",
|
||||
"content": "Jan 30, 2024 \u2014 The synergy of low-code functionality with Python's versatility empowers data professionals to orchestrate complex transformations seamlessly.",
|
||||
"img_src": null,
|
||||
"engine": "google",
|
||||
"parsed_url": [
|
||||
"https",
|
||||
"www.matillion.com",
|
||||
"/blog/the-importance-of-python-and-its-growing-influence-on-data-productivty-a-matillion-perspective",
|
||||
"",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"template": "default.html",
|
||||
"engines": ["google"],
|
||||
"positions": [10],
|
||||
"score": 0.1,
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"title": "BeginnersGuide - Python Wiki",
|
||||
"content": "This is the program that reads Python programs and carries out their instructions; you need it before you can do any Python programming. Mac and Linux distributions may include an outdated version of Python (Python 2), but you should install an updated one (Python 3). See BeginnersGuide/Download for instructions to download the correct version ...",
|
||||
"url": "https://wiki.python.org/moin/BeginnersGuide",
|
||||
"engine": "duckduckgo",
|
||||
"parsed_url": ["https", "wiki.python.org", "/moin/BeginnersGuide", "", "", ""],
|
||||
"template": "default.html",
|
||||
"engines": ["duckduckgo"],
|
||||
"positions": [16],
|
||||
"score": 0.0625,
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"title": "Learn Python - Free Interactive Python Tutorial",
|
||||
"content": "Learn Python from scratch or improve your skills with this website that offers tutorials, exercises, tests and certification. Explore topics such as basics, data science, advanced features and more with DataCamp.",
|
||||
"url": "https://www.learnpython.org/",
|
||||
"engine": "duckduckgo",
|
||||
"parsed_url": ["https", "www.learnpython.org", "/", "", "", ""],
|
||||
"template": "default.html",
|
||||
"engines": ["duckduckgo"],
|
||||
"positions": [17],
|
||||
"score": 0.058823529411764705,
|
||||
"category": "general"
|
||||
}
|
||||
],
|
||||
"answers": [],
|
||||
"corrections": [],
|
||||
"infoboxes": [
|
||||
{
|
||||
"infobox": "Python",
|
||||
"id": "https://en.wikipedia.org/wiki/Python_(programming_language)",
|
||||
"content": "general-purpose programming language",
|
||||
"img_src": "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6f/.PY_file_recreation.png/500px-.PY_file_recreation.png",
|
||||
"urls": [
|
||||
{
|
||||
"title": "Official website",
|
||||
"url": "https://www.python.org/",
|
||||
"official": true
|
||||
},
|
||||
{
|
||||
"title": "Wikipedia (en)",
|
||||
"url": "https://en.wikipedia.org/wiki/Python_(programming_language)"
|
||||
},
|
||||
{
|
||||
"title": "Wikidata",
|
||||
"url": "http://www.wikidata.org/entity/Q28865"
|
||||
}
|
||||
],
|
||||
"attributes": [
|
||||
{
|
||||
"label": "Inception",
|
||||
"value": "Wednesday, February 20, 1991",
|
||||
"entity": "P571"
|
||||
},
|
||||
{
|
||||
"label": "Developer",
|
||||
"value": "Python Software Foundation, Guido van Rossum",
|
||||
"entity": "P178"
|
||||
},
|
||||
{
|
||||
"label": "Copyright license",
|
||||
"value": "Python Software Foundation License",
|
||||
"entity": "P275"
|
||||
},
|
||||
{
|
||||
"label": "Programmed in",
|
||||
"value": "C, Python",
|
||||
"entity": "P277"
|
||||
},
|
||||
{
|
||||
"label": "Software version identifier",
|
||||
"value": "3.12.3, 3.13.0a6",
|
||||
"entity": "P348"
|
||||
}
|
||||
],
|
||||
"engine": "wikidata",
|
||||
"engines": ["wikidata"]
|
||||
}
|
||||
],
|
||||
"suggestions": [
|
||||
"python turtle",
|
||||
"micro python tutorial",
|
||||
"python docs",
|
||||
"python compiler",
|
||||
"snapcraft python",
|
||||
"micropython vs python",
|
||||
"python online",
|
||||
"python download"
|
||||
],
|
||||
"unresponsive_engines": []
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"searchParameters": {
|
||||
"q": "apple inc",
|
||||
"gl": "us",
|
||||
"hl": "en",
|
||||
"autocorrect": true,
|
||||
"page": 1,
|
||||
"type": "search"
|
||||
},
|
||||
"knowledgeGraph": {
|
||||
"title": "Apple",
|
||||
"type": "Technology company",
|
||||
"website": "http://www.apple.com/",
|
||||
"imageUrl": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQwGQRv5TjjkycpctY66mOg_e2-npacrmjAb6_jAWhzlzkFE3OTjxyzbA&s=0",
|
||||
"description": "Apple Inc. is an American multinational technology company specializing in consumer electronics, software and online services headquartered in Cupertino, California, United States.",
|
||||
"descriptionSource": "Wikipedia",
|
||||
"descriptionLink": "https://en.wikipedia.org/wiki/Apple_Inc.",
|
||||
"attributes": {
|
||||
"Headquarters": "Cupertino, CA",
|
||||
"CEO": "Tim Cook (Aug 24, 2011–)",
|
||||
"Founded": "April 1, 1976, Los Altos, CA",
|
||||
"Sales": "1 (800) 692-7753",
|
||||
"Products": "iPhone, Apple Watch, iPad, and more",
|
||||
"Founders": "Steve Jobs, Steve Wozniak, and Ronald Wayne",
|
||||
"Subsidiaries": "Apple Store, Beats Electronics, Beddit, and more"
|
||||
}
|
||||
},
|
||||
"organic": [
|
||||
{
|
||||
"title": "Apple",
|
||||
"link": "https://www.apple.com/",
|
||||
"snippet": "Discover the innovative world of Apple and shop everything iPhone, iPad, Apple Watch, Mac, and Apple TV, plus explore accessories, entertainment, ...",
|
||||
"sitelinks": [
|
||||
{
|
||||
"title": "Support",
|
||||
"link": "https://support.apple.com/"
|
||||
},
|
||||
{
|
||||
"title": "iPhone",
|
||||
"link": "https://www.apple.com/iphone/"
|
||||
},
|
||||
{
|
||||
"title": "Apple makes business better.",
|
||||
"link": "https://www.apple.com/business/"
|
||||
},
|
||||
{
|
||||
"title": "Mac",
|
||||
"link": "https://www.apple.com/mac/"
|
||||
}
|
||||
],
|
||||
"position": 1
|
||||
},
|
||||
{
|
||||
"title": "Apple Inc. - Wikipedia",
|
||||
"link": "https://en.wikipedia.org/wiki/Apple_Inc.",
|
||||
"snippet": "Apple Inc. is an American multinational technology company specializing in consumer electronics, software and online services headquartered in Cupertino, ...",
|
||||
"attributes": {
|
||||
"Products": "AirPods; Apple Watch; iPad; iPhone; Mac",
|
||||
"Founders": "Steve Jobs; Steve Wozniak; Ronald Wayne",
|
||||
"Founded": "April 1, 1976; 46 years ago in Los Altos, California, U.S",
|
||||
"Industry": "Consumer electronics; Software services; Online services"
|
||||
},
|
||||
"sitelinks": [
|
||||
{
|
||||
"title": "History",
|
||||
"link": "https://en.wikipedia.org/wiki/History_of_Apple_Inc."
|
||||
},
|
||||
{
|
||||
"title": "Timeline of Apple Inc. products",
|
||||
"link": "https://en.wikipedia.org/wiki/Timeline_of_Apple_Inc._products"
|
||||
},
|
||||
{
|
||||
"title": "List of software by Apple Inc.",
|
||||
"link": "https://en.wikipedia.org/wiki/List_of_software_by_Apple_Inc."
|
||||
},
|
||||
{
|
||||
"title": "Apple Store",
|
||||
"link": "https://en.wikipedia.org/wiki/Apple_Store"
|
||||
}
|
||||
],
|
||||
"position": 2
|
||||
},
|
||||
{
|
||||
"title": "Apple Inc. | History, Products, Headquarters, & Facts | Britannica",
|
||||
"link": "https://www.britannica.com/topic/Apple-Inc",
|
||||
"snippet": "Apple Inc., formerly Apple Computer, Inc., American manufacturer of personal computers, smartphones, tablet computers, computer peripherals, ...",
|
||||
"date": "Aug 31, 2022",
|
||||
"attributes": {
|
||||
"Related People": "Steve Jobs Steve Wozniak Jony Ive Tim Cook Angela Ahrendts",
|
||||
"Date": "1976 - present",
|
||||
"Areas Of Involvement": "peripheral device"
|
||||
},
|
||||
"position": 3
|
||||
},
|
||||
{
|
||||
"title": "AAPL: Apple Inc Stock Price Quote - NASDAQ GS - Bloomberg.com",
|
||||
"link": "https://www.bloomberg.com/quote/AAPL:US",
|
||||
"snippet": "Stock analysis for Apple Inc (AAPL:NASDAQ GS) including stock price, stock chart, company news, key statistics, fundamentals and company profile.",
|
||||
"position": 4
|
||||
},
|
||||
{
|
||||
"title": "Apple Inc. (AAPL) Company Profile & Facts - Yahoo Finance",
|
||||
"link": "https://finance.yahoo.com/quote/AAPL/profile/",
|
||||
"snippet": "Apple Inc. designs, manufactures, and markets smartphones, personal computers, tablets, wearables, and accessories worldwide. It also sells various related ...",
|
||||
"position": 5
|
||||
},
|
||||
{
|
||||
"title": "AAPL | Apple Inc. Stock Price & News - WSJ",
|
||||
"link": "https://www.wsj.com/market-data/quotes/AAPL",
|
||||
"snippet": "Apple, Inc. engages in the design, manufacture, and sale of smartphones, personal computers, tablets, wearables and accessories, and other varieties of ...",
|
||||
"position": 6
|
||||
},
|
||||
{
|
||||
"title": "Apple Inc Company Profile - Apple Inc Overview - GlobalData",
|
||||
"link": "https://www.globaldata.com/company-profile/apple-inc/",
|
||||
"snippet": "Apple Inc (Apple) designs, manufactures, and markets smartphones, tablets, personal computers (PCs), portable and wearable devices. The company also offers ...",
|
||||
"position": 7
|
||||
},
|
||||
{
|
||||
"title": "Apple Inc (AAPL) Stock Price & News - Google Finance",
|
||||
"link": "https://www.google.com/finance/quote/AAPL:NASDAQ?hl=en",
|
||||
"snippet": "Get the latest Apple Inc (AAPL) real-time quote, historical performance, charts, and other financial information to help you make more informed trading and ...",
|
||||
"position": 8
|
||||
}
|
||||
],
|
||||
"peopleAlsoAsk": [
|
||||
{
|
||||
"question": "What does Apple Inc mean?",
|
||||
"snippet": "Apple Inc., formerly Apple Computer, Inc., American manufacturer of personal\ncomputers, smartphones, tablet computers, computer peripherals, and computer\nsoftware. It was the first successful personal computer company and the\npopularizer of the graphical user interface.\nAug 31, 2022",
|
||||
"title": "Apple Inc. | History, Products, Headquarters, & Facts | Britannica",
|
||||
"link": "https://www.britannica.com/topic/Apple-Inc"
|
||||
},
|
||||
{
|
||||
"question": "Is Apple and Apple Inc same?",
|
||||
"snippet": "Apple was founded as Apple Computer Company on April 1, 1976, by Steve Jobs,\nSteve Wozniak and Ronald Wayne to develop and sell Wozniak's Apple I personal\ncomputer. It was incorporated by Jobs and Wozniak as Apple Computer, Inc.",
|
||||
"title": "Apple Inc. - Wikipedia",
|
||||
"link": "https://en.wikipedia.org/wiki/Apple_Inc."
|
||||
},
|
||||
{
|
||||
"question": "Who owns Apple Inc?",
|
||||
"snippet": "Apple Inc. is owned by two main institutional investors (Vanguard Group and\nBlackRock, Inc). While its major individual shareholders comprise people like\nArt Levinson, Tim Cook, Bruce Sewell, Al Gore, Johny Sroujli, and others.",
|
||||
"title": "Who Owns Apple In 2022? - FourWeekMBA",
|
||||
"link": "https://fourweekmba.com/who-owns-apple/"
|
||||
},
|
||||
{
|
||||
"question": "What products does Apple Inc offer?",
|
||||
"snippet": "APPLE FOOTER\nStore.\nMac.\niPad.\niPhone.\nWatch.\nAirPods.\nTV & Home.\nAirTag.",
|
||||
"title": "More items...",
|
||||
"link": "https://www.apple.com/business/"
|
||||
}
|
||||
],
|
||||
"relatedSearches": [
|
||||
{
|
||||
"query": "Who invented the iPhone"
|
||||
},
|
||||
{
|
||||
"query": "Apple Inc competitors"
|
||||
},
|
||||
{
|
||||
"query": "Apple iPad"
|
||||
},
|
||||
{
|
||||
"query": "iPhones"
|
||||
},
|
||||
{
|
||||
"query": "Apple Inc us"
|
||||
},
|
||||
{
|
||||
"query": "Apple company history"
|
||||
},
|
||||
{
|
||||
"query": "Apple Store"
|
||||
},
|
||||
{
|
||||
"query": "Apple customer service"
|
||||
},
|
||||
{
|
||||
"query": "Apple Watch"
|
||||
},
|
||||
{
|
||||
"query": "Apple Inc Industry"
|
||||
},
|
||||
{
|
||||
"query": "Apple Inc registered address"
|
||||
},
|
||||
{
|
||||
"query": "Apple Inc Bloomberg"
|
||||
}
|
||||
]
|
||||
}
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
{
|
||||
"ads": [],
|
||||
"ads_count": 0,
|
||||
"answers": [],
|
||||
"results": [
|
||||
{
|
||||
"title": "Apple",
|
||||
"link": "https://www.apple.com/",
|
||||
"description": "Discover the innovative world of Apple and shop everything iPhone, iPad, Apple Watch, Mac, and Apple TV, plus explore accessories, entertainment, ...",
|
||||
"additional_links": [
|
||||
{
|
||||
"text": "AppleApplehttps://www.apple.com",
|
||||
"href": "https://www.apple.com/"
|
||||
}
|
||||
],
|
||||
"cite": {},
|
||||
"subdomains": [
|
||||
{
|
||||
"title": "Support",
|
||||
"link": "https://support.apple.com/",
|
||||
"description": "SupportContact - iPhone Support - Billing and Subscriptions - Apple Repair"
|
||||
},
|
||||
{
|
||||
"title": "Store",
|
||||
"link": "https://www.apple.com/store",
|
||||
"description": "StoreShop iPhone - Shop iPad - App Store - Shop Mac - ..."
|
||||
},
|
||||
{
|
||||
"title": "Mac",
|
||||
"link": "https://www.apple.com/mac/",
|
||||
"description": "MacMacBook Air - MacBook Pro - iMac - Compare Mac models - Mac mini"
|
||||
},
|
||||
{
|
||||
"title": "iPad",
|
||||
"link": "https://www.apple.com/ipad/",
|
||||
"description": "iPadShop iPad - iPad Pro - iPad Air - Compare iPad models - ..."
|
||||
},
|
||||
{
|
||||
"title": "Watch",
|
||||
"link": "https://www.apple.com/watch/",
|
||||
"description": "WatchShop Apple Watch - Series 9 - SE - Ultra 2 - Nike - Hermès - ..."
|
||||
}
|
||||
],
|
||||
"realPosition": 1
|
||||
},
|
||||
{
|
||||
"title": "Apple",
|
||||
"link": "https://www.apple.com/",
|
||||
"description": "Discover the innovative world of Apple and shop everything iPhone, iPad, Apple Watch, Mac, and Apple TV, plus explore accessories, entertainment, ...",
|
||||
"additional_links": [
|
||||
{
|
||||
"text": "AppleApplehttps://www.apple.com",
|
||||
"href": "https://www.apple.com/"
|
||||
}
|
||||
],
|
||||
"cite": {},
|
||||
"realPosition": 2
|
||||
},
|
||||
{
|
||||
"title": "Apple Inc.",
|
||||
"link": "https://en.wikipedia.org/wiki/Apple_Inc.",
|
||||
"description": "Apple Inc. (formerly Apple Computer, Inc.) is an American multinational corporation and technology company headquartered in Cupertino, California, ...",
|
||||
"additional_links": [
|
||||
{
|
||||
"text": "Apple Inc.Wikipediahttps://en.wikipedia.org › wiki › Apple_Inc",
|
||||
"href": "https://en.wikipedia.org/wiki/Apple_Inc."
|
||||
},
|
||||
{
|
||||
"text": "",
|
||||
"href": "https://en.wikipedia.org/wiki/Apple_Inc."
|
||||
},
|
||||
{
|
||||
"text": "History",
|
||||
"href": "https://en.wikipedia.org/wiki/History_of_Apple_Inc."
|
||||
},
|
||||
{
|
||||
"text": "List of Apple products",
|
||||
"href": "https://en.wikipedia.org/wiki/List_of_Apple_products"
|
||||
},
|
||||
{
|
||||
"text": "Litigation involving Apple Inc.",
|
||||
"href": "https://en.wikipedia.org/wiki/Litigation_involving_Apple_Inc."
|
||||
},
|
||||
{
|
||||
"text": "Apple Park",
|
||||
"href": "https://en.wikipedia.org/wiki/Apple_Park"
|
||||
}
|
||||
],
|
||||
"cite": {
|
||||
"domain": "https://en.wikipedia.org › wiki › Apple_Inc",
|
||||
"span": " › wiki › Apple_Inc"
|
||||
},
|
||||
"realPosition": 3
|
||||
},
|
||||
{
|
||||
"title": "Apple Inc. (AAPL) Company Profile & Facts",
|
||||
"link": "https://finance.yahoo.com/quote/AAPL/profile/",
|
||||
"description": "Apple Inc. designs, manufactures, and markets smartphones, personal computers, tablets, wearables, and accessories worldwide. The company offers iPhone, a line ...",
|
||||
"additional_links": [
|
||||
{
|
||||
"text": "Apple Inc. (AAPL) Company Profile & FactsYahoo Financehttps://finance.yahoo.com › quote › AAPL › profile",
|
||||
"href": "https://finance.yahoo.com/quote/AAPL/profile/"
|
||||
}
|
||||
],
|
||||
"cite": {
|
||||
"domain": "https://finance.yahoo.com › quote › AAPL › profile",
|
||||
"span": " › quote › AAPL › profile"
|
||||
},
|
||||
"realPosition": 4
|
||||
},
|
||||
{
|
||||
"title": "Apple Inc - Company Profile and News",
|
||||
"link": "https://www.bloomberg.com/profile/company/AAPL:US",
|
||||
"description": "Apple Inc. Apple Inc. designs, manufactures, and markets smartphones, personal computers, tablets, wearables and accessories, and sells a variety of related ...",
|
||||
"additional_links": [
|
||||
{
|
||||
"text": "Apple Inc - Company Profile and NewsBloomberghttps://www.bloomberg.com › company › AAPL:US",
|
||||
"href": "https://www.bloomberg.com/profile/company/AAPL:US"
|
||||
},
|
||||
{
|
||||
"text": "",
|
||||
"href": "https://www.bloomberg.com/profile/company/AAPL:US"
|
||||
}
|
||||
],
|
||||
"cite": {
|
||||
"domain": "https://www.bloomberg.com › company › AAPL:US",
|
||||
"span": " › company › AAPL:US"
|
||||
},
|
||||
"realPosition": 5
|
||||
},
|
||||
{
|
||||
"title": "Apple Inc. | History, Products, Headquarters, & Facts",
|
||||
"link": "https://www.britannica.com/money/Apple-Inc",
|
||||
"description": "May 22, 2024 — Apple Inc. is an American multinational technology company that revolutionized the technology sector through its innovation of computer ...",
|
||||
"additional_links": [
|
||||
{
|
||||
"text": "Apple Inc. | History, Products, Headquarters, & FactsBritannicahttps://www.britannica.com › money › Apple-Inc",
|
||||
"href": "https://www.britannica.com/money/Apple-Inc"
|
||||
},
|
||||
{
|
||||
"text": "",
|
||||
"href": "https://www.britannica.com/money/Apple-Inc"
|
||||
}
|
||||
],
|
||||
"cite": {
|
||||
"domain": "https://www.britannica.com › money › Apple-Inc",
|
||||
"span": " › money › Apple-Inc"
|
||||
},
|
||||
"realPosition": 6
|
||||
}
|
||||
],
|
||||
"shopping_ads": [],
|
||||
"places": [
|
||||
{
|
||||
"title": "Apple Inc."
|
||||
},
|
||||
{
|
||||
"title": "Apple Inc"
|
||||
},
|
||||
{
|
||||
"title": "Apple Inc"
|
||||
}
|
||||
],
|
||||
"related_searches": {
|
||||
"images": [],
|
||||
"text": [
|
||||
{
|
||||
"title": "apple inc full form",
|
||||
"link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+Inc+full+form&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhPEAE"
|
||||
},
|
||||
{
|
||||
"title": "apple company history",
|
||||
"link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+company+history&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhOEAE"
|
||||
},
|
||||
{
|
||||
"title": "apple store",
|
||||
"link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+Store&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhQEAE"
|
||||
},
|
||||
{
|
||||
"title": "apple id",
|
||||
"link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+id&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhSEAE"
|
||||
},
|
||||
{
|
||||
"title": "apple inc industry",
|
||||
"link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+Inc+industry&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhREAE"
|
||||
},
|
||||
{
|
||||
"title": "apple login",
|
||||
"link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+login&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhTEAE"
|
||||
}
|
||||
]
|
||||
},
|
||||
"image_results": [],
|
||||
"carousel": [],
|
||||
"total": 2450000000,
|
||||
"knowledge_graph": "",
|
||||
"related_questions": [
|
||||
"What does the Apple Inc do?",
|
||||
"Why did Apple change to Apple Inc?",
|
||||
"Who owns Apple Inc.?",
|
||||
"What is Apple Inc best known for?"
|
||||
],
|
||||
"carousel_count": 0,
|
||||
"ts": 2.491065263748169,
|
||||
"device_type": null
|
||||
}
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
{
|
||||
"request": {
|
||||
"success": true,
|
||||
"total_time_taken": 3.4,
|
||||
"processed_timestamp": 1714968442,
|
||||
"search_url": "http://www.google.com/search?q=mcdonalds\u0026gl=us\u0026hl=en\u0026safe=0\u0026num=10"
|
||||
},
|
||||
"search_parameters": {
|
||||
"engine": "google",
|
||||
"type": "web",
|
||||
"device": "desktop",
|
||||
"auto_location": "1",
|
||||
"google_domain": "google.com",
|
||||
"gl": "us",
|
||||
"hl": "en",
|
||||
"safe": "0",
|
||||
"news_type": "all",
|
||||
"exclude_autocorrected_results": "0",
|
||||
"images_color": "any",
|
||||
"page": "1",
|
||||
"num": "10",
|
||||
"output": "json",
|
||||
"csv_fields": "search_parameters.query,organic_results.position,organic_results.title,organic_results.url,organic_results.domain",
|
||||
"query": "mcdonalds",
|
||||
"action": "search",
|
||||
"access_key": "aac48e007e15c532bb94ffb34532a4b2",
|
||||
"error": {}
|
||||
},
|
||||
"search_information": {
|
||||
"total_results": 1170000000,
|
||||
"time_taken_displayed": 0.49,
|
||||
"detected_location": {},
|
||||
"did_you_mean": {},
|
||||
"no_results_for_original_query": false,
|
||||
"showing_results_for": {}
|
||||
},
|
||||
"organic_results": [
|
||||
{
|
||||
"position": 1,
|
||||
"title": "Our Full McDonald\u0027s Food Menu",
|
||||
"snippet": "",
|
||||
"prerender": false,
|
||||
"cached_page_url": {},
|
||||
"related_pages_url": {},
|
||||
"url": "https://www.mcdonalds.com/us/en-us/full-menu.html",
|
||||
"domain": "www.mcdonalds.com",
|
||||
"displayed_url": "https://www.mcdonalds.com \u203a en-us \u203a full-menu"
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"title": "McDonald\u0027s",
|
||||
"snippet": "McDonald\u0027s is the world\u0027s largest fast food restaurant chain, serving over 69 million customers daily in over 100 countries in more than 40,000 outlets as of\u00a0...",
|
||||
"prerender": false,
|
||||
"cached_page_url": {},
|
||||
"related_pages_url": {},
|
||||
"url": "https://en.wikipedia.org/wiki/McDonald%27s",
|
||||
"domain": "en.wikipedia.org",
|
||||
"displayed_url": "https://en.wikipedia.org \u203a wiki \u203a McDonald\u0027s"
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"title": "Restaurants Near Me: Nearby McDonald\u0027s Locations",
|
||||
"snippet": "",
|
||||
"prerender": false,
|
||||
"cached_page_url": {},
|
||||
"related_pages_url": {},
|
||||
"url": "https://www.mcdonalds.com/us/en-us/restaurant-locator.html",
|
||||
"domain": "www.mcdonalds.com",
|
||||
"displayed_url": "https://www.mcdonalds.com \u203a en-us \u203a restaurant-locator"
|
||||
},
|
||||
{
|
||||
"position": 4,
|
||||
"title": "Download the McDonald\u0027s App: Deals, Promotions \u0026 ...",
|
||||
"snippet": "Download the McDonald\u0027s app for Mobile Order \u0026 Pay, exclusive deals and coupons, menu information and special promotions.",
|
||||
"prerender": false,
|
||||
"cached_page_url": {},
|
||||
"related_pages_url": {},
|
||||
"url": "https://www.mcdonalds.com/us/en-us/download-app.html",
|
||||
"domain": "www.mcdonalds.com",
|
||||
"displayed_url": "https://www.mcdonalds.com \u203a en-us \u203a download-app"
|
||||
},
|
||||
{
|
||||
"position": 5,
|
||||
"title": "McDonald\u0027s Restaurant Careers in the US",
|
||||
"snippet": "McDonald\u0027s restaurant jobs are one-of-a-kind \u2013 just like you. Restaurants are hiring across all levels, from Crew team to Management. Apply today!",
|
||||
"prerender": false,
|
||||
"cached_page_url": {},
|
||||
"related_pages_url": {},
|
||||
"url": "https://jobs.mchire.com/",
|
||||
"domain": "jobs.mchire.com",
|
||||
"displayed_url": "https://jobs.mchire.com"
|
||||
}
|
||||
],
|
||||
"inline_images": [
|
||||
{
|
||||
"image_url": "https://serpstack-assets.apilayer.net/2418910010831954152.png",
|
||||
"title": ""
|
||||
}
|
||||
],
|
||||
"local_results": [
|
||||
{
|
||||
"position": 1,
|
||||
"title": "McDonald\u0027s",
|
||||
"coordinates": {
|
||||
"latitude": 0,
|
||||
"longitude": 0
|
||||
},
|
||||
"address": "",
|
||||
"rating": 0,
|
||||
"reviews": 0,
|
||||
"type": "",
|
||||
"price": {},
|
||||
"url": 0
|
||||
},
|
||||
{
|
||||
"position": 2,
|
||||
"title": "McDonald\u0027s",
|
||||
"coordinates": {
|
||||
"latitude": 0,
|
||||
"longitude": 0
|
||||
},
|
||||
"address": "",
|
||||
"rating": 0,
|
||||
"reviews": 0,
|
||||
"type": "",
|
||||
"price": {},
|
||||
"url": 0
|
||||
},
|
||||
{
|
||||
"position": 3,
|
||||
"title": "McDonald\u0027s",
|
||||
"coordinates": {
|
||||
"latitude": 0,
|
||||
"longitude": 0
|
||||
},
|
||||
"address": "",
|
||||
"rating": 0,
|
||||
"reviews": 0,
|
||||
"type": "",
|
||||
"price": {},
|
||||
"url": 0
|
||||
}
|
||||
],
|
||||
"top_stories": [
|
||||
{
|
||||
"block_position": 1,
|
||||
"title": "Menu nutrition",
|
||||
"url": "/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=mcdonald%27s+double+quarter+pounder+with+cheese\u0026stick=H4sIAAAAAAAAAONgFuLUz9U3ME-vLDBX4tVP1zc0TCsuNE0ytjTTUs5OttJPy89P0c9NzSuNLyjKL8tMSS2yAvNS80qKMlOLF7Hq5ian5Ocl5qSoFyuk5Jcm5aQqFJYmFpWkFikU5JfmATUolGeWZCgkZ6SmFqcCAM4ilJtxAAAA\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4Qri56BAh0EAM",
|
||||
"source": "",
|
||||
"uploaded": "",
|
||||
"uploaded_utc": "2024-05-06T04:07:22.082Z"
|
||||
},
|
||||
{
|
||||
"block_position": 2,
|
||||
"title": "Profiles",
|
||||
"url": "https://www.instagram.com/McDonalds",
|
||||
"source": "",
|
||||
"uploaded": "",
|
||||
"uploaded_utc": "2024-05-06T04:07:22.082Z"
|
||||
},
|
||||
{
|
||||
"block_position": 3,
|
||||
"title": "People also search for",
|
||||
"url": "/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026si=ACC90nzx_D3_zUKRnpAjmO0UBLNxnt7EyN4YYdru6U3bxLI-L5Wg8IL2sxPFxxcDEhVbocy-LJPZIvZySijw0ho2hfZ-KtV-sSEEJ9lw7JuEkXHDnRK5y4Dm8aqbiLwugbLbslwjG3hO_gpDTFZK2VoUGZPy2nrmOBCy0G3PoOfoiEtct2GSZlUz0uufG-xP8emtNzQKQpvjkAm5Zmi57iVZueiD62upz7-x2N3dAbwtm6FkInAPRw1yR91zuT7F3lEaPblTW3LaRwCDC0bvaRCh9x4N9zHgY1OOQa_rzts2jf5WpXcuw4Y%3D\u0026q=Burger+King\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4Qs9oBKAB6BAhzEAI",
|
||||
"source": "",
|
||||
"uploaded": "",
|
||||
"uploaded_utc": "2024-05-06T04:07:22.082Z"
|
||||
}
|
||||
],
|
||||
"related_questions": [
|
||||
{
|
||||
"question": "What\u0027s a number 7 at McDonald\u0027s?What\u0027s a number 7 at McDonald\u0027s?What\u0027s a number 7 at McDonald\u0027s?",
|
||||
"answer": "",
|
||||
"title": "",
|
||||
"displayed_url": ""
|
||||
},
|
||||
{
|
||||
"question": "Why is McDonald\u0027s changing their name?Why is McDonald\u0027s changing their name?Why is McDonald\u0027s changing their name?",
|
||||
"answer": "",
|
||||
"title": "",
|
||||
"displayed_url": ""
|
||||
},
|
||||
{
|
||||
"question": "What is the oldest still running Mcdonalds?What is the oldest still running Mcdonalds?What is the oldest still running Mcdonalds?",
|
||||
"answer": "",
|
||||
"title": "",
|
||||
"displayed_url": ""
|
||||
},
|
||||
{
|
||||
"question": "Why is McDonald\u0027s now WcDonald\u0027s?Why is McDonald\u0027s now WcDonald\u0027s?Why is McDonald\u0027s now WcDonald\u0027s?",
|
||||
"answer": "",
|
||||
"title": "",
|
||||
"displayed_url": ""
|
||||
}
|
||||
],
|
||||
"knowledge_graph": {
|
||||
"title": "",
|
||||
"type": "Fast-food restaurant company",
|
||||
"image_urls": ["https://serpstack-assets.apilayer.net/2418910010831954152.png"],
|
||||
"description": "McDonald\u0027s Corporation is an American multinational fast food chain, founded in 1940 as a restaurant operated by Richard and Maurice McDonald, in San Bernardino, California, United States.",
|
||||
"source": {
|
||||
"name": "Wikipedia",
|
||||
"url": "https://en.wikipedia.org/wiki/McDonald\u0027s"
|
||||
},
|
||||
"people_also_search_for": [],
|
||||
"known_attributes": [
|
||||
{
|
||||
"attribute": "kc:/business/business_operation:founder",
|
||||
"link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Ray+Kroc\u0026si=ACC90nzx_D3_zUKRnpAjmO0UBLNxnt7EyN4YYdru6U3bxLI-LxARWRdbk5SkoY2sDn5Qq7yOmqYGei6qZ7sfJhsjZXBPgjMlLbS7824rpJOm69GzqVWMdoNIZiFX2T4A2td14sZOn4a1BexZLtZXHU7NZdF6VsWbGMVuiSYtXdev7uaUjEJKumiwlqTAATTebOriYTEBuSzC\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECHgQAg",
|
||||
"name": "Founder: ",
|
||||
"value": "Ray Kroc"
|
||||
},
|
||||
{
|
||||
"attribute": "kc:/organization/organization:ceo",
|
||||
"link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Chris+Kempczinski\u0026si=ACC90nwLLwns5sISZcdzuISy7t-NHozt8Cbt6G3WNQfC9ekAgKFbjdEFCDgxLbt57EDZGosYDGiZuq1AcBhA6IhTOSZxfVSySuGQ3VDwmmTA7Z93n3K3596jAuZH9VVv5h8PyvKJSuGuSsQWviJTl3eKj2UL1ZIWuDgkjyVMnC47rN7j0G9PlHRCCLdQF7VDQ1gubTiC4onXqLRBTbwAj6a--PD6Jv_NoA%3D%3D\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECHUQAg",
|
||||
"name": "CEO: ",
|
||||
"value": "Chris Kempczinski (Nov 1, 2019\u2013)"
|
||||
},
|
||||
{
|
||||
"attribute": "kc:/business/employer:revenue",
|
||||
"link": "",
|
||||
"name": "Revenue: ",
|
||||
"value": "25.49\u00a0billion USD (2023)"
|
||||
},
|
||||
{
|
||||
"attribute": "kc:/organization/organization:founded",
|
||||
"link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Des+Plaines\u0026si=ACC90nyvvWro6QmnyY1IfSdgk5wwjB1r8BGd_IWRjXqmKPQqm_yqLtI_DBi5PXGOtg_Z3qrzzEP6mcih1nN7h5A7v6OefnEJiC7a8dBR-v9LxlRubfyR6vlMr3fZ3TmVKWwz9FRpvZb1eYNt-RM7KIDKQlwGEIgINvzhxjUrv6uxSmceduzxd8W7Pkz71XGwxF0F8OlSzHlx\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECG4QAg",
|
||||
"name": "Founded: ",
|
||||
"value": "April 15, 1955, Des Plaines, IL"
|
||||
},
|
||||
{
|
||||
"attribute": "kc:/organization/organization:headquarters",
|
||||
"link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Chicago\u0026si=ACC90nyvvWro6QmnyY1IfSdgk5wwjB1r8BGd_IWRjXqmKPQqm-46AEJ_kJbUIEvsvEEZqteiYJvXVXs2ScRNDvFFpjfeAaW3dxtpTGCgcsf5RMdi6IdzOdtjJMN3ZaFwqZOmdi7tC6r0Mh1O9bnP3HrVDB9hH02m7aA6f70dCAfTdpOFnGxDU6wVMAI5MxWBE3wTugtUDOK-\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECHYQAg",
|
||||
"name": "Headquarters: ",
|
||||
"value": "Chicago, IL"
|
||||
},
|
||||
{
|
||||
"attribute": "kc:/organization/organization:president",
|
||||
"link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Chris+Kempczinski\u0026si=ACC90nwLLwns5sISZcdzuISy7t-NHozt8Cbt6G3WNQfC9ekAgKFbjdEFCDgxLbt57EDZGosYDGiZuq1AcBhA6IhTOSZxfVSySuGQ3VDwmmTA7Z93n3K3596jAuZH9VVv5h8PyvKJSuGuSsQWviJTl3eKj2UL1ZIWuDgkjyVMnC47rN7j0G9PlHRCCLdQF7VDQ1gubTiC4onXqLRBTbwAj6a--PD6Jv_NoA%3D%3D\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECHEQAg",
|
||||
"name": "President: ",
|
||||
"value": "Chris Kempczinski"
|
||||
}
|
||||
],
|
||||
"website": "https://www.mcdonalds.com/us/en-us.html",
|
||||
"profiles": [
|
||||
{
|
||||
"name": "Instagram",
|
||||
"url": "https://www.instagram.com/McDonalds"
|
||||
},
|
||||
{
|
||||
"name": "X (Twitter)",
|
||||
"url": "https://twitter.com/McDonalds"
|
||||
},
|
||||
{
|
||||
"name": "Facebook",
|
||||
"url": "https://www.facebook.com/McDonaldsUS"
|
||||
},
|
||||
{
|
||||
"name": "YouTube",
|
||||
"url": "https://www.youtube.com/user/McDonaldsUS"
|
||||
},
|
||||
{
|
||||
"name": "Pinterest",
|
||||
"url": "https://www.pinterest.com/mcdonalds"
|
||||
}
|
||||
],
|
||||
"founded": "April 15, 1955, Des Plaines, IL",
|
||||
"headquarters": "Chicago, IL",
|
||||
"founders": [
|
||||
{
|
||||
"name": "Ray Kroc",
|
||||
"link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Ray+Kroc\u0026si=ACC90nzx_D3_zUKRnpAjmO0UBLNxnt7EyN4YYdru6U3bxLI-LxARWRdbk5SkoY2sDn5Qq7yOmqYGei6qZ7sfJhsjZXBPgjMlLbS7824rpJOm69GzqVWMdoNIZiFX2T4A2td14sZOn4a1BexZLtZXHU7NZdF6VsWbGMVuiSYtXdev7uaUjEJKumiwlqTAATTebOriYTEBuSzC\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECHgQAg"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+31
-60
@@ -2,7 +2,7 @@ import os
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from typing import List
|
||||
from typing import List, Union
|
||||
|
||||
from apps.ollama.main import (
|
||||
generate_ollama_embeddings,
|
||||
@@ -19,8 +19,9 @@ from langchain.retrievers import (
|
||||
)
|
||||
|
||||
from typing import Optional
|
||||
from config import SRC_LOG_LEVELS, CHROMA_CLIENT
|
||||
|
||||
from utils.misc import get_last_user_message, add_or_update_system_message
|
||||
from config import SRC_LOG_LEVELS, CHROMA_CLIENT
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||
@@ -198,6 +199,7 @@ def get_embedding_function(
|
||||
embedding_function,
|
||||
openai_key,
|
||||
openai_url,
|
||||
batch_size,
|
||||
):
|
||||
if embedding_engine == "":
|
||||
return lambda query: embedding_function.encode(query).tolist()
|
||||
@@ -221,17 +223,22 @@ def get_embedding_function(
|
||||
|
||||
def generate_multiple(query, f):
|
||||
if isinstance(query, list):
|
||||
return [f(q) for q in query]
|
||||
if embedding_engine == "openai":
|
||||
embeddings = []
|
||||
for i in range(0, len(query), batch_size):
|
||||
embeddings.extend(f(query[i : i + batch_size]))
|
||||
return embeddings
|
||||
else:
|
||||
return [f(q) for q in query]
|
||||
else:
|
||||
return f(query)
|
||||
|
||||
return lambda query: generate_multiple(query, func)
|
||||
|
||||
|
||||
def rag_messages(
|
||||
def get_rag_context(
|
||||
docs,
|
||||
messages,
|
||||
template,
|
||||
embedding_function,
|
||||
k,
|
||||
reranking_function,
|
||||
@@ -239,31 +246,7 @@ def rag_messages(
|
||||
hybrid_search,
|
||||
):
|
||||
log.debug(f"docs: {docs} {messages} {embedding_function} {reranking_function}")
|
||||
|
||||
last_user_message_idx = None
|
||||
for i in range(len(messages) - 1, -1, -1):
|
||||
if messages[i]["role"] == "user":
|
||||
last_user_message_idx = i
|
||||
break
|
||||
|
||||
user_message = messages[last_user_message_idx]
|
||||
|
||||
if isinstance(user_message["content"], list):
|
||||
# Handle list content input
|
||||
content_type = "list"
|
||||
query = ""
|
||||
for content_item in user_message["content"]:
|
||||
if content_item["type"] == "text":
|
||||
query = content_item["text"]
|
||||
break
|
||||
elif isinstance(user_message["content"], str):
|
||||
# Handle text content input
|
||||
content_type = "text"
|
||||
query = user_message["content"]
|
||||
else:
|
||||
# Fallback in case the input does not match expected types
|
||||
content_type = None
|
||||
query = ""
|
||||
query = get_last_user_message(messages)
|
||||
|
||||
extracted_collections = []
|
||||
relevant_contexts = []
|
||||
@@ -334,33 +317,7 @@ def rag_messages(
|
||||
|
||||
context_string = context_string.strip()
|
||||
|
||||
ra_content = rag_template(
|
||||
template=template,
|
||||
context=context_string,
|
||||
query=query,
|
||||
)
|
||||
|
||||
log.debug(f"ra_content: {ra_content}")
|
||||
|
||||
if content_type == "list":
|
||||
new_content = []
|
||||
for content_item in user_message["content"]:
|
||||
if content_item["type"] == "text":
|
||||
# Update the text item's content with ra_content
|
||||
new_content.append({"type": "text", "text": ra_content})
|
||||
else:
|
||||
# Keep other types of content as they are
|
||||
new_content.append(content_item)
|
||||
new_user_message = {**user_message, "content": new_content}
|
||||
else:
|
||||
new_user_message = {
|
||||
**user_message,
|
||||
"content": ra_content,
|
||||
}
|
||||
|
||||
messages[last_user_message_idx] = new_user_message
|
||||
|
||||
return messages, citations
|
||||
return context_string, citations
|
||||
|
||||
|
||||
def get_model_path(model: str, update_model: bool = False):
|
||||
@@ -402,8 +359,22 @@ def get_model_path(model: str, update_model: bool = False):
|
||||
|
||||
|
||||
def generate_openai_embeddings(
|
||||
model: str, text: str, key: str, url: str = "https://api.openai.com/v1"
|
||||
model: str,
|
||||
text: Union[str, list[str]],
|
||||
key: str,
|
||||
url: str = "https://api.openai.com/v1",
|
||||
):
|
||||
if isinstance(text, list):
|
||||
embeddings = generate_openai_batch_embeddings(model, text, key, url)
|
||||
else:
|
||||
embeddings = generate_openai_batch_embeddings(model, [text], key, url)
|
||||
|
||||
return embeddings[0] if isinstance(text, str) else embeddings
|
||||
|
||||
|
||||
def generate_openai_batch_embeddings(
|
||||
model: str, texts: list[str], key: str, url: str = "https://api.openai.com/v1"
|
||||
) -> Optional[list[list[float]]]:
|
||||
try:
|
||||
r = requests.post(
|
||||
f"{url}/embeddings",
|
||||
@@ -411,12 +382,12 @@ def generate_openai_embeddings(
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {key}",
|
||||
},
|
||||
json={"input": text, "model": model},
|
||||
json={"input": texts, "model": model},
|
||||
)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
if "data" in data:
|
||||
return data["data"][0]["embedding"]
|
||||
return [elem["embedding"] for elem in data["data"]]
|
||||
else:
|
||||
raise "Something went wrong :/"
|
||||
except Exception as e:
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import socketio
|
||||
import asyncio
|
||||
|
||||
|
||||
from apps.webui.models.users import Users
|
||||
from utils.utils import decode_token
|
||||
|
||||
sio = socketio.AsyncServer(cors_allowed_origins=[], async_mode="asgi")
|
||||
app = socketio.ASGIApp(sio, socketio_path="/ws/socket.io")
|
||||
|
||||
# Dictionary to maintain the user pool
|
||||
|
||||
SESSION_POOL = {}
|
||||
USER_POOL = {}
|
||||
USAGE_POOL = {}
|
||||
# Timeout duration in seconds
|
||||
TIMEOUT_DURATION = 3
|
||||
|
||||
|
||||
@sio.event
|
||||
async def connect(sid, environ, auth):
|
||||
user = None
|
||||
if auth and "token" in auth:
|
||||
data = decode_token(auth["token"])
|
||||
|
||||
if data is not None and "id" in data:
|
||||
user = Users.get_user_by_id(data["id"])
|
||||
|
||||
if user:
|
||||
SESSION_POOL[sid] = user.id
|
||||
if user.id in USER_POOL:
|
||||
USER_POOL[user.id].append(sid)
|
||||
else:
|
||||
USER_POOL[user.id] = [sid]
|
||||
|
||||
print(f"user {user.name}({user.id}) connected with session ID {sid}")
|
||||
|
||||
await sio.emit("user-count", {"count": len(set(USER_POOL))})
|
||||
await sio.emit("usage", {"models": get_models_in_use()})
|
||||
|
||||
|
||||
@sio.on("user-join")
|
||||
async def user_join(sid, data):
|
||||
print("user-join", sid, data)
|
||||
|
||||
auth = data["auth"] if "auth" in data else None
|
||||
|
||||
if auth and "token" in auth:
|
||||
data = decode_token(auth["token"])
|
||||
|
||||
if data is not None and "id" in data:
|
||||
user = Users.get_user_by_id(data["id"])
|
||||
|
||||
if user:
|
||||
|
||||
SESSION_POOL[sid] = user.id
|
||||
if user.id in USER_POOL:
|
||||
USER_POOL[user.id].append(sid)
|
||||
else:
|
||||
USER_POOL[user.id] = [sid]
|
||||
|
||||
print(f"user {user.name}({user.id}) connected with session ID {sid}")
|
||||
|
||||
await sio.emit("user-count", {"count": len(set(USER_POOL))})
|
||||
|
||||
|
||||
@sio.on("user-count")
|
||||
async def user_count(sid):
|
||||
await sio.emit("user-count", {"count": len(set(USER_POOL))})
|
||||
|
||||
|
||||
def get_models_in_use():
|
||||
# Aggregate all models in use
|
||||
models_in_use = []
|
||||
for model_id, data in USAGE_POOL.items():
|
||||
models_in_use.append(model_id)
|
||||
|
||||
return models_in_use
|
||||
|
||||
|
||||
@sio.on("usage")
|
||||
async def usage(sid, data):
|
||||
|
||||
model_id = data["model"]
|
||||
|
||||
# Cancel previous callback if there is one
|
||||
if model_id in USAGE_POOL:
|
||||
USAGE_POOL[model_id]["callback"].cancel()
|
||||
|
||||
# Store the new usage data and task
|
||||
|
||||
if model_id in USAGE_POOL:
|
||||
USAGE_POOL[model_id]["sids"].append(sid)
|
||||
USAGE_POOL[model_id]["sids"] = list(set(USAGE_POOL[model_id]["sids"]))
|
||||
|
||||
else:
|
||||
USAGE_POOL[model_id] = {"sids": [sid]}
|
||||
|
||||
# Schedule a task to remove the usage data after TIMEOUT_DURATION
|
||||
USAGE_POOL[model_id]["callback"] = asyncio.create_task(
|
||||
remove_after_timeout(sid, model_id)
|
||||
)
|
||||
|
||||
# Broadcast the usage data to all clients
|
||||
await sio.emit("usage", {"models": get_models_in_use()})
|
||||
|
||||
|
||||
async def remove_after_timeout(sid, model_id):
|
||||
try:
|
||||
await asyncio.sleep(TIMEOUT_DURATION)
|
||||
if model_id in USAGE_POOL:
|
||||
print(USAGE_POOL[model_id]["sids"])
|
||||
USAGE_POOL[model_id]["sids"].remove(sid)
|
||||
USAGE_POOL[model_id]["sids"] = list(set(USAGE_POOL[model_id]["sids"]))
|
||||
|
||||
if len(USAGE_POOL[model_id]["sids"]) == 0:
|
||||
del USAGE_POOL[model_id]
|
||||
|
||||
# Broadcast the usage data to all clients
|
||||
await sio.emit("usage", {"models": get_models_in_use()})
|
||||
except asyncio.CancelledError:
|
||||
# Task was cancelled due to new 'usage' event
|
||||
pass
|
||||
|
||||
|
||||
@sio.event
|
||||
async def disconnect(sid):
|
||||
if sid in SESSION_POOL:
|
||||
user_id = SESSION_POOL[sid]
|
||||
del SESSION_POOL[sid]
|
||||
|
||||
USER_POOL[user_id].remove(sid)
|
||||
|
||||
if len(USER_POOL[user_id]) == 0:
|
||||
del USER_POOL[user_id]
|
||||
|
||||
await sio.emit("user-count", {"count": len(USER_POOL)})
|
||||
else:
|
||||
print(f"Unknown session ID {sid} disconnected")
|
||||
@@ -1,136 +0,0 @@
|
||||
from pydantic import BaseModel
|
||||
from peewee import *
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
from typing import List, Union, Optional
|
||||
import time
|
||||
|
||||
from utils.utils import decode_token
|
||||
from utils.misc import get_gravatar_url
|
||||
|
||||
from apps.web.internal.db import DB
|
||||
|
||||
import json
|
||||
|
||||
####################
|
||||
# Modelfile DB Schema
|
||||
####################
|
||||
|
||||
|
||||
class Modelfile(Model):
|
||||
tag_name = CharField(unique=True)
|
||||
user_id = CharField()
|
||||
modelfile = TextField()
|
||||
timestamp = BigIntegerField()
|
||||
|
||||
class Meta:
|
||||
database = DB
|
||||
|
||||
|
||||
class ModelfileModel(BaseModel):
|
||||
tag_name: str
|
||||
user_id: str
|
||||
modelfile: str
|
||||
timestamp: int # timestamp in epoch
|
||||
|
||||
|
||||
####################
|
||||
# Forms
|
||||
####################
|
||||
|
||||
|
||||
class ModelfileForm(BaseModel):
|
||||
modelfile: dict
|
||||
|
||||
|
||||
class ModelfileTagNameForm(BaseModel):
|
||||
tag_name: str
|
||||
|
||||
|
||||
class ModelfileUpdateForm(ModelfileForm, ModelfileTagNameForm):
|
||||
pass
|
||||
|
||||
|
||||
class ModelfileResponse(BaseModel):
|
||||
tag_name: str
|
||||
user_id: str
|
||||
modelfile: dict
|
||||
timestamp: int # timestamp in epoch
|
||||
|
||||
|
||||
class ModelfilesTable:
|
||||
|
||||
def __init__(self, db):
|
||||
self.db = db
|
||||
self.db.create_tables([Modelfile])
|
||||
|
||||
def insert_new_modelfile(
|
||||
self, user_id: str, form_data: ModelfileForm
|
||||
) -> Optional[ModelfileModel]:
|
||||
if "tagName" in form_data.modelfile:
|
||||
modelfile = ModelfileModel(
|
||||
**{
|
||||
"user_id": user_id,
|
||||
"tag_name": form_data.modelfile["tagName"],
|
||||
"modelfile": json.dumps(form_data.modelfile),
|
||||
"timestamp": int(time.time()),
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
result = Modelfile.create(**modelfile.model_dump())
|
||||
if result:
|
||||
return modelfile
|
||||
else:
|
||||
return None
|
||||
except:
|
||||
return None
|
||||
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_modelfile_by_tag_name(self, tag_name: str) -> Optional[ModelfileModel]:
|
||||
try:
|
||||
modelfile = Modelfile.get(Modelfile.tag_name == tag_name)
|
||||
return ModelfileModel(**model_to_dict(modelfile))
|
||||
except:
|
||||
return None
|
||||
|
||||
def get_modelfiles(self, skip: int = 0, limit: int = 50) -> List[ModelfileResponse]:
|
||||
return [
|
||||
ModelfileResponse(
|
||||
**{
|
||||
**model_to_dict(modelfile),
|
||||
"modelfile": json.loads(modelfile.modelfile),
|
||||
}
|
||||
)
|
||||
for modelfile in Modelfile.select()
|
||||
# .limit(limit).offset(skip)
|
||||
]
|
||||
|
||||
def update_modelfile_by_tag_name(
|
||||
self, tag_name: str, modelfile: dict
|
||||
) -> Optional[ModelfileModel]:
|
||||
try:
|
||||
query = Modelfile.update(
|
||||
modelfile=json.dumps(modelfile),
|
||||
timestamp=int(time.time()),
|
||||
).where(Modelfile.tag_name == tag_name)
|
||||
|
||||
query.execute()
|
||||
|
||||
modelfile = Modelfile.get(Modelfile.tag_name == tag_name)
|
||||
return ModelfileModel(**model_to_dict(modelfile))
|
||||
except:
|
||||
return None
|
||||
|
||||
def delete_modelfile_by_tag_name(self, tag_name: str) -> bool:
|
||||
try:
|
||||
query = Modelfile.delete().where((Modelfile.tag_name == tag_name))
|
||||
query.execute() # Remove the rows, return number of rows removed.
|
||||
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
Modelfiles = ModelfilesTable(DB)
|
||||
@@ -1,124 +0,0 @@
|
||||
from fastapi import Depends, FastAPI, HTTPException, status
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Union, Optional
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
import json
|
||||
from apps.web.models.modelfiles import (
|
||||
Modelfiles,
|
||||
ModelfileForm,
|
||||
ModelfileTagNameForm,
|
||||
ModelfileUpdateForm,
|
||||
ModelfileResponse,
|
||||
)
|
||||
|
||||
from utils.utils import get_current_user, get_admin_user
|
||||
from constants import ERROR_MESSAGES
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
############################
|
||||
# GetModelfiles
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/", response_model=List[ModelfileResponse])
|
||||
async def get_modelfiles(
|
||||
skip: int = 0, limit: int = 50, user=Depends(get_current_user)
|
||||
):
|
||||
return Modelfiles.get_modelfiles(skip, limit)
|
||||
|
||||
|
||||
############################
|
||||
# CreateNewModelfile
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/create", response_model=Optional[ModelfileResponse])
|
||||
async def create_new_modelfile(form_data: ModelfileForm, user=Depends(get_admin_user)):
|
||||
modelfile = Modelfiles.insert_new_modelfile(user.id, form_data)
|
||||
|
||||
if modelfile:
|
||||
return ModelfileResponse(
|
||||
**{
|
||||
**modelfile.model_dump(),
|
||||
"modelfile": json.loads(modelfile.modelfile),
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.DEFAULT(),
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# GetModelfileByTagName
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/", response_model=Optional[ModelfileResponse])
|
||||
async def get_modelfile_by_tag_name(
|
||||
form_data: ModelfileTagNameForm, user=Depends(get_current_user)
|
||||
):
|
||||
modelfile = Modelfiles.get_modelfile_by_tag_name(form_data.tag_name)
|
||||
|
||||
if modelfile:
|
||||
return ModelfileResponse(
|
||||
**{
|
||||
**modelfile.model_dump(),
|
||||
"modelfile": json.loads(modelfile.modelfile),
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# UpdateModelfileByTagName
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/update", response_model=Optional[ModelfileResponse])
|
||||
async def update_modelfile_by_tag_name(
|
||||
form_data: ModelfileUpdateForm, user=Depends(get_admin_user)
|
||||
):
|
||||
modelfile = Modelfiles.get_modelfile_by_tag_name(form_data.tag_name)
|
||||
if modelfile:
|
||||
updated_modelfile = {
|
||||
**json.loads(modelfile.modelfile),
|
||||
**form_data.modelfile,
|
||||
}
|
||||
|
||||
modelfile = Modelfiles.update_modelfile_by_tag_name(
|
||||
form_data.tag_name, updated_modelfile
|
||||
)
|
||||
|
||||
return ModelfileResponse(
|
||||
**{
|
||||
**modelfile.model_dump(),
|
||||
"modelfile": json.loads(modelfile.modelfile),
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# DeleteModelfileByTagName
|
||||
############################
|
||||
|
||||
|
||||
@router.delete("/delete", response_model=bool)
|
||||
async def delete_modelfile_by_tag_name(
|
||||
form_data: ModelfileTagNameForm, user=Depends(get_admin_user)
|
||||
):
|
||||
result = Modelfiles.delete_modelfile_by_tag_name(form_data.tag_name)
|
||||
return result
|
||||
@@ -1,13 +1,25 @@
|
||||
import json
|
||||
|
||||
from peewee import *
|
||||
from peewee_migrate import Router
|
||||
from playhouse.db_url import connect
|
||||
from config import SRC_LOG_LEVELS, DATA_DIR, DATABASE_URL
|
||||
from config import SRC_LOG_LEVELS, DATA_DIR, DATABASE_URL, BACKEND_DIR
|
||||
import os
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["DB"])
|
||||
|
||||
|
||||
class JSONField(TextField):
|
||||
def db_value(self, value):
|
||||
return json.dumps(value)
|
||||
|
||||
def python_value(self, value):
|
||||
if value is not None:
|
||||
return json.loads(value)
|
||||
|
||||
|
||||
# Check if the file exists
|
||||
if os.path.exists(f"{DATA_DIR}/ollama.db"):
|
||||
# Rename the file
|
||||
@@ -18,6 +30,10 @@ else:
|
||||
|
||||
DB = connect(DATABASE_URL)
|
||||
log.info(f"Connected to a {DB.__class__.__name__} database.")
|
||||
router = Router(DB, migrate_dir="apps/web/internal/migrations", logger=log)
|
||||
router = Router(
|
||||
DB,
|
||||
migrate_dir=BACKEND_DIR / "apps" / "webui" / "internal" / "migrations",
|
||||
logger=log,
|
||||
)
|
||||
router.run()
|
||||
DB.connect(reuse_if_open=True)
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Peewee migrations -- 009_add_models.py.
|
||||
|
||||
Some examples (model - class or model name)::
|
||||
|
||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||
> Model = migrator.ModelClass # Return model in current state by name
|
||||
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
> migrator.add_constraint(model, name, sql)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.drop_constraints(model, *constraints)
|
||||
|
||||
"""
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
import peewee as pw
|
||||
from peewee_migrate import Migrator
|
||||
|
||||
|
||||
with suppress(ImportError):
|
||||
import playhouse.postgres_ext as pw_pext
|
||||
|
||||
|
||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your migrations here."""
|
||||
|
||||
@migrator.create_model
|
||||
class Model(pw.Model):
|
||||
id = pw.TextField(unique=True)
|
||||
user_id = pw.TextField()
|
||||
base_model_id = pw.TextField(null=True)
|
||||
|
||||
name = pw.TextField()
|
||||
|
||||
meta = pw.TextField()
|
||||
params = pw.TextField()
|
||||
|
||||
created_at = pw.BigIntegerField(null=False)
|
||||
updated_at = pw.BigIntegerField(null=False)
|
||||
|
||||
class Meta:
|
||||
table_name = "model"
|
||||
|
||||
|
||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your rollback migrations here."""
|
||||
|
||||
migrator.remove_model("model")
|
||||
@@ -0,0 +1,130 @@
|
||||
"""Peewee migrations -- 009_add_models.py.
|
||||
|
||||
Some examples (model - class or model name)::
|
||||
|
||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||
> Model = migrator.ModelClass # Return model in current state by name
|
||||
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
> migrator.add_constraint(model, name, sql)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.drop_constraints(model, *constraints)
|
||||
|
||||
"""
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
import peewee as pw
|
||||
from peewee_migrate import Migrator
|
||||
import json
|
||||
|
||||
from utils.misc import parse_ollama_modelfile
|
||||
|
||||
with suppress(ImportError):
|
||||
import playhouse.postgres_ext as pw_pext
|
||||
|
||||
|
||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your migrations here."""
|
||||
|
||||
# Fetch data from 'modelfile' table and insert into 'model' table
|
||||
migrate_modelfile_to_model(migrator, database)
|
||||
# Drop the 'modelfile' table
|
||||
migrator.remove_model("modelfile")
|
||||
|
||||
|
||||
def migrate_modelfile_to_model(migrator: Migrator, database: pw.Database):
|
||||
ModelFile = migrator.orm["modelfile"]
|
||||
Model = migrator.orm["model"]
|
||||
|
||||
modelfiles = ModelFile.select()
|
||||
|
||||
for modelfile in modelfiles:
|
||||
# Extract and transform data in Python
|
||||
|
||||
modelfile.modelfile = json.loads(modelfile.modelfile)
|
||||
meta = json.dumps(
|
||||
{
|
||||
"description": modelfile.modelfile.get("desc"),
|
||||
"profile_image_url": modelfile.modelfile.get("imageUrl"),
|
||||
"ollama": {"modelfile": modelfile.modelfile.get("content")},
|
||||
"suggestion_prompts": modelfile.modelfile.get("suggestionPrompts"),
|
||||
"categories": modelfile.modelfile.get("categories"),
|
||||
"user": {**modelfile.modelfile.get("user", {}), "community": True},
|
||||
}
|
||||
)
|
||||
|
||||
info = parse_ollama_modelfile(modelfile.modelfile.get("content"))
|
||||
|
||||
# Insert the processed data into the 'model' table
|
||||
Model.create(
|
||||
id=f"ollama-{modelfile.tag_name}",
|
||||
user_id=modelfile.user_id,
|
||||
base_model_id=info.get("base_model_id"),
|
||||
name=modelfile.modelfile.get("title"),
|
||||
meta=meta,
|
||||
params=json.dumps(info.get("params", {})),
|
||||
created_at=modelfile.timestamp,
|
||||
updated_at=modelfile.timestamp,
|
||||
)
|
||||
|
||||
|
||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your rollback migrations here."""
|
||||
|
||||
recreate_modelfile_table(migrator, database)
|
||||
move_data_back_to_modelfile(migrator, database)
|
||||
migrator.remove_model("model")
|
||||
|
||||
|
||||
def recreate_modelfile_table(migrator: Migrator, database: pw.Database):
|
||||
query = """
|
||||
CREATE TABLE IF NOT EXISTS modelfile (
|
||||
user_id TEXT,
|
||||
tag_name TEXT,
|
||||
modelfile JSON,
|
||||
timestamp BIGINT
|
||||
)
|
||||
"""
|
||||
migrator.sql(query)
|
||||
|
||||
|
||||
def move_data_back_to_modelfile(migrator: Migrator, database: pw.Database):
|
||||
Model = migrator.orm["model"]
|
||||
Modelfile = migrator.orm["modelfile"]
|
||||
|
||||
models = Model.select()
|
||||
|
||||
for model in models:
|
||||
# Extract and transform data in Python
|
||||
meta = json.loads(model.meta)
|
||||
|
||||
modelfile_data = {
|
||||
"title": model.name,
|
||||
"desc": meta.get("description"),
|
||||
"imageUrl": meta.get("profile_image_url"),
|
||||
"content": meta.get("ollama", {}).get("modelfile"),
|
||||
"suggestionPrompts": meta.get("suggestion_prompts"),
|
||||
"categories": meta.get("categories"),
|
||||
"user": {k: v for k, v in meta.get("user", {}).items() if k != "community"},
|
||||
}
|
||||
|
||||
# Insert the processed data back into the 'modelfile' table
|
||||
Modelfile.create(
|
||||
user_id=model.user_id,
|
||||
tag_name=model.id,
|
||||
modelfile=modelfile_data,
|
||||
timestamp=model.created_at,
|
||||
)
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Peewee migrations -- 002_add_local_sharing.py.
|
||||
|
||||
Some examples (model - class or model name)::
|
||||
|
||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||
> Model = migrator.ModelClass # Return model in current state by name
|
||||
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
> migrator.add_constraint(model, name, sql)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.drop_constraints(model, *constraints)
|
||||
|
||||
"""
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
import peewee as pw
|
||||
from peewee_migrate import Migrator
|
||||
|
||||
|
||||
with suppress(ImportError):
|
||||
import playhouse.postgres_ext as pw_pext
|
||||
|
||||
|
||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your migrations here."""
|
||||
|
||||
# Adding fields settings to the 'user' table
|
||||
migrator.add_fields("user", settings=pw.TextField(null=True))
|
||||
|
||||
|
||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your rollback migrations here."""
|
||||
|
||||
# Remove the settings field
|
||||
migrator.remove_fields("user", "settings")
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Peewee migrations -- 009_add_models.py.
|
||||
|
||||
Some examples (model - class or model name)::
|
||||
|
||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||
> Model = migrator.ModelClass # Return model in current state by name
|
||||
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
> migrator.add_constraint(model, name, sql)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.drop_constraints(model, *constraints)
|
||||
|
||||
"""
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
import peewee as pw
|
||||
from peewee_migrate import Migrator
|
||||
|
||||
|
||||
with suppress(ImportError):
|
||||
import playhouse.postgres_ext as pw_pext
|
||||
|
||||
|
||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your migrations here."""
|
||||
|
||||
@migrator.create_model
|
||||
class Tool(pw.Model):
|
||||
id = pw.TextField(unique=True)
|
||||
user_id = pw.TextField()
|
||||
|
||||
name = pw.TextField()
|
||||
content = pw.TextField()
|
||||
specs = pw.TextField()
|
||||
|
||||
meta = pw.TextField()
|
||||
|
||||
created_at = pw.BigIntegerField(null=False)
|
||||
updated_at = pw.BigIntegerField(null=False)
|
||||
|
||||
class Meta:
|
||||
table_name = "tool"
|
||||
|
||||
|
||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your rollback migrations here."""
|
||||
|
||||
migrator.remove_model("tool")
|
||||
+1
-1
@@ -14,7 +14,7 @@ You will need to create a migration file to ensure that existing databases are u
|
||||
2. Make your changes to the models.
|
||||
3. From the `backend` directory, run the following command:
|
||||
```bash
|
||||
pw_migrate create --auto --auto-source apps.web.models --database sqlite:///${SQLITE_DB} --directory apps/web/internal/migrations ${MIGRATION_NAME}
|
||||
pw_migrate create --auto --auto-source apps.webui.models --database sqlite:///${SQLITE_DB} --directory apps/web/internal/migrations ${MIGRATION_NAME}
|
||||
```
|
||||
- `$SQLITE_DB` should be the path to the database file.
|
||||
- `$MIGRATION_NAME` should be a descriptive name for the migration.
|
||||
@@ -1,19 +1,22 @@
|
||||
from fastapi import FastAPI, Depends
|
||||
from fastapi.routing import APIRoute
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from apps.web.routers import (
|
||||
from apps.webui.routers import (
|
||||
auths,
|
||||
users,
|
||||
chats,
|
||||
documents,
|
||||
modelfiles,
|
||||
tools,
|
||||
models,
|
||||
prompts,
|
||||
configs,
|
||||
memories,
|
||||
utils,
|
||||
)
|
||||
from config import (
|
||||
WEBUI_VERSION,
|
||||
WEBUI_BUILD_HASH,
|
||||
SHOW_ADMIN_DETAILS,
|
||||
ADMIN_EMAIL,
|
||||
WEBUI_AUTH,
|
||||
DEFAULT_MODELS,
|
||||
DEFAULT_PROMPT_SUGGESTIONS,
|
||||
@@ -23,6 +26,8 @@ from config import (
|
||||
WEBHOOK_URL,
|
||||
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
|
||||
JWT_EXPIRES_IN,
|
||||
WEBUI_BANNERS,
|
||||
ENABLE_COMMUNITY_SHARING,
|
||||
AppConfig,
|
||||
)
|
||||
|
||||
@@ -34,13 +39,24 @@ app.state.config = AppConfig()
|
||||
|
||||
app.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP
|
||||
app.state.config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
|
||||
app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
|
||||
|
||||
|
||||
app.state.config.SHOW_ADMIN_DETAILS = SHOW_ADMIN_DETAILS
|
||||
app.state.config.ADMIN_EMAIL = ADMIN_EMAIL
|
||||
|
||||
|
||||
app.state.config.DEFAULT_MODELS = DEFAULT_MODELS
|
||||
app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
|
||||
app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
|
||||
app.state.config.USER_PERMISSIONS = USER_PERMISSIONS
|
||||
app.state.config.WEBHOOK_URL = WEBHOOK_URL
|
||||
app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
|
||||
app.state.config.BANNERS = WEBUI_BANNERS
|
||||
|
||||
app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
|
||||
|
||||
app.state.MODELS = {}
|
||||
app.state.TOOLS = {}
|
||||
|
||||
|
||||
app.add_middleware(
|
||||
@@ -56,11 +72,11 @@ app.include_router(users.router, prefix="/users", tags=["users"])
|
||||
app.include_router(chats.router, prefix="/chats", tags=["chats"])
|
||||
|
||||
app.include_router(documents.router, prefix="/documents", tags=["documents"])
|
||||
app.include_router(modelfiles.router, prefix="/modelfiles", tags=["modelfiles"])
|
||||
app.include_router(tools.router, prefix="/tools", tags=["tools"])
|
||||
app.include_router(models.router, prefix="/models", tags=["models"])
|
||||
app.include_router(prompts.router, prefix="/prompts", tags=["prompts"])
|
||||
app.include_router(memories.router, prefix="/memories", tags=["memories"])
|
||||
|
||||
|
||||
app.include_router(configs.router, prefix="/configs", tags=["configs"])
|
||||
app.include_router(utils.router, prefix="/utils", tags=["utils"])
|
||||
|
||||
@@ -5,10 +5,10 @@ import uuid
|
||||
import logging
|
||||
from peewee import *
|
||||
|
||||
from apps.web.models.users import UserModel, Users
|
||||
from apps.webui.models.users import UserModel, Users
|
||||
from utils.utils import verify_password
|
||||
|
||||
from apps.web.internal.db import DB
|
||||
from apps.webui.internal.db import DB
|
||||
|
||||
from config import SRC_LOG_LEVELS
|
||||
|
||||
@@ -7,7 +7,7 @@ import json
|
||||
import uuid
|
||||
import time
|
||||
|
||||
from apps.web.internal.db import DB
|
||||
from apps.webui.internal.db import DB
|
||||
|
||||
####################
|
||||
# Chat DB Schema
|
||||
@@ -191,6 +191,20 @@ class ChatTable:
|
||||
except:
|
||||
return None
|
||||
|
||||
def archive_all_chats_by_user_id(self, user_id: str) -> bool:
|
||||
try:
|
||||
chats = self.get_chats_by_user_id(user_id)
|
||||
for chat in chats:
|
||||
query = Chat.update(
|
||||
archived=True,
|
||||
).where(Chat.id == chat.id)
|
||||
|
||||
query.execute()
|
||||
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def get_archived_chat_list_by_user_id(
|
||||
self, user_id: str, skip: int = 0, limit: int = 50
|
||||
) -> List[ChatModel]:
|
||||
@@ -205,17 +219,31 @@ class ChatTable:
|
||||
]
|
||||
|
||||
def get_chat_list_by_user_id(
|
||||
self, user_id: str, skip: int = 0, limit: int = 50
|
||||
self,
|
||||
user_id: str,
|
||||
include_archived: bool = False,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
) -> List[ChatModel]:
|
||||
return [
|
||||
ChatModel(**model_to_dict(chat))
|
||||
for chat in Chat.select()
|
||||
.where(Chat.archived == False)
|
||||
.where(Chat.user_id == user_id)
|
||||
.order_by(Chat.updated_at.desc())
|
||||
# .limit(limit)
|
||||
# .offset(skip)
|
||||
]
|
||||
if include_archived:
|
||||
return [
|
||||
ChatModel(**model_to_dict(chat))
|
||||
for chat in Chat.select()
|
||||
.where(Chat.user_id == user_id)
|
||||
.order_by(Chat.updated_at.desc())
|
||||
# .limit(limit)
|
||||
# .offset(skip)
|
||||
]
|
||||
else:
|
||||
return [
|
||||
ChatModel(**model_to_dict(chat))
|
||||
for chat in Chat.select()
|
||||
.where(Chat.archived == False)
|
||||
.where(Chat.user_id == user_id)
|
||||
.order_by(Chat.updated_at.desc())
|
||||
# .limit(limit)
|
||||
# .offset(skip)
|
||||
]
|
||||
|
||||
def get_chat_list_by_chat_ids(
|
||||
self, chat_ids: List[str], skip: int = 0, limit: int = 50
|
||||
@@ -270,6 +298,15 @@ class ChatTable:
|
||||
# .limit(limit).offset(skip)
|
||||
]
|
||||
|
||||
def get_archived_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
|
||||
return [
|
||||
ChatModel(**model_to_dict(chat))
|
||||
for chat in Chat.select()
|
||||
.where(Chat.archived == True)
|
||||
.where(Chat.user_id == user_id)
|
||||
.order_by(Chat.updated_at.desc())
|
||||
]
|
||||
|
||||
def delete_chat_by_id(self, id: str) -> bool:
|
||||
try:
|
||||
query = Chat.delete().where((Chat.id == id))
|
||||
@@ -8,7 +8,7 @@ import logging
|
||||
from utils.utils import decode_token
|
||||
from utils.misc import get_gravatar_url
|
||||
|
||||
from apps.web.internal.db import DB
|
||||
from apps.webui.internal.db import DB
|
||||
|
||||
import json
|
||||
|
||||
@@ -3,8 +3,8 @@ from peewee import *
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
from typing import List, Union, Optional
|
||||
|
||||
from apps.web.internal.db import DB
|
||||
from apps.web.models.chats import Chats
|
||||
from apps.webui.internal.db import DB
|
||||
from apps.webui.models.chats import Chats
|
||||
|
||||
import time
|
||||
import uuid
|
||||
@@ -0,0 +1,179 @@
|
||||
import json
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import peewee as pw
|
||||
from peewee import *
|
||||
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from apps.webui.internal.db import DB, JSONField
|
||||
|
||||
from typing import List, Union, Optional
|
||||
from config import SRC_LOG_LEVELS
|
||||
|
||||
import time
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||
|
||||
|
||||
####################
|
||||
# Models DB Schema
|
||||
####################
|
||||
|
||||
|
||||
# ModelParams is a model for the data stored in the params field of the Model table
|
||||
class ModelParams(BaseModel):
|
||||
model_config = ConfigDict(extra="allow")
|
||||
pass
|
||||
|
||||
|
||||
# ModelMeta is a model for the data stored in the meta field of the Model table
|
||||
class ModelMeta(BaseModel):
|
||||
profile_image_url: Optional[str] = "/favicon.png"
|
||||
|
||||
description: Optional[str] = None
|
||||
"""
|
||||
User-facing description of the model.
|
||||
"""
|
||||
|
||||
capabilities: Optional[dict] = None
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Model(pw.Model):
|
||||
id = pw.TextField(unique=True)
|
||||
"""
|
||||
The model's id as used in the API. If set to an existing model, it will override the model.
|
||||
"""
|
||||
user_id = pw.TextField()
|
||||
|
||||
base_model_id = pw.TextField(null=True)
|
||||
"""
|
||||
An optional pointer to the actual model that should be used when proxying requests.
|
||||
"""
|
||||
|
||||
name = pw.TextField()
|
||||
"""
|
||||
The human-readable display name of the model.
|
||||
"""
|
||||
|
||||
params = JSONField()
|
||||
"""
|
||||
Holds a JSON encoded blob of parameters, see `ModelParams`.
|
||||
"""
|
||||
|
||||
meta = JSONField()
|
||||
"""
|
||||
Holds a JSON encoded blob of metadata, see `ModelMeta`.
|
||||
"""
|
||||
|
||||
updated_at = BigIntegerField()
|
||||
created_at = BigIntegerField()
|
||||
|
||||
class Meta:
|
||||
database = DB
|
||||
|
||||
|
||||
class ModelModel(BaseModel):
|
||||
id: str
|
||||
user_id: str
|
||||
base_model_id: Optional[str] = None
|
||||
|
||||
name: str
|
||||
params: ModelParams
|
||||
meta: ModelMeta
|
||||
|
||||
updated_at: int # timestamp in epoch
|
||||
created_at: int # timestamp in epoch
|
||||
|
||||
|
||||
####################
|
||||
# Forms
|
||||
####################
|
||||
|
||||
|
||||
class ModelResponse(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
meta: ModelMeta
|
||||
updated_at: int # timestamp in epoch
|
||||
created_at: int # timestamp in epoch
|
||||
|
||||
|
||||
class ModelForm(BaseModel):
|
||||
id: str
|
||||
base_model_id: Optional[str] = None
|
||||
name: str
|
||||
meta: ModelMeta
|
||||
params: ModelParams
|
||||
|
||||
|
||||
class ModelsTable:
|
||||
def __init__(
|
||||
self,
|
||||
db: pw.SqliteDatabase | pw.PostgresqlDatabase,
|
||||
):
|
||||
self.db = db
|
||||
self.db.create_tables([Model])
|
||||
|
||||
def insert_new_model(
|
||||
self, form_data: ModelForm, user_id: str
|
||||
) -> Optional[ModelModel]:
|
||||
model = ModelModel(
|
||||
**{
|
||||
**form_data.model_dump(),
|
||||
"user_id": user_id,
|
||||
"created_at": int(time.time()),
|
||||
"updated_at": int(time.time()),
|
||||
}
|
||||
)
|
||||
try:
|
||||
result = Model.create(**model.model_dump())
|
||||
|
||||
if result:
|
||||
return model
|
||||
else:
|
||||
return None
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return None
|
||||
|
||||
def get_all_models(self) -> List[ModelModel]:
|
||||
return [ModelModel(**model_to_dict(model)) for model in Model.select()]
|
||||
|
||||
def get_model_by_id(self, id: str) -> Optional[ModelModel]:
|
||||
try:
|
||||
model = Model.get(Model.id == id)
|
||||
return ModelModel(**model_to_dict(model))
|
||||
except:
|
||||
return None
|
||||
|
||||
def update_model_by_id(self, id: str, model: ModelForm) -> Optional[ModelModel]:
|
||||
try:
|
||||
# update only the fields that are present in the model
|
||||
query = Model.update(**model.model_dump()).where(Model.id == id)
|
||||
query.execute()
|
||||
|
||||
model = Model.get(Model.id == id)
|
||||
return ModelModel(**model_to_dict(model))
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
return None
|
||||
|
||||
def delete_model_by_id(self, id: str) -> bool:
|
||||
try:
|
||||
query = Model.delete().where(Model.id == id)
|
||||
query.execute()
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
Models = ModelsTable(DB)
|
||||
@@ -7,7 +7,7 @@ import time
|
||||
from utils.utils import decode_token
|
||||
from utils.misc import get_gravatar_url
|
||||
|
||||
from apps.web.internal.db import DB
|
||||
from apps.webui.internal.db import DB
|
||||
|
||||
import json
|
||||
|
||||
@@ -8,7 +8,7 @@ import uuid
|
||||
import time
|
||||
import logging
|
||||
|
||||
from apps.web.internal.db import DB
|
||||
from apps.webui.internal.db import DB
|
||||
|
||||
from config import SRC_LOG_LEVELS
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
from pydantic import BaseModel
|
||||
from peewee import *
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
from typing import List, Union, Optional
|
||||
import time
|
||||
import logging
|
||||
from apps.webui.internal.db import DB, JSONField
|
||||
|
||||
import json
|
||||
|
||||
from config import SRC_LOG_LEVELS
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||
|
||||
####################
|
||||
# Tools DB Schema
|
||||
####################
|
||||
|
||||
|
||||
class Tool(Model):
|
||||
id = CharField(unique=True)
|
||||
user_id = CharField()
|
||||
name = TextField()
|
||||
content = TextField()
|
||||
specs = JSONField()
|
||||
meta = JSONField()
|
||||
updated_at = BigIntegerField()
|
||||
created_at = BigIntegerField()
|
||||
|
||||
class Meta:
|
||||
database = DB
|
||||
|
||||
|
||||
class ToolMeta(BaseModel):
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class ToolModel(BaseModel):
|
||||
id: str
|
||||
user_id: str
|
||||
name: str
|
||||
content: str
|
||||
specs: List[dict]
|
||||
meta: ToolMeta
|
||||
updated_at: int # timestamp in epoch
|
||||
created_at: int # timestamp in epoch
|
||||
|
||||
|
||||
####################
|
||||
# Forms
|
||||
####################
|
||||
|
||||
|
||||
class ToolResponse(BaseModel):
|
||||
id: str
|
||||
user_id: str
|
||||
name: str
|
||||
meta: ToolMeta
|
||||
updated_at: int # timestamp in epoch
|
||||
created_at: int # timestamp in epoch
|
||||
|
||||
|
||||
class ToolForm(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
content: str
|
||||
meta: ToolMeta
|
||||
|
||||
|
||||
class ToolsTable:
|
||||
def __init__(self, db):
|
||||
self.db = db
|
||||
self.db.create_tables([Tool])
|
||||
|
||||
def insert_new_tool(
|
||||
self, user_id: str, form_data: ToolForm, specs: List[dict]
|
||||
) -> Optional[ToolModel]:
|
||||
tool = ToolModel(
|
||||
**{
|
||||
**form_data.model_dump(),
|
||||
"specs": specs,
|
||||
"user_id": user_id,
|
||||
"updated_at": int(time.time()),
|
||||
"created_at": int(time.time()),
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
result = Tool.create(**tool.model_dump())
|
||||
if result:
|
||||
return tool
|
||||
else:
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Error creating tool: {e}")
|
||||
return None
|
||||
|
||||
def get_tool_by_id(self, id: str) -> Optional[ToolModel]:
|
||||
try:
|
||||
tool = Tool.get(Tool.id == id)
|
||||
return ToolModel(**model_to_dict(tool))
|
||||
except:
|
||||
return None
|
||||
|
||||
def get_tools(self) -> List[ToolModel]:
|
||||
return [ToolModel(**model_to_dict(tool)) for tool in Tool.select()]
|
||||
|
||||
def update_tool_by_id(self, id: str, updated: dict) -> Optional[ToolModel]:
|
||||
try:
|
||||
query = Tool.update(
|
||||
**updated,
|
||||
updated_at=int(time.time()),
|
||||
).where(Tool.id == id)
|
||||
query.execute()
|
||||
|
||||
tool = Tool.get(Tool.id == id)
|
||||
return ToolModel(**model_to_dict(tool))
|
||||
except:
|
||||
return None
|
||||
|
||||
def delete_tool_by_id(self, id: str) -> bool:
|
||||
try:
|
||||
query = Tool.delete().where((Tool.id == id))
|
||||
query.execute() # Remove the rows, return number of rows removed.
|
||||
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
Tools = ToolsTable(DB)
|
||||
@@ -1,12 +1,12 @@
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from peewee import *
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
from typing import List, Union, Optional
|
||||
import time
|
||||
from utils.misc import get_gravatar_url
|
||||
|
||||
from apps.web.internal.db import DB
|
||||
from apps.web.models.chats import Chats
|
||||
from apps.webui.internal.db import DB, JSONField
|
||||
from apps.webui.models.chats import Chats
|
||||
|
||||
####################
|
||||
# User DB Schema
|
||||
@@ -25,11 +25,18 @@ class User(Model):
|
||||
created_at = BigIntegerField()
|
||||
|
||||
api_key = CharField(null=True, unique=True)
|
||||
settings = JSONField(null=True)
|
||||
|
||||
class Meta:
|
||||
database = DB
|
||||
|
||||
|
||||
class UserSettings(BaseModel):
|
||||
ui: Optional[dict] = {}
|
||||
model_config = ConfigDict(extra="allow")
|
||||
pass
|
||||
|
||||
|
||||
class UserModel(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
@@ -42,6 +49,7 @@ class UserModel(BaseModel):
|
||||
created_at: int # timestamp in epoch
|
||||
|
||||
api_key: Optional[str] = None
|
||||
settings: Optional[UserSettings] = None
|
||||
|
||||
|
||||
####################
|
||||
@@ -10,7 +10,7 @@ import uuid
|
||||
import csv
|
||||
|
||||
|
||||
from apps.web.models.auths import (
|
||||
from apps.webui.models.auths import (
|
||||
SigninForm,
|
||||
SignupForm,
|
||||
AddUserForm,
|
||||
@@ -21,7 +21,7 @@ from apps.web.models.auths import (
|
||||
Auths,
|
||||
ApiKey,
|
||||
)
|
||||
from apps.web.models.users import Users
|
||||
from apps.webui.models.users import Users
|
||||
|
||||
from utils.utils import (
|
||||
get_password_hash,
|
||||
@@ -269,73 +269,88 @@ async def add_user(form_data: AddUserForm, user=Depends(get_admin_user)):
|
||||
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
|
||||
|
||||
|
||||
############################
|
||||
# GetAdminDetails
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/admin/details")
|
||||
async def get_admin_details(request: Request, user=Depends(get_current_user)):
|
||||
if request.app.state.config.SHOW_ADMIN_DETAILS:
|
||||
admin_email = request.app.state.config.ADMIN_EMAIL
|
||||
admin_name = None
|
||||
|
||||
print(admin_email, admin_name)
|
||||
|
||||
if admin_email:
|
||||
admin = Users.get_user_by_email(admin_email)
|
||||
if admin:
|
||||
admin_name = admin.name
|
||||
else:
|
||||
admin = Users.get_first_user()
|
||||
if admin:
|
||||
admin_email = admin.email
|
||||
admin_name = admin.name
|
||||
|
||||
return {
|
||||
"name": admin_name,
|
||||
"email": admin_email,
|
||||
}
|
||||
else:
|
||||
raise HTTPException(400, detail=ERROR_MESSAGES.ACTION_PROHIBITED)
|
||||
|
||||
|
||||
############################
|
||||
# ToggleSignUp
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/signup/enabled", response_model=bool)
|
||||
async def get_sign_up_status(request: Request, user=Depends(get_admin_user)):
|
||||
return request.app.state.config.ENABLE_SIGNUP
|
||||
@router.get("/admin/config")
|
||||
async def get_admin_config(request: Request, user=Depends(get_admin_user)):
|
||||
return {
|
||||
"SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS,
|
||||
"ENABLE_SIGNUP": request.app.state.config.ENABLE_SIGNUP,
|
||||
"DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE,
|
||||
"JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN,
|
||||
"ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/signup/enabled/toggle", response_model=bool)
|
||||
async def toggle_sign_up(request: Request, user=Depends(get_admin_user)):
|
||||
request.app.state.config.ENABLE_SIGNUP = not request.app.state.config.ENABLE_SIGNUP
|
||||
return request.app.state.config.ENABLE_SIGNUP
|
||||
class AdminConfig(BaseModel):
|
||||
SHOW_ADMIN_DETAILS: bool
|
||||
ENABLE_SIGNUP: bool
|
||||
DEFAULT_USER_ROLE: str
|
||||
JWT_EXPIRES_IN: str
|
||||
ENABLE_COMMUNITY_SHARING: bool
|
||||
|
||||
|
||||
############################
|
||||
# Default User Role
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/signup/user/role")
|
||||
async def get_default_user_role(request: Request, user=Depends(get_admin_user)):
|
||||
return request.app.state.config.DEFAULT_USER_ROLE
|
||||
|
||||
|
||||
class UpdateRoleForm(BaseModel):
|
||||
role: str
|
||||
|
||||
|
||||
@router.post("/signup/user/role")
|
||||
async def update_default_user_role(
|
||||
request: Request, form_data: UpdateRoleForm, user=Depends(get_admin_user)
|
||||
@router.post("/admin/config")
|
||||
async def update_admin_config(
|
||||
request: Request, form_data: AdminConfig, user=Depends(get_admin_user)
|
||||
):
|
||||
if form_data.role in ["pending", "user", "admin"]:
|
||||
request.app.state.config.DEFAULT_USER_ROLE = form_data.role
|
||||
return request.app.state.config.DEFAULT_USER_ROLE
|
||||
request.app.state.config.SHOW_ADMIN_DETAILS = form_data.SHOW_ADMIN_DETAILS
|
||||
request.app.state.config.ENABLE_SIGNUP = form_data.ENABLE_SIGNUP
|
||||
|
||||
if form_data.DEFAULT_USER_ROLE in ["pending", "user", "admin"]:
|
||||
request.app.state.config.DEFAULT_USER_ROLE = form_data.DEFAULT_USER_ROLE
|
||||
|
||||
############################
|
||||
# JWT Expiration
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/token/expires")
|
||||
async def get_token_expires_duration(request: Request, user=Depends(get_admin_user)):
|
||||
return request.app.state.config.JWT_EXPIRES_IN
|
||||
|
||||
|
||||
class UpdateJWTExpiresDurationForm(BaseModel):
|
||||
duration: str
|
||||
|
||||
|
||||
@router.post("/token/expires/update")
|
||||
async def update_token_expires_duration(
|
||||
request: Request,
|
||||
form_data: UpdateJWTExpiresDurationForm,
|
||||
user=Depends(get_admin_user),
|
||||
):
|
||||
pattern = r"^(-1|0|(-?\d+(\.\d+)?)(ms|s|m|h|d|w))$"
|
||||
|
||||
# Check if the input string matches the pattern
|
||||
if re.match(pattern, form_data.duration):
|
||||
request.app.state.config.JWT_EXPIRES_IN = form_data.duration
|
||||
return request.app.state.config.JWT_EXPIRES_IN
|
||||
else:
|
||||
return request.app.state.config.JWT_EXPIRES_IN
|
||||
if re.match(pattern, form_data.JWT_EXPIRES_IN):
|
||||
request.app.state.config.JWT_EXPIRES_IN = form_data.JWT_EXPIRES_IN
|
||||
|
||||
request.app.state.config.ENABLE_COMMUNITY_SHARING = (
|
||||
form_data.ENABLE_COMMUNITY_SHARING
|
||||
)
|
||||
|
||||
return {
|
||||
"SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS,
|
||||
"ENABLE_SIGNUP": request.app.state.config.ENABLE_SIGNUP,
|
||||
"DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE,
|
||||
"JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN,
|
||||
"ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
|
||||
}
|
||||
|
||||
|
||||
############################
|
||||
@@ -7,8 +7,8 @@ from pydantic import BaseModel
|
||||
import json
|
||||
import logging
|
||||
|
||||
from apps.web.models.users import Users
|
||||
from apps.web.models.chats import (
|
||||
from apps.webui.models.users import Users
|
||||
from apps.webui.models.chats import (
|
||||
ChatModel,
|
||||
ChatResponse,
|
||||
ChatTitleForm,
|
||||
@@ -18,7 +18,7 @@ from apps.web.models.chats import (
|
||||
)
|
||||
|
||||
|
||||
from apps.web.models.tags import (
|
||||
from apps.webui.models.tags import (
|
||||
TagModel,
|
||||
ChatIdTagModel,
|
||||
ChatIdTagForm,
|
||||
@@ -78,7 +78,70 @@ async def delete_all_user_chats(request: Request, user=Depends(get_current_user)
|
||||
async def get_user_chat_list_by_user_id(
|
||||
user_id: str, user=Depends(get_admin_user), skip: int = 0, limit: int = 50
|
||||
):
|
||||
return Chats.get_chat_list_by_user_id(user_id, skip, limit)
|
||||
return Chats.get_chat_list_by_user_id(
|
||||
user_id, include_archived=True, skip=skip, limit=limit
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# CreateNewChat
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/new", response_model=Optional[ChatResponse])
|
||||
async def create_new_chat(form_data: ChatForm, user=Depends(get_current_user)):
|
||||
try:
|
||||
chat = Chats.insert_new_chat(user.id, form_data)
|
||||
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# GetChats
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/all", response_model=List[ChatResponse])
|
||||
async def get_user_chats(user=Depends(get_current_user)):
|
||||
return [
|
||||
ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
|
||||
for chat in Chats.get_chats_by_user_id(user.id)
|
||||
]
|
||||
|
||||
|
||||
############################
|
||||
# GetArchivedChats
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/all/archived", response_model=List[ChatResponse])
|
||||
async def get_user_chats(user=Depends(get_current_user)):
|
||||
return [
|
||||
ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
|
||||
for chat in Chats.get_archived_chats_by_user_id(user.id)
|
||||
]
|
||||
|
||||
|
||||
############################
|
||||
# GetAllChatsInDB
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/all/db", response_model=List[ChatResponse])
|
||||
async def get_all_user_chats_in_db(user=Depends(get_admin_user)):
|
||||
if not ENABLE_ADMIN_EXPORT:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
return [
|
||||
ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
|
||||
for chat in Chats.get_chats()
|
||||
]
|
||||
|
||||
|
||||
############################
|
||||
@@ -93,6 +156,16 @@ async def get_archived_session_user_chat_list(
|
||||
return Chats.get_archived_chat_list_by_user_id(user.id, skip, limit)
|
||||
|
||||
|
||||
############################
|
||||
# ArchiveAllChats
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/archive/all", response_model=bool)
|
||||
async def archive_all_chats(user=Depends(get_current_user)):
|
||||
return Chats.archive_all_chats_by_user_id(user.id)
|
||||
|
||||
|
||||
############################
|
||||
# GetSharedChatById
|
||||
############################
|
||||
@@ -118,54 +191,6 @@ async def get_shared_chat_by_id(share_id: str, user=Depends(get_current_user)):
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# GetChats
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/all", response_model=List[ChatResponse])
|
||||
async def get_user_chats(user=Depends(get_current_user)):
|
||||
return [
|
||||
ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
|
||||
for chat in Chats.get_chats_by_user_id(user.id)
|
||||
]
|
||||
|
||||
|
||||
############################
|
||||
# GetAllChatsInDB
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/all/db", response_model=List[ChatResponse])
|
||||
async def get_all_user_chats_in_db(user=Depends(get_admin_user)):
|
||||
if not ENABLE_ADMIN_EXPORT:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
return [
|
||||
ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
|
||||
for chat in Chats.get_chats()
|
||||
]
|
||||
|
||||
|
||||
############################
|
||||
# CreateNewChat
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/new", response_model=Optional[ChatResponse])
|
||||
async def create_new_chat(form_data: ChatForm, user=Depends(get_current_user)):
|
||||
try:
|
||||
chat = Chats.insert_new_chat(user.id, form_data)
|
||||
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# GetChatsByTags
|
||||
############################
|
||||
@@ -276,6 +301,32 @@ async def delete_chat_by_id(request: Request, id: str, user=Depends(get_current_
|
||||
return result
|
||||
|
||||
|
||||
############################
|
||||
# CloneChat
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/{id}/clone", response_model=Optional[ChatResponse])
|
||||
async def clone_chat_by_id(id: str, user=Depends(get_current_user)):
|
||||
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
|
||||
if chat:
|
||||
|
||||
chat_body = json.loads(chat.chat)
|
||||
updated_chat = {
|
||||
**chat_body,
|
||||
"originalChatId": chat.id,
|
||||
"branchPointMessageId": chat_body["history"]["currentId"],
|
||||
"title": f"Clone of {chat.title}",
|
||||
}
|
||||
|
||||
chat = Chats.insert_new_chat(user.id, ChatForm(**{"chat": updated_chat}))
|
||||
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# ArchiveChat
|
||||
############################
|
||||
@@ -8,7 +8,9 @@ from pydantic import BaseModel
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from apps.web.models.users import Users
|
||||
from config import BannerModel
|
||||
|
||||
from apps.webui.models.users import Users
|
||||
|
||||
from utils.utils import (
|
||||
get_password_hash,
|
||||
@@ -57,3 +59,31 @@ async def set_global_default_suggestions(
|
||||
data = form_data.model_dump()
|
||||
request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS = data["suggestions"]
|
||||
return request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS
|
||||
|
||||
|
||||
############################
|
||||
# SetBanners
|
||||
############################
|
||||
|
||||
|
||||
class SetBannersForm(BaseModel):
|
||||
banners: List[BannerModel]
|
||||
|
||||
|
||||
@router.post("/banners", response_model=List[BannerModel])
|
||||
async def set_banners(
|
||||
request: Request,
|
||||
form_data: SetBannersForm,
|
||||
user=Depends(get_admin_user),
|
||||
):
|
||||
data = form_data.model_dump()
|
||||
request.app.state.config.BANNERS = data["banners"]
|
||||
return request.app.state.config.BANNERS
|
||||
|
||||
|
||||
@router.get("/banners", response_model=List[BannerModel])
|
||||
async def get_banners(
|
||||
request: Request,
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
return request.app.state.config.BANNERS
|
||||
@@ -6,7 +6,7 @@ from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
import json
|
||||
|
||||
from apps.web.models.documents import (
|
||||
from apps.webui.models.documents import (
|
||||
Documents,
|
||||
DocumentForm,
|
||||
DocumentUpdateForm,
|
||||
@@ -7,7 +7,7 @@ from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
import logging
|
||||
|
||||
from apps.web.models.memories import Memories, MemoryModel
|
||||
from apps.webui.models.memories import Memories, MemoryModel
|
||||
|
||||
from utils.utils import get_verified_user
|
||||
from constants import ERROR_MESSAGES
|
||||
@@ -0,0 +1,107 @@
|
||||
from fastapi import Depends, FastAPI, HTTPException, status, Request
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Union, Optional
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
import json
|
||||
from apps.webui.models.models import Models, ModelModel, ModelForm, ModelResponse
|
||||
|
||||
from utils.utils import get_verified_user, get_admin_user
|
||||
from constants import ERROR_MESSAGES
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
###########################
|
||||
# getModels
|
||||
###########################
|
||||
|
||||
|
||||
@router.get("/", response_model=List[ModelResponse])
|
||||
async def get_models(user=Depends(get_verified_user)):
|
||||
return Models.get_all_models()
|
||||
|
||||
|
||||
############################
|
||||
# AddNewModel
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/add", response_model=Optional[ModelModel])
|
||||
async def add_new_model(
|
||||
request: Request, form_data: ModelForm, user=Depends(get_admin_user)
|
||||
):
|
||||
if form_data.id in request.app.state.MODELS:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.MODEL_ID_TAKEN,
|
||||
)
|
||||
else:
|
||||
model = Models.insert_new_model(form_data, user.id)
|
||||
|
||||
if model:
|
||||
return model
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.DEFAULT(),
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# GetModelById
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/", response_model=Optional[ModelModel])
|
||||
async def get_model_by_id(id: str, user=Depends(get_verified_user)):
|
||||
model = Models.get_model_by_id(id)
|
||||
|
||||
if model:
|
||||
return model
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# UpdateModelById
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/update", response_model=Optional[ModelModel])
|
||||
async def update_model_by_id(
|
||||
request: Request, id: str, form_data: ModelForm, user=Depends(get_admin_user)
|
||||
):
|
||||
model = Models.get_model_by_id(id)
|
||||
if model:
|
||||
model = Models.update_model_by_id(id, form_data)
|
||||
return model
|
||||
else:
|
||||
if form_data.id in request.app.state.MODELS:
|
||||
model = Models.insert_new_model(form_data, user.id)
|
||||
if model:
|
||||
return model
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.DEFAULT(),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.DEFAULT(),
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# DeleteModelById
|
||||
############################
|
||||
|
||||
|
||||
@router.delete("/delete", response_model=bool)
|
||||
async def delete_model_by_id(id: str, user=Depends(get_admin_user)):
|
||||
result = Models.delete_model_by_id(id)
|
||||
return result
|
||||
@@ -6,7 +6,7 @@ from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
import json
|
||||
|
||||
from apps.web.models.prompts import Prompts, PromptForm, PromptModel
|
||||
from apps.webui.models.prompts import Prompts, PromptForm, PromptModel
|
||||
|
||||
from utils.utils import get_current_user, get_admin_user
|
||||
from constants import ERROR_MESSAGES
|
||||
@@ -0,0 +1,183 @@
|
||||
from fastapi import Depends, FastAPI, HTTPException, status, Request
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Union, Optional
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
import json
|
||||
|
||||
from apps.webui.models.tools import Tools, ToolForm, ToolModel, ToolResponse
|
||||
from apps.webui.utils import load_toolkit_module_by_id
|
||||
|
||||
from utils.utils import get_current_user, get_admin_user
|
||||
from utils.tools import get_tools_specs
|
||||
from constants import ERROR_MESSAGES
|
||||
|
||||
from importlib import util
|
||||
import os
|
||||
|
||||
from config import DATA_DIR
|
||||
|
||||
|
||||
TOOLS_DIR = f"{DATA_DIR}/tools"
|
||||
os.makedirs(TOOLS_DIR, exist_ok=True)
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
############################
|
||||
# GetToolkits
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/", response_model=List[ToolResponse])
|
||||
async def get_toolkits(user=Depends(get_current_user)):
|
||||
toolkits = [toolkit for toolkit in Tools.get_tools()]
|
||||
return toolkits
|
||||
|
||||
|
||||
############################
|
||||
# ExportToolKits
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/export", response_model=List[ToolModel])
|
||||
async def get_toolkits(user=Depends(get_admin_user)):
|
||||
toolkits = [toolkit for toolkit in Tools.get_tools()]
|
||||
return toolkits
|
||||
|
||||
|
||||
############################
|
||||
# CreateNewToolKit
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/create", response_model=Optional[ToolResponse])
|
||||
async def create_new_toolkit(
|
||||
request: Request, form_data: ToolForm, user=Depends(get_admin_user)
|
||||
):
|
||||
if not form_data.id.isidentifier():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Only alphanumeric characters and underscores are allowed in the id",
|
||||
)
|
||||
|
||||
form_data.id = form_data.id.lower()
|
||||
|
||||
toolkit = Tools.get_tool_by_id(form_data.id)
|
||||
if toolkit == None:
|
||||
toolkit_path = os.path.join(TOOLS_DIR, f"{form_data.id}.py")
|
||||
try:
|
||||
with open(toolkit_path, "w") as tool_file:
|
||||
tool_file.write(form_data.content)
|
||||
|
||||
toolkit_module = load_toolkit_module_by_id(form_data.id)
|
||||
|
||||
TOOLS = request.app.state.TOOLS
|
||||
TOOLS[form_data.id] = toolkit_module
|
||||
|
||||
specs = get_tools_specs(TOOLS[form_data.id])
|
||||
toolkit = Tools.insert_new_tool(user.id, form_data, specs)
|
||||
|
||||
if toolkit:
|
||||
return toolkit
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT("Error creating toolkit"),
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.ID_TAKEN,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# GetToolkitById
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/id/{id}", response_model=Optional[ToolModel])
|
||||
async def get_toolkit_by_id(id: str, user=Depends(get_admin_user)):
|
||||
toolkit = Tools.get_tool_by_id(id)
|
||||
|
||||
if toolkit:
|
||||
return toolkit
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# UpdateToolkitById
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/id/{id}/update", response_model=Optional[ToolModel])
|
||||
async def update_toolkit_by_id(
|
||||
request: Request, id: str, form_data: ToolForm, user=Depends(get_admin_user)
|
||||
):
|
||||
toolkit_path = os.path.join(TOOLS_DIR, f"{id}.py")
|
||||
|
||||
try:
|
||||
with open(toolkit_path, "w") as tool_file:
|
||||
tool_file.write(form_data.content)
|
||||
|
||||
toolkit_module = load_toolkit_module_by_id(id)
|
||||
|
||||
TOOLS = request.app.state.TOOLS
|
||||
TOOLS[id] = toolkit_module
|
||||
|
||||
specs = get_tools_specs(TOOLS[id])
|
||||
|
||||
updated = {
|
||||
**form_data.model_dump(exclude={"id"}),
|
||||
"specs": specs,
|
||||
}
|
||||
|
||||
print(updated)
|
||||
toolkit = Tools.update_tool_by_id(id, updated)
|
||||
|
||||
if toolkit:
|
||||
return toolkit
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT("Error updating toolkit"),
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# DeleteToolkitById
|
||||
############################
|
||||
|
||||
|
||||
@router.delete("/id/{id}/delete", response_model=bool)
|
||||
async def delete_toolkit_by_id(request: Request, id: str, user=Depends(get_admin_user)):
|
||||
result = Tools.delete_tool_by_id(id)
|
||||
|
||||
if result:
|
||||
TOOLS = request.app.state.TOOLS
|
||||
if id in TOOLS:
|
||||
del TOOLS[id]
|
||||
|
||||
# delete the toolkit file
|
||||
toolkit_path = os.path.join(TOOLS_DIR, f"{id}.py")
|
||||
os.remove(toolkit_path)
|
||||
|
||||
return result
|
||||
@@ -9,11 +9,22 @@ import time
|
||||
import uuid
|
||||
import logging
|
||||
|
||||
from apps.web.models.users import UserModel, UserUpdateForm, UserRoleUpdateForm, Users
|
||||
from apps.web.models.auths import Auths
|
||||
from apps.web.models.chats import Chats
|
||||
from apps.webui.models.users import (
|
||||
UserModel,
|
||||
UserUpdateForm,
|
||||
UserRoleUpdateForm,
|
||||
UserSettings,
|
||||
Users,
|
||||
)
|
||||
from apps.webui.models.auths import Auths
|
||||
from apps.webui.models.chats import Chats
|
||||
|
||||
from utils.utils import get_verified_user, get_password_hash, get_admin_user
|
||||
from utils.utils import (
|
||||
get_verified_user,
|
||||
get_password_hash,
|
||||
get_current_user,
|
||||
get_admin_user,
|
||||
)
|
||||
from constants import ERROR_MESSAGES
|
||||
|
||||
from config import SRC_LOG_LEVELS
|
||||
@@ -68,6 +79,42 @@ async def update_user_role(form_data: UserRoleUpdateForm, user=Depends(get_admin
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# GetUserSettingsBySessionUser
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/user/settings", response_model=Optional[UserSettings])
|
||||
async def get_user_settings_by_session_user(user=Depends(get_verified_user)):
|
||||
user = Users.get_user_by_id(user.id)
|
||||
if user:
|
||||
return user.settings
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.USER_NOT_FOUND,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# UpdateUserSettingsBySessionUser
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/user/settings/update", response_model=UserSettings)
|
||||
async def update_user_settings_by_session_user(
|
||||
form_data: UserSettings, user=Depends(get_verified_user)
|
||||
):
|
||||
user = Users.update_user_by_id(user.id, {"settings": form_data.model_dump()})
|
||||
if user:
|
||||
return user.settings
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.USER_NOT_FOUND,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# GetUserById
|
||||
############################
|
||||
@@ -81,6 +128,8 @@ class UserResponse(BaseModel):
|
||||
@router.get("/{user_id}", response_model=UserResponse)
|
||||
async def get_user_by_id(user_id: str, user=Depends(get_verified_user)):
|
||||
|
||||
# Check if user_id is a shared chat
|
||||
# If it is, get the user_id from the chat
|
||||
if user_id.startswith("shared-"):
|
||||
chat_id = user_id.replace("shared-", "")
|
||||
chat = Chats.get_chat_by_id(chat_id)
|
||||
@@ -7,8 +7,10 @@ from pydantic import BaseModel
|
||||
|
||||
from fpdf import FPDF
|
||||
import markdown
|
||||
import black
|
||||
|
||||
from apps.web.internal.db import DB
|
||||
|
||||
from apps.webui.internal.db import DB
|
||||
from utils.utils import get_admin_user
|
||||
from utils.misc import calculate_sha256, get_gravatar_url
|
||||
|
||||
@@ -26,6 +28,21 @@ async def get_gravatar(
|
||||
return get_gravatar_url(email)
|
||||
|
||||
|
||||
class CodeFormatRequest(BaseModel):
|
||||
code: str
|
||||
|
||||
|
||||
@router.post("/code/format")
|
||||
async def format_code(request: CodeFormatRequest):
|
||||
try:
|
||||
formatted_code = black.format_str(request.code, mode=black.Mode())
|
||||
return {"code": formatted_code}
|
||||
except black.NothingChanged:
|
||||
return {"code": request.code}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
class MarkdownForm(BaseModel):
|
||||
md: str
|
||||
|
||||
@@ -107,3 +124,12 @@ async def download_db(user=Depends(get_admin_user)):
|
||||
media_type="application/octet-stream",
|
||||
filename="webui.db",
|
||||
)
|
||||
|
||||
|
||||
@router.get("/litellm/config")
|
||||
async def download_litellm_config_yaml(user=Depends(get_admin_user)):
|
||||
return FileResponse(
|
||||
f"{DATA_DIR}/litellm/config.yaml",
|
||||
media_type="application/octet-stream",
|
||||
filename="config.yaml",
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
from importlib import util
|
||||
import os
|
||||
|
||||
from config import TOOLS_DIR
|
||||
|
||||
|
||||
def load_toolkit_module_by_id(toolkit_id):
|
||||
toolkit_path = os.path.join(TOOLS_DIR, f"{toolkit_id}.py")
|
||||
spec = util.spec_from_file_location(toolkit_id, toolkit_path)
|
||||
module = util.module_from_spec(spec)
|
||||
|
||||
try:
|
||||
spec.loader.exec_module(module)
|
||||
print(f"Loaded module: {module.__name__}")
|
||||
if hasattr(module, "Tools"):
|
||||
return module.Tools()
|
||||
else:
|
||||
raise Exception("No Tools class found")
|
||||
except Exception as e:
|
||||
print(f"Error loading module: {toolkit_id}")
|
||||
# Move the file to the error folder
|
||||
os.rename(toolkit_path, f"{toolkit_path}.error")
|
||||
raise e
|
||||
+301
-48
@@ -1,11 +1,15 @@
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import importlib.metadata
|
||||
import pkgutil
|
||||
import chromadb
|
||||
from chromadb import Settings
|
||||
from base64 import b64encode
|
||||
from bs4 import BeautifulSoup
|
||||
from typing import TypeVar, Generic, Union
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
from pathlib import Path
|
||||
import json
|
||||
@@ -22,10 +26,15 @@ from constants import ERROR_MESSAGES
|
||||
# Load .env file
|
||||
####################################
|
||||
|
||||
BACKEND_DIR = Path(__file__).parent # the path containing this file
|
||||
BASE_DIR = BACKEND_DIR.parent # the path containing the backend/
|
||||
|
||||
print(BASE_DIR)
|
||||
|
||||
try:
|
||||
from dotenv import load_dotenv, find_dotenv
|
||||
|
||||
load_dotenv(find_dotenv("../.env"))
|
||||
load_dotenv(find_dotenv(str(BASE_DIR / ".env")))
|
||||
except ImportError:
|
||||
print("dotenv not installed, skipping...")
|
||||
|
||||
@@ -51,7 +60,6 @@ log_sources = [
|
||||
"CONFIG",
|
||||
"DB",
|
||||
"IMAGES",
|
||||
"LITELLM",
|
||||
"MAIN",
|
||||
"MODELS",
|
||||
"OLLAMA",
|
||||
@@ -87,10 +95,12 @@ WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png"
|
||||
ENV = os.environ.get("ENV", "dev")
|
||||
|
||||
try:
|
||||
with open(f"../package.json", "r") as f:
|
||||
PACKAGE_DATA = json.load(f)
|
||||
PACKAGE_DATA = json.loads((BASE_DIR / "package.json").read_text())
|
||||
except:
|
||||
PACKAGE_DATA = {"version": "0.0.0"}
|
||||
try:
|
||||
PACKAGE_DATA = {"version": importlib.metadata.version("open-webui")}
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
PACKAGE_DATA = {"version": "0.0.0"}
|
||||
|
||||
VERSION = PACKAGE_DATA["version"]
|
||||
|
||||
@@ -115,10 +125,13 @@ def parse_section(section):
|
||||
|
||||
|
||||
try:
|
||||
with open("../CHANGELOG.md", "r") as file:
|
||||
changelog_path = BASE_DIR / "CHANGELOG.md"
|
||||
with open(str(changelog_path.absolute()), "r", encoding="utf8") as file:
|
||||
changelog_content = file.read()
|
||||
|
||||
except:
|
||||
changelog_content = ""
|
||||
changelog_content = (pkgutil.get_data("open_webui", "CHANGELOG.md") or b"").decode()
|
||||
|
||||
|
||||
# Convert markdown content to HTML
|
||||
html_content = markdown.markdown(changelog_content)
|
||||
@@ -155,21 +168,31 @@ CHANGELOG = changelog_json
|
||||
|
||||
|
||||
####################################
|
||||
# WEBUI_VERSION
|
||||
# WEBUI_BUILD_HASH
|
||||
####################################
|
||||
|
||||
WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.100")
|
||||
WEBUI_BUILD_HASH = os.environ.get("WEBUI_BUILD_HASH", "dev-build")
|
||||
|
||||
####################################
|
||||
# DATA/FRONTEND BUILD DIR
|
||||
####################################
|
||||
|
||||
DATA_DIR = str(Path(os.getenv("DATA_DIR", "./data")).resolve())
|
||||
FRONTEND_BUILD_DIR = str(Path(os.getenv("FRONTEND_BUILD_DIR", "../build")))
|
||||
DATA_DIR = Path(os.getenv("DATA_DIR", BACKEND_DIR / "data")).resolve()
|
||||
FRONTEND_BUILD_DIR = Path(os.getenv("FRONTEND_BUILD_DIR", BASE_DIR / "build")).resolve()
|
||||
|
||||
RESET_CONFIG_ON_START = (
|
||||
os.environ.get("RESET_CONFIG_ON_START", "False").lower() == "true"
|
||||
)
|
||||
if RESET_CONFIG_ON_START:
|
||||
try:
|
||||
os.remove(f"{DATA_DIR}/config.json")
|
||||
with open(f"{DATA_DIR}/config.json", "w") as f:
|
||||
f.write("{}")
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
with open(f"{DATA_DIR}/config.json", "r") as f:
|
||||
CONFIG_DATA = json.load(f)
|
||||
CONFIG_DATA = json.loads((DATA_DIR / "config.json").read_text())
|
||||
except:
|
||||
CONFIG_DATA = {}
|
||||
|
||||
@@ -279,11 +302,15 @@ JWT_EXPIRES_IN = PersistentConfig(
|
||||
# Static DIR
|
||||
####################################
|
||||
|
||||
STATIC_DIR = str(Path(os.getenv("STATIC_DIR", "./static")).resolve())
|
||||
STATIC_DIR = Path(os.getenv("STATIC_DIR", BACKEND_DIR / "static")).resolve()
|
||||
|
||||
frontend_favicon = FRONTEND_BUILD_DIR / "favicon.png"
|
||||
if frontend_favicon.exists():
|
||||
try:
|
||||
shutil.copyfile(frontend_favicon, STATIC_DIR / "favicon.png")
|
||||
except Exception as e:
|
||||
logging.error(f"An error occurred: {e}")
|
||||
|
||||
frontend_favicon = f"{FRONTEND_BUILD_DIR}/favicon.png"
|
||||
if os.path.exists(frontend_favicon):
|
||||
shutil.copyfile(frontend_favicon, f"{STATIC_DIR}/favicon.png")
|
||||
else:
|
||||
logging.warning(f"Frontend favicon not found at {frontend_favicon}")
|
||||
|
||||
@@ -341,6 +368,14 @@ DOCS_DIR = os.getenv("DOCS_DIR", f"{DATA_DIR}/docs")
|
||||
Path(DOCS_DIR).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
####################################
|
||||
# Tools DIR
|
||||
####################################
|
||||
|
||||
TOOLS_DIR = os.getenv("TOOLS_DIR", f"{DATA_DIR}/tools")
|
||||
Path(TOOLS_DIR).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
####################################
|
||||
# LITELLM_CONFIG
|
||||
####################################
|
||||
@@ -368,16 +403,23 @@ def create_config_file(file_path):
|
||||
|
||||
LITELLM_CONFIG_PATH = f"{DATA_DIR}/litellm/config.yaml"
|
||||
|
||||
if not os.path.exists(LITELLM_CONFIG_PATH):
|
||||
log.info("Config file doesn't exist. Creating...")
|
||||
create_config_file(LITELLM_CONFIG_PATH)
|
||||
log.info("Config file created successfully.")
|
||||
# if not os.path.exists(LITELLM_CONFIG_PATH):
|
||||
# log.info("Config file doesn't exist. Creating...")
|
||||
# create_config_file(LITELLM_CONFIG_PATH)
|
||||
# log.info("Config file created successfully.")
|
||||
|
||||
|
||||
####################################
|
||||
# OLLAMA_BASE_URL
|
||||
####################################
|
||||
|
||||
|
||||
ENABLE_OLLAMA_API = PersistentConfig(
|
||||
"ENABLE_OLLAMA_API",
|
||||
"ollama.enable",
|
||||
os.environ.get("ENABLE_OLLAMA_API", "True").lower() == "true",
|
||||
)
|
||||
|
||||
OLLAMA_API_BASE_URL = os.environ.get(
|
||||
"OLLAMA_API_BASE_URL", "http://localhost:11434/api"
|
||||
)
|
||||
@@ -549,6 +591,114 @@ WEBHOOK_URL = PersistentConfig(
|
||||
|
||||
ENABLE_ADMIN_EXPORT = os.environ.get("ENABLE_ADMIN_EXPORT", "True").lower() == "true"
|
||||
|
||||
ENABLE_COMMUNITY_SHARING = PersistentConfig(
|
||||
"ENABLE_COMMUNITY_SHARING",
|
||||
"ui.enable_community_sharing",
|
||||
os.environ.get("ENABLE_COMMUNITY_SHARING", "True").lower() == "true",
|
||||
)
|
||||
|
||||
|
||||
class BannerModel(BaseModel):
|
||||
id: str
|
||||
type: str
|
||||
title: Optional[str] = None
|
||||
content: str
|
||||
dismissible: bool
|
||||
timestamp: int
|
||||
|
||||
|
||||
WEBUI_BANNERS = PersistentConfig(
|
||||
"WEBUI_BANNERS",
|
||||
"ui.banners",
|
||||
[BannerModel(**banner) for banner in json.loads("[]")],
|
||||
)
|
||||
|
||||
|
||||
SHOW_ADMIN_DETAILS = PersistentConfig(
|
||||
"SHOW_ADMIN_DETAILS",
|
||||
"auth.admin.show",
|
||||
os.environ.get("SHOW_ADMIN_DETAILS", "true").lower() == "true",
|
||||
)
|
||||
|
||||
ADMIN_EMAIL = PersistentConfig(
|
||||
"ADMIN_EMAIL",
|
||||
"auth.admin.email",
|
||||
os.environ.get("ADMIN_EMAIL", None),
|
||||
)
|
||||
|
||||
|
||||
####################################
|
||||
# TASKS
|
||||
####################################
|
||||
|
||||
|
||||
TASK_MODEL = PersistentConfig(
|
||||
"TASK_MODEL",
|
||||
"task.model.default",
|
||||
os.environ.get("TASK_MODEL", ""),
|
||||
)
|
||||
|
||||
TASK_MODEL_EXTERNAL = PersistentConfig(
|
||||
"TASK_MODEL_EXTERNAL",
|
||||
"task.model.external",
|
||||
os.environ.get("TASK_MODEL_EXTERNAL", ""),
|
||||
)
|
||||
|
||||
TITLE_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
|
||||
"TITLE_GENERATION_PROMPT_TEMPLATE",
|
||||
"task.title.prompt_template",
|
||||
os.environ.get(
|
||||
"TITLE_GENERATION_PROMPT_TEMPLATE",
|
||||
"""Here is the query:
|
||||
{{prompt:middletruncate:8000}}
|
||||
|
||||
Create a concise, 3-5 word phrase with an emoji as a title for the previous query. Suitable Emojis for the summary can be used to enhance understanding but avoid quotation marks or special formatting. RESPOND ONLY WITH THE TITLE TEXT.
|
||||
|
||||
Examples of titles:
|
||||
📉 Stock Market Trends
|
||||
🍪 Perfect Chocolate Chip Recipe
|
||||
Evolution of Music Streaming
|
||||
Remote Work Productivity Tips
|
||||
Artificial Intelligence in Healthcare
|
||||
🎮 Video Game Development Insights""",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
|
||||
"SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE",
|
||||
"task.search.prompt_template",
|
||||
os.environ.get(
|
||||
"SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE",
|
||||
"""You are tasked with generating web search queries. Give me an appropriate query to answer my question for google search. Answer with only the query. Today is {{CURRENT_DATE}}.
|
||||
|
||||
Question:
|
||||
{{prompt:end:4000}}""",
|
||||
),
|
||||
)
|
||||
|
||||
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD = PersistentConfig(
|
||||
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD",
|
||||
"task.search.prompt_length_threshold",
|
||||
int(
|
||||
os.environ.get(
|
||||
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD",
|
||||
100,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = PersistentConfig(
|
||||
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE",
|
||||
"task.tools.prompt_template",
|
||||
os.environ.get(
|
||||
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE",
|
||||
"""Tools: {{TOOLS}}
|
||||
If a function tool doesn't match the query, return an empty string. Else, pick a function tool, fill in the parameters from the function tool's schema, and return it in the format { "name": \"functionName\", "parameters": { "key": "value" } }. Only pick a function if the user asks. Only return the object. Do not return any other text.""",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
####################################
|
||||
# WEBUI_SECRET_KEY
|
||||
####################################
|
||||
@@ -631,6 +781,12 @@ RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE = (
|
||||
os.environ.get("RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE", "").lower() == "true"
|
||||
)
|
||||
|
||||
RAG_EMBEDDING_OPENAI_BATCH_SIZE = PersistentConfig(
|
||||
"RAG_EMBEDDING_OPENAI_BATCH_SIZE",
|
||||
"rag.embedding_openai_batch_size",
|
||||
os.environ.get("RAG_EMBEDDING_OPENAI_BATCH_SIZE", 1),
|
||||
)
|
||||
|
||||
RAG_RERANKING_MODEL = PersistentConfig(
|
||||
"RAG_RERANKING_MODEL",
|
||||
"rag.reranking_model",
|
||||
@@ -725,6 +881,81 @@ YOUTUBE_LOADER_LANGUAGE = PersistentConfig(
|
||||
os.getenv("YOUTUBE_LOADER_LANGUAGE", "en").split(","),
|
||||
)
|
||||
|
||||
|
||||
ENABLE_RAG_WEB_SEARCH = PersistentConfig(
|
||||
"ENABLE_RAG_WEB_SEARCH",
|
||||
"rag.web.search.enable",
|
||||
os.getenv("ENABLE_RAG_WEB_SEARCH", "False").lower() == "true",
|
||||
)
|
||||
|
||||
RAG_WEB_SEARCH_ENGINE = PersistentConfig(
|
||||
"RAG_WEB_SEARCH_ENGINE",
|
||||
"rag.web.search.engine",
|
||||
os.getenv("RAG_WEB_SEARCH_ENGINE", ""),
|
||||
)
|
||||
|
||||
SEARXNG_QUERY_URL = PersistentConfig(
|
||||
"SEARXNG_QUERY_URL",
|
||||
"rag.web.search.searxng_query_url",
|
||||
os.getenv("SEARXNG_QUERY_URL", ""),
|
||||
)
|
||||
|
||||
GOOGLE_PSE_API_KEY = PersistentConfig(
|
||||
"GOOGLE_PSE_API_KEY",
|
||||
"rag.web.search.google_pse_api_key",
|
||||
os.getenv("GOOGLE_PSE_API_KEY", ""),
|
||||
)
|
||||
|
||||
GOOGLE_PSE_ENGINE_ID = PersistentConfig(
|
||||
"GOOGLE_PSE_ENGINE_ID",
|
||||
"rag.web.search.google_pse_engine_id",
|
||||
os.getenv("GOOGLE_PSE_ENGINE_ID", ""),
|
||||
)
|
||||
|
||||
BRAVE_SEARCH_API_KEY = PersistentConfig(
|
||||
"BRAVE_SEARCH_API_KEY",
|
||||
"rag.web.search.brave_search_api_key",
|
||||
os.getenv("BRAVE_SEARCH_API_KEY", ""),
|
||||
)
|
||||
|
||||
SERPSTACK_API_KEY = PersistentConfig(
|
||||
"SERPSTACK_API_KEY",
|
||||
"rag.web.search.serpstack_api_key",
|
||||
os.getenv("SERPSTACK_API_KEY", ""),
|
||||
)
|
||||
|
||||
SERPSTACK_HTTPS = PersistentConfig(
|
||||
"SERPSTACK_HTTPS",
|
||||
"rag.web.search.serpstack_https",
|
||||
os.getenv("SERPSTACK_HTTPS", "True").lower() == "true",
|
||||
)
|
||||
|
||||
SERPER_API_KEY = PersistentConfig(
|
||||
"SERPER_API_KEY",
|
||||
"rag.web.search.serper_api_key",
|
||||
os.getenv("SERPER_API_KEY", ""),
|
||||
)
|
||||
|
||||
SERPLY_API_KEY = PersistentConfig(
|
||||
"SERPLY_API_KEY",
|
||||
"rag.web.search.serply_api_key",
|
||||
os.getenv("SERPLY_API_KEY", ""),
|
||||
)
|
||||
|
||||
|
||||
RAG_WEB_SEARCH_RESULT_COUNT = PersistentConfig(
|
||||
"RAG_WEB_SEARCH_RESULT_COUNT",
|
||||
"rag.web.search.result_count",
|
||||
int(os.getenv("RAG_WEB_SEARCH_RESULT_COUNT", "3")),
|
||||
)
|
||||
|
||||
RAG_WEB_SEARCH_CONCURRENT_REQUESTS = PersistentConfig(
|
||||
"RAG_WEB_SEARCH_CONCURRENT_REQUESTS",
|
||||
"rag.web.search.concurrent_requests",
|
||||
int(os.getenv("RAG_WEB_SEARCH_CONCURRENT_REQUESTS", "10")),
|
||||
)
|
||||
|
||||
|
||||
####################################
|
||||
# Transcribe
|
||||
####################################
|
||||
@@ -792,38 +1023,60 @@ IMAGE_GENERATION_MODEL = PersistentConfig(
|
||||
# Audio
|
||||
####################################
|
||||
|
||||
AUDIO_OPENAI_API_BASE_URL = PersistentConfig(
|
||||
"AUDIO_OPENAI_API_BASE_URL",
|
||||
"audio.openai.api_base_url",
|
||||
os.getenv("AUDIO_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
|
||||
)
|
||||
AUDIO_OPENAI_API_KEY = PersistentConfig(
|
||||
"AUDIO_OPENAI_API_KEY",
|
||||
"audio.openai.api_key",
|
||||
os.getenv("AUDIO_OPENAI_API_KEY", OPENAI_API_KEY),
|
||||
)
|
||||
AUDIO_OPENAI_API_MODEL = PersistentConfig(
|
||||
"AUDIO_OPENAI_API_MODEL",
|
||||
"audio.openai.api_model",
|
||||
os.getenv("AUDIO_OPENAI_API_MODEL", "tts-1"),
|
||||
)
|
||||
AUDIO_OPENAI_API_VOICE = PersistentConfig(
|
||||
"AUDIO_OPENAI_API_VOICE",
|
||||
"audio.openai.api_voice",
|
||||
os.getenv("AUDIO_OPENAI_API_VOICE", "alloy"),
|
||||
AUDIO_STT_OPENAI_API_BASE_URL = PersistentConfig(
|
||||
"AUDIO_STT_OPENAI_API_BASE_URL",
|
||||
"audio.stt.openai.api_base_url",
|
||||
os.getenv("AUDIO_STT_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
|
||||
)
|
||||
|
||||
####################################
|
||||
# LiteLLM
|
||||
####################################
|
||||
AUDIO_STT_OPENAI_API_KEY = PersistentConfig(
|
||||
"AUDIO_STT_OPENAI_API_KEY",
|
||||
"audio.stt.openai.api_key",
|
||||
os.getenv("AUDIO_STT_OPENAI_API_KEY", OPENAI_API_KEY),
|
||||
)
|
||||
|
||||
AUDIO_STT_ENGINE = PersistentConfig(
|
||||
"AUDIO_STT_ENGINE",
|
||||
"audio.stt.engine",
|
||||
os.getenv("AUDIO_STT_ENGINE", ""),
|
||||
)
|
||||
|
||||
AUDIO_STT_MODEL = PersistentConfig(
|
||||
"AUDIO_STT_MODEL",
|
||||
"audio.stt.model",
|
||||
os.getenv("AUDIO_STT_MODEL", "whisper-1"),
|
||||
)
|
||||
|
||||
AUDIO_TTS_OPENAI_API_BASE_URL = PersistentConfig(
|
||||
"AUDIO_TTS_OPENAI_API_BASE_URL",
|
||||
"audio.tts.openai.api_base_url",
|
||||
os.getenv("AUDIO_TTS_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
|
||||
)
|
||||
AUDIO_TTS_OPENAI_API_KEY = PersistentConfig(
|
||||
"AUDIO_TTS_OPENAI_API_KEY",
|
||||
"audio.tts.openai.api_key",
|
||||
os.getenv("AUDIO_TTS_OPENAI_API_KEY", OPENAI_API_KEY),
|
||||
)
|
||||
|
||||
|
||||
ENABLE_LITELLM = os.environ.get("ENABLE_LITELLM", "True").lower() == "true"
|
||||
AUDIO_TTS_ENGINE = PersistentConfig(
|
||||
"AUDIO_TTS_ENGINE",
|
||||
"audio.tts.engine",
|
||||
os.getenv("AUDIO_TTS_ENGINE", ""),
|
||||
)
|
||||
|
||||
LITELLM_PROXY_PORT = int(os.getenv("LITELLM_PROXY_PORT", "14365"))
|
||||
if LITELLM_PROXY_PORT < 0 or LITELLM_PROXY_PORT > 65535:
|
||||
raise ValueError("Invalid port number for LITELLM_PROXY_PORT")
|
||||
LITELLM_PROXY_HOST = os.getenv("LITELLM_PROXY_HOST", "127.0.0.1")
|
||||
|
||||
AUDIO_TTS_MODEL = PersistentConfig(
|
||||
"AUDIO_TTS_MODEL",
|
||||
"audio.tts.model",
|
||||
os.getenv("AUDIO_TTS_MODEL", "tts-1"),
|
||||
)
|
||||
|
||||
AUDIO_TTS_VOICE = PersistentConfig(
|
||||
"AUDIO_TTS_VOICE",
|
||||
"audio.tts.voice",
|
||||
os.getenv("AUDIO_TTS_VOICE", "alloy"),
|
||||
)
|
||||
|
||||
|
||||
####################################
|
||||
|
||||
@@ -32,6 +32,9 @@ class ERROR_MESSAGES(str, Enum):
|
||||
COMMAND_TAKEN = "Uh-oh! This command is already registered. Please choose another command string."
|
||||
FILE_EXISTS = "Uh-oh! This file is already registered. Please choose another file."
|
||||
|
||||
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."
|
||||
@@ -78,3 +81,11 @@ class ERROR_MESSAGES(str, Enum):
|
||||
INVALID_URL = (
|
||||
"Oops! The URL you provided is invalid. Please double-check and try again."
|
||||
)
|
||||
|
||||
WEB_SEARCH_ERROR = (
|
||||
lambda err="": f"{err if err else 'Oops! Something went wrong while searching the web.'}"
|
||||
)
|
||||
|
||||
OLLAMA_API_DISABLED = (
|
||||
"The Ollama API is disabled. Please enable it to use this feature."
|
||||
)
|
||||
|
||||
+1074
-60
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,60 @@
|
||||
import base64
|
||||
import os
|
||||
import random
|
||||
from pathlib import Path
|
||||
|
||||
import typer
|
||||
import uvicorn
|
||||
|
||||
app = typer.Typer()
|
||||
|
||||
KEY_FILE = Path.cwd() / ".webui_secret_key"
|
||||
if (frontend_build_dir := Path(__file__).parent / "frontend").exists():
|
||||
os.environ["FRONTEND_BUILD_DIR"] = str(frontend_build_dir)
|
||||
|
||||
|
||||
@app.command()
|
||||
def serve(
|
||||
host: str = "0.0.0.0",
|
||||
port: int = 8080,
|
||||
):
|
||||
if os.getenv("WEBUI_SECRET_KEY") is None:
|
||||
typer.echo(
|
||||
"Loading WEBUI_SECRET_KEY from file, not provided as an environment variable."
|
||||
)
|
||||
if not KEY_FILE.exists():
|
||||
typer.echo(f"Generating a new secret key and saving it to {KEY_FILE}")
|
||||
KEY_FILE.write_bytes(base64.b64encode(random.randbytes(12)))
|
||||
typer.echo(f"Loading WEBUI_SECRET_KEY from {KEY_FILE}")
|
||||
os.environ["WEBUI_SECRET_KEY"] = KEY_FILE.read_text()
|
||||
|
||||
if os.getenv("USE_CUDA_DOCKER", "false") == "true":
|
||||
typer.echo(
|
||||
"CUDA is enabled, appending LD_LIBRARY_PATH to include torch/cudnn & cublas libraries."
|
||||
)
|
||||
LD_LIBRARY_PATH = os.getenv("LD_LIBRARY_PATH", "").split(":")
|
||||
os.environ["LD_LIBRARY_PATH"] = ":".join(
|
||||
LD_LIBRARY_PATH
|
||||
+ [
|
||||
"/usr/local/lib/python3.11/site-packages/torch/lib",
|
||||
"/usr/local/lib/python3.11/site-packages/nvidia/cudnn/lib",
|
||||
]
|
||||
)
|
||||
import main # we need set environment variables before importing main
|
||||
|
||||
uvicorn.run(main.app, host=host, port=port, forwarded_allow_ips="*")
|
||||
|
||||
|
||||
@app.command()
|
||||
def dev(
|
||||
host: str = "0.0.0.0",
|
||||
port: int = 8080,
|
||||
reload: bool = True,
|
||||
):
|
||||
uvicorn.run(
|
||||
"main:app", host=host, port=port, reload=reload, forwarded_allow_ips="*"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
+23
-20
@@ -1,41 +1,40 @@
|
||||
fastapi==0.109.2
|
||||
fastapi==0.111.0
|
||||
uvicorn[standard]==0.22.0
|
||||
pydantic==2.7.1
|
||||
python-multipart==0.0.9
|
||||
|
||||
Flask==3.0.3
|
||||
Flask-Cors==4.0.0
|
||||
Flask-Cors==4.0.1
|
||||
|
||||
python-socketio==5.11.2
|
||||
python-jose==3.3.0
|
||||
passlib[bcrypt]==1.7.4
|
||||
|
||||
requests==2.31.0
|
||||
requests==2.32.2
|
||||
aiohttp==3.9.5
|
||||
peewee==3.17.3
|
||||
peewee==3.17.5
|
||||
peewee-migrate==1.12.2
|
||||
psycopg2-binary==2.9.9
|
||||
PyMySQL==1.1.0
|
||||
bcrypt==4.1.2
|
||||
PyMySQL==1.1.1
|
||||
bcrypt==4.1.3
|
||||
|
||||
litellm[proxy]==1.35.28
|
||||
|
||||
boto3==1.34.95
|
||||
boto3==1.34.110
|
||||
|
||||
argon2-cffi==23.1.0
|
||||
APScheduler==3.10.4
|
||||
google-generativeai==0.5.2
|
||||
google-generativeai==0.5.4
|
||||
|
||||
langchain==0.1.16
|
||||
langchain-community==0.0.34
|
||||
langchain-chroma==0.1.0
|
||||
langchain==0.2.0
|
||||
langchain-community==0.2.0
|
||||
langchain-chroma==0.1.1
|
||||
|
||||
fake-useragent==1.5.1
|
||||
chromadb==0.4.24
|
||||
chromadb==0.5.0
|
||||
sentence-transformers==2.7.0
|
||||
pypdf==4.2.0
|
||||
docx2txt==0.8
|
||||
unstructured==0.11.8
|
||||
python-pptx==0.6.23
|
||||
unstructured==0.14.0
|
||||
Markdown==3.6
|
||||
pypandoc==1.13
|
||||
pandas==2.2.2
|
||||
@@ -45,16 +44,20 @@ xlrd==2.0.1
|
||||
validators==0.28.1
|
||||
|
||||
opencv-python-headless==4.9.0.80
|
||||
rapidocr-onnxruntime==1.2.3
|
||||
rapidocr-onnxruntime==1.3.22
|
||||
|
||||
fpdf2==2.7.8
|
||||
fpdf2==2.7.9
|
||||
rank-bm25==0.2.2
|
||||
|
||||
faster-whisper==1.0.1
|
||||
faster-whisper==1.0.2
|
||||
|
||||
PyJWT[crypto]==2.8.0
|
||||
|
||||
black==24.4.2
|
||||
langfuse==2.27.3
|
||||
langfuse==2.33.0
|
||||
youtube-transcript-api==0.6.2
|
||||
pytube
|
||||
pytube==15.0.0
|
||||
|
||||
extract_msg
|
||||
pydub
|
||||
duckduckgo-search~=6.1.5
|
||||
+27
-2
@@ -20,14 +20,39 @@ if test "$WEBUI_SECRET_KEY $WEBUI_JWT_SECRET_KEY" = " "; then
|
||||
WEBUI_SECRET_KEY=$(cat "$KEY_FILE")
|
||||
fi
|
||||
|
||||
if [ "$USE_OLLAMA_DOCKER" = "true" ]; then
|
||||
if [[ "${USE_OLLAMA_DOCKER,,}" == "true" ]]; then
|
||||
echo "USE_OLLAMA is set to true, starting ollama serve."
|
||||
ollama serve &
|
||||
fi
|
||||
|
||||
if [ "$USE_CUDA_DOCKER" = "true" ]; then
|
||||
if [[ "${USE_CUDA_DOCKER,,}" == "true" ]]; then
|
||||
echo "CUDA is enabled, appending LD_LIBRARY_PATH to include torch/cudnn & cublas libraries."
|
||||
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/local/lib/python3.11/site-packages/torch/lib:/usr/local/lib/python3.11/site-packages/nvidia/cudnn/lib"
|
||||
fi
|
||||
|
||||
|
||||
# Check if SPACE_ID is set, if so, configure for space
|
||||
if [ -n "$SPACE_ID" ]; then
|
||||
echo "Configuring for HuggingFace Space deployment"
|
||||
if [ -n "$ADMIN_USER_EMAIL" ] && [ -n "$ADMIN_USER_PASSWORD" ]; then
|
||||
echo "Admin user configured, creating"
|
||||
WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" uvicorn main:app --host "$HOST" --port "$PORT" --forwarded-allow-ips '*' &
|
||||
webui_pid=$!
|
||||
echo "Waiting for webui to start..."
|
||||
while ! curl -s http://localhost:8080/health > /dev/null; do
|
||||
sleep 1
|
||||
done
|
||||
echo "Creating admin user..."
|
||||
curl \
|
||||
-X POST "http://localhost:8080/api/v1/auths/signup" \
|
||||
-H "accept: application/json" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{ \"email\": \"${ADMIN_USER_EMAIL}\", \"password\": \"${ADMIN_USER_PASSWORD}\", \"name\": \"Admin\" }"
|
||||
echo "Shutting down webui..."
|
||||
kill $webui_pid
|
||||
fi
|
||||
|
||||
export WEBUI_URL=${SPACE_HOST}
|
||||
fi
|
||||
|
||||
WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec uvicorn main:app --host "$HOST" --port "$PORT" --forwarded-allow-ips '*'
|
||||
|
||||
@@ -8,6 +8,7 @@ cd /d "%SCRIPT_DIR%" || exit /b
|
||||
|
||||
SET "KEY_FILE=.webui_secret_key"
|
||||
IF "%PORT%"=="" SET PORT=8080
|
||||
IF "%HOST%"=="" SET HOST=0.0.0.0
|
||||
SET "WEBUI_SECRET_KEY=%WEBUI_SECRET_KEY%"
|
||||
SET "WEBUI_JWT_SECRET_KEY=%WEBUI_JWT_SECRET_KEY%"
|
||||
|
||||
@@ -29,4 +30,4 @@ IF "%WEBUI_SECRET_KEY%%WEBUI_JWT_SECRET_KEY%" == " " (
|
||||
|
||||
:: Execute uvicorn
|
||||
SET "WEBUI_SECRET_KEY=%WEBUI_SECRET_KEY%"
|
||||
uvicorn main:app --host 0.0.0.0 --port "%PORT%" --forwarded-allow-ips '*'
|
||||
uvicorn main:app --host "%HOST%" --port "%PORT%" --forwarded-allow-ips '*'
|
||||
|
||||
+144
-1
@@ -1,8 +1,50 @@
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from datetime import timedelta
|
||||
from typing import Optional
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
def get_last_user_message(messages: List[dict]) -> str:
|
||||
for message in reversed(messages):
|
||||
if message["role"] == "user":
|
||||
if isinstance(message["content"], list):
|
||||
for item in message["content"]:
|
||||
if item["type"] == "text":
|
||||
return item["text"]
|
||||
return message["content"]
|
||||
return None
|
||||
|
||||
|
||||
def get_last_assistant_message(messages: List[dict]) -> str:
|
||||
for message in reversed(messages):
|
||||
if message["role"] == "assistant":
|
||||
if isinstance(message["content"], list):
|
||||
for item in message["content"]:
|
||||
if item["type"] == "text":
|
||||
return item["text"]
|
||||
return message["content"]
|
||||
return None
|
||||
|
||||
|
||||
def add_or_update_system_message(content: str, messages: List[dict]):
|
||||
"""
|
||||
Adds a new system message at the beginning of the messages list
|
||||
or updates the existing system message at the beginning.
|
||||
|
||||
:param msg: The message to be added or appended.
|
||||
:param messages: The list of message dictionaries.
|
||||
:return: The updated list of message dictionaries.
|
||||
"""
|
||||
|
||||
if messages and messages[0].get("role") == "system":
|
||||
messages[0]["content"] += f"{content}\n{messages[0]['content']}"
|
||||
else:
|
||||
# Insert at the beginning
|
||||
messages.insert(0, {"role": "system", "content": content})
|
||||
|
||||
return messages
|
||||
|
||||
|
||||
def get_gravatar_url(email):
|
||||
@@ -110,3 +152,104 @@ def parse_duration(duration: str) -> Optional[timedelta]:
|
||||
total_duration += timedelta(weeks=number)
|
||||
|
||||
return total_duration
|
||||
|
||||
|
||||
def parse_ollama_modelfile(model_text):
|
||||
parameters_meta = {
|
||||
"mirostat": int,
|
||||
"mirostat_eta": float,
|
||||
"mirostat_tau": float,
|
||||
"num_ctx": int,
|
||||
"repeat_last_n": int,
|
||||
"repeat_penalty": float,
|
||||
"temperature": float,
|
||||
"seed": int,
|
||||
"tfs_z": float,
|
||||
"num_predict": int,
|
||||
"top_k": int,
|
||||
"top_p": float,
|
||||
"num_keep": int,
|
||||
"typical_p": float,
|
||||
"presence_penalty": float,
|
||||
"frequency_penalty": float,
|
||||
"penalize_newline": bool,
|
||||
"numa": bool,
|
||||
"num_batch": int,
|
||||
"num_gpu": int,
|
||||
"main_gpu": int,
|
||||
"low_vram": bool,
|
||||
"f16_kv": bool,
|
||||
"vocab_only": bool,
|
||||
"use_mmap": bool,
|
||||
"use_mlock": bool,
|
||||
"num_thread": int,
|
||||
}
|
||||
|
||||
data = {"base_model_id": None, "params": {}}
|
||||
|
||||
# Parse base model
|
||||
base_model_match = re.search(
|
||||
r"^FROM\s+(\w+)", model_text, re.MULTILINE | re.IGNORECASE
|
||||
)
|
||||
if base_model_match:
|
||||
data["base_model_id"] = base_model_match.group(1)
|
||||
|
||||
# Parse template
|
||||
template_match = re.search(
|
||||
r'TEMPLATE\s+"""(.+?)"""', model_text, re.DOTALL | re.IGNORECASE
|
||||
)
|
||||
if template_match:
|
||||
data["params"] = {"template": template_match.group(1).strip()}
|
||||
|
||||
# Parse stops
|
||||
stops = re.findall(r'PARAMETER stop "(.*?)"', model_text, re.IGNORECASE)
|
||||
if stops:
|
||||
data["params"]["stop"] = stops
|
||||
|
||||
# Parse other parameters from the provided list
|
||||
for param, param_type in parameters_meta.items():
|
||||
param_match = re.search(rf"PARAMETER {param} (.+)", model_text, re.IGNORECASE)
|
||||
if param_match:
|
||||
value = param_match.group(1)
|
||||
|
||||
try:
|
||||
if param_type == int:
|
||||
value = int(value)
|
||||
elif param_type == float:
|
||||
value = float(value)
|
||||
elif param_type == bool:
|
||||
value = value.lower() == "true"
|
||||
except Exception as e:
|
||||
print(e)
|
||||
continue
|
||||
|
||||
data["params"][param] = value
|
||||
|
||||
# Parse adapter
|
||||
adapter_match = re.search(r"ADAPTER (.+)", model_text, re.IGNORECASE)
|
||||
if adapter_match:
|
||||
data["params"]["adapter"] = adapter_match.group(1)
|
||||
|
||||
# Parse system description
|
||||
system_desc_match = re.search(
|
||||
r'SYSTEM\s+"""(.+?)"""', model_text, re.DOTALL | re.IGNORECASE
|
||||
)
|
||||
system_desc_match_single = re.search(
|
||||
r"SYSTEM\s+([^\n]+)", model_text, re.IGNORECASE
|
||||
)
|
||||
|
||||
if system_desc_match:
|
||||
data["params"]["system"] = system_desc_match.group(1).strip()
|
||||
elif system_desc_match_single:
|
||||
data["params"]["system"] = system_desc_match_single.group(1).strip()
|
||||
|
||||
# Parse messages
|
||||
messages = []
|
||||
message_matches = re.findall(r"MESSAGE (\w+) (.+)", model_text, re.IGNORECASE)
|
||||
for role, content in message_matches:
|
||||
messages.append({"role": role, "content": content})
|
||||
|
||||
if messages:
|
||||
data["params"]["messages"] = messages
|
||||
|
||||
return data
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import re
|
||||
import math
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def prompt_template(
|
||||
template: str, user_name: str = None, current_location: str = None
|
||||
) -> str:
|
||||
# Get the current date
|
||||
current_date = datetime.now()
|
||||
|
||||
# Format the date to YYYY-MM-DD
|
||||
formatted_date = current_date.strftime("%Y-%m-%d")
|
||||
|
||||
# Replace {{CURRENT_DATE}} in the template with the formatted date
|
||||
template = template.replace("{{CURRENT_DATE}}", formatted_date)
|
||||
|
||||
if user_name:
|
||||
# Replace {{USER_NAME}} in the template with the user's name
|
||||
template = template.replace("{{USER_NAME}}", user_name)
|
||||
|
||||
if current_location:
|
||||
# Replace {{CURRENT_LOCATION}} in the template with the current location
|
||||
template = template.replace("{{CURRENT_LOCATION}}", current_location)
|
||||
|
||||
return template
|
||||
|
||||
|
||||
def title_generation_template(
|
||||
template: str, prompt: str, user: Optional[dict] = None
|
||||
) -> 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
|
||||
elif start_length is not None:
|
||||
return prompt[: int(start_length)]
|
||||
elif end_length is not None:
|
||||
return prompt[-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}"
|
||||
return ""
|
||||
|
||||
template = re.sub(
|
||||
r"{{prompt}}|{{prompt:start:(\d+)}}|{{prompt:end:(\d+)}}|{{prompt:middletruncate:(\d+)}}",
|
||||
replacement_function,
|
||||
template,
|
||||
)
|
||||
|
||||
template = prompt_template(
|
||||
template,
|
||||
**(
|
||||
{"user_name": user.get("name"), "current_location": user.get("location")}
|
||||
if user
|
||||
else {}
|
||||
),
|
||||
)
|
||||
|
||||
return template
|
||||
|
||||
|
||||
def search_query_generation_template(
|
||||
template: str, prompt: str, user: Optional[dict] = None
|
||||
) -> 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
|
||||
elif start_length is not None:
|
||||
return prompt[: int(start_length)]
|
||||
elif end_length is not None:
|
||||
return prompt[-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}"
|
||||
return ""
|
||||
|
||||
template = re.sub(
|
||||
r"{{prompt}}|{{prompt:start:(\d+)}}|{{prompt:end:(\d+)}}|{{prompt:middletruncate:(\d+)}}",
|
||||
replacement_function,
|
||||
template,
|
||||
)
|
||||
|
||||
template = prompt_template(
|
||||
template,
|
||||
**(
|
||||
{"user_name": user.get("name"), "current_location": user.get("location")}
|
||||
if user
|
||||
else {}
|
||||
),
|
||||
)
|
||||
return template
|
||||
|
||||
|
||||
def tools_function_calling_generation_template(template: str, tools_specs: str) -> str:
|
||||
template = template.replace("{{TOOLS}}", tools_specs)
|
||||
return template
|
||||
@@ -0,0 +1,73 @@
|
||||
import inspect
|
||||
from typing import get_type_hints, List, Dict, Any
|
||||
|
||||
|
||||
def doc_to_dict(docstring):
|
||||
lines = docstring.split("\n")
|
||||
description = lines[1].strip()
|
||||
param_dict = {}
|
||||
|
||||
for line in lines:
|
||||
if ":param" in line:
|
||||
line = line.replace(":param", "").strip()
|
||||
param, desc = line.split(":", 1)
|
||||
param_dict[param.strip()] = desc.strip()
|
||||
ret_dict = {"description": description, "params": param_dict}
|
||||
return ret_dict
|
||||
|
||||
|
||||
def get_tools_specs(tools) -> List[dict]:
|
||||
function_list = [
|
||||
{"name": func, "function": getattr(tools, func)}
|
||||
for func in dir(tools)
|
||||
if callable(getattr(tools, func)) and not func.startswith("__")
|
||||
]
|
||||
|
||||
specs = []
|
||||
for function_item in function_list:
|
||||
function_name = function_item["name"]
|
||||
function = function_item["function"]
|
||||
|
||||
function_doc = doc_to_dict(function.__doc__ or function_name)
|
||||
specs.append(
|
||||
{
|
||||
"name": function_name,
|
||||
# TODO: multi-line desc?
|
||||
"description": function_doc.get("description", function_name),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
param_name: {
|
||||
"type": param_annotation.__name__.lower(),
|
||||
**(
|
||||
{
|
||||
"enum": (
|
||||
str(param_annotation.__args__)
|
||||
if hasattr(param_annotation, "__args__")
|
||||
else None
|
||||
)
|
||||
}
|
||||
if hasattr(param_annotation, "__args__")
|
||||
else {}
|
||||
),
|
||||
"description": function_doc.get("params", {}).get(
|
||||
param_name, param_name
|
||||
),
|
||||
}
|
||||
for param_name, param_annotation in get_type_hints(
|
||||
function
|
||||
).items()
|
||||
if param_name != "return" and param_name != "__user__"
|
||||
},
|
||||
"required": [
|
||||
name
|
||||
for name, param in inspect.signature(
|
||||
function
|
||||
).parameters.items()
|
||||
if param.default is param.empty
|
||||
],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return specs
|
||||
@@ -1,7 +1,7 @@
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from fastapi import HTTPException, status, Depends
|
||||
|
||||
from apps.web.models.users import Users
|
||||
from apps.webui.models.users import Users
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import Union, Optional
|
||||
|
||||
@@ -74,5 +74,28 @@ describe('Settings', () => {
|
||||
expect(spy).to.be.callCount(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('user can generate image', () => {
|
||||
// Click on the model selector
|
||||
cy.get('button[aria-label="Select a model"]').click();
|
||||
// Select the first model
|
||||
cy.get('button[aria-label="model-item"]').first().click();
|
||||
// Type a message
|
||||
cy.get('#chat-textarea').type('Hi, what can you do? A single sentence only please.', {
|
||||
force: true
|
||||
});
|
||||
// Send the message
|
||||
cy.get('button[type="submit"]').click();
|
||||
// User's message should be visible
|
||||
cy.get('.chat-user').should('exist');
|
||||
// Wait for the response
|
||||
cy.get('.chat-assistant', { timeout: 120_000 }) // .chat-assistant is created after the first token is received
|
||||
.find('div[aria-label="Generation Info"]', { timeout: 120_000 }) // Generation Info is created after the stop token is received
|
||||
.should('exist');
|
||||
// Click on the generate image button
|
||||
cy.get('[aria-label="Generate Image"]').click();
|
||||
// Wait for image to be visible
|
||||
cy.get('img[data-cy="image"]', { timeout: 60_000 }).should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,19 +28,6 @@ describe('Settings', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('Connections', () => {
|
||||
it('user can open the Connections modal and hit save', () => {
|
||||
cy.get('button').contains('Connections').click();
|
||||
cy.get('button').contains('Save').click();
|
||||
});
|
||||
});
|
||||
|
||||
context('Models', () => {
|
||||
it('user can open the Models modal', () => {
|
||||
cy.get('button').contains('Models').click();
|
||||
});
|
||||
});
|
||||
|
||||
context('Interface', () => {
|
||||
it('user can open the Interface modal and hit save', () => {
|
||||
cy.get('button').contains('Interface').click();
|
||||
@@ -55,14 +42,6 @@ describe('Settings', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('Images', () => {
|
||||
it('user can open the Images modal and hit save', () => {
|
||||
cy.get('button').contains('Images').click();
|
||||
// Currently fails because the backend requires a valid URL
|
||||
// cy.get('button').contains('Save').click();
|
||||
});
|
||||
});
|
||||
|
||||
context('Chats', () => {
|
||||
it('user can open the Chats modal', () => {
|
||||
cy.get('button').contains('Chats').click();
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.0 MiB After Width: | Height: | Size: 4.1 MiB |
@@ -0,0 +1,31 @@
|
||||
# This is an overlay that spins up stable-diffusion-webui for integration testing
|
||||
# This is not designed to be used in production
|
||||
services:
|
||||
stable-diffusion-webui:
|
||||
# Not built for ARM64
|
||||
platform: linux/amd64
|
||||
image: ghcr.io/neggles/sd-webui-docker:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
CLI_ARGS: "--api --use-cpu all --precision full --no-half --skip-torch-cuda-test --ckpt /empty.pt --do-not-download-clip --disable-nan-check --disable-opt-split-attention"
|
||||
PYTHONUNBUFFERED: "1"
|
||||
TERM: "vt100"
|
||||
SD_WEBUI_VARIANT: "default"
|
||||
# Hack to get container working on Apple Silicon
|
||||
# Rosetta creates a conflict ${HOME}/.cache folder
|
||||
entrypoint: /bin/bash
|
||||
command:
|
||||
- -c
|
||||
- |
|
||||
export HOME=/root-home
|
||||
rm -rf $${HOME}/.cache
|
||||
/docker/entrypoint.sh python -u webui.py --listen --port $${WEBUI_PORT} --skip-version-check $${CLI_ARGS}
|
||||
volumes:
|
||||
- ./test/test_files/image_gen/sd-empty.pt:/empty.pt
|
||||
|
||||
open-webui:
|
||||
environment:
|
||||
ENABLE_IMAGE_GENERATION: "true"
|
||||
AUTOMATIC1111_BASE_URL: http://stable-diffusion-webui:7860
|
||||
IMAGE_SIZE: "64x64"
|
||||
IMAGE_STEPS: "3"
|
||||
@@ -41,10 +41,11 @@ Looking to contribute? Great! Here's how you can help:
|
||||
|
||||
We welcome pull requests. Before submitting one, please:
|
||||
|
||||
1. Discuss your idea or issue in the [issues section](https://github.com/open-webui/open-webui/issues).
|
||||
1. Open a discussion regarding your ideas [here](https://github.com/open-webui/open-webui/discussions/new/choose).
|
||||
2. Follow the project's coding standards and include tests for new features.
|
||||
3. Update documentation as necessary.
|
||||
4. Write clear, descriptive commit messages.
|
||||
5. It's essential to complete your pull request in a timely manner. We move fast, and having PRs hang around too long is not feasible. If you can't get it done within a reasonable time frame, we may have to close it to keep the project moving forward.
|
||||
|
||||
### 📚 Documentation & Tutorials
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# noqa: INP001
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from sys import stderr
|
||||
|
||||
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
||||
|
||||
|
||||
class CustomBuildHook(BuildHookInterface):
|
||||
def initialize(self, version, build_data):
|
||||
super().initialize(version, build_data)
|
||||
stderr.write(">>> Building Open Webui frontend\n")
|
||||
npm = shutil.which("npm")
|
||||
if npm is None:
|
||||
raise RuntimeError(
|
||||
"NodeJS `npm` is required for building Open Webui but it was not found"
|
||||
)
|
||||
stderr.write("### npm install\n")
|
||||
subprocess.run([npm, "install"], check=True) # noqa: S603
|
||||
stderr.write("\n### npm run build\n")
|
||||
os.environ["APP_BUILD_HASH"] = version
|
||||
subprocess.run([npm, "run", "build"], check=True) # noqa: S603
|
||||
Generated
+1397
-12
File diff suppressed because it is too large
Load Diff
+10
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "open-webui",
|
||||
"version": "0.1.125",
|
||||
"version": "0.3.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "npm run pyodide:fetch && vite dev --host",
|
||||
@@ -13,10 +13,10 @@
|
||||
"lint:types": "npm run check",
|
||||
"lint:backend": "pylint backend/",
|
||||
"format": "prettier --plugin-search-dir --write \"**/*.{js,ts,svelte,css,md,html,json}\"",
|
||||
"format:backend": "black . --exclude \"/venv/\"",
|
||||
"format:backend": "black . --exclude \".venv/|/venv/\"",
|
||||
"i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write \"src/lib/i18n/**/*.{js,json}\"",
|
||||
"cy:open": "cypress open",
|
||||
"test:frontend": "vitest",
|
||||
"test:frontend": "vitest --passWithNoTests",
|
||||
"pyodide:fetch": "node scripts/prepare-pyodide.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -48,10 +48,14 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-javascript": "^6.2.2",
|
||||
"@codemirror/lang-python": "^6.1.6",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@pyscript/core": "^0.4.32",
|
||||
"@sveltejs/adapter-node": "^1.3.1",
|
||||
"async": "^3.2.5",
|
||||
"bits-ui": "^0.19.7",
|
||||
"codemirror": "^6.0.1",
|
||||
"dayjs": "^1.11.10",
|
||||
"eventsource-parser": "^1.1.2",
|
||||
"file-saver": "^2.0.5",
|
||||
@@ -63,7 +67,10 @@
|
||||
"js-sha256": "^0.10.1",
|
||||
"katex": "^0.16.9",
|
||||
"marked": "^9.1.0",
|
||||
"mermaid": "^10.9.1",
|
||||
"pyodide": "^0.26.0-alpha.4",
|
||||
"socket.io-client": "^4.7.5",
|
||||
"sortablejs": "^1.15.2",
|
||||
"svelte-sonner": "^0.3.19",
|
||||
"tippy.js": "^6.3.7",
|
||||
"uuid": "^9.0.1"
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
[project]
|
||||
name = "open-webui"
|
||||
description = "Open WebUI (Formerly Ollama WebUI)"
|
||||
authors = [
|
||||
{ name = "Timothy Jaeryang Baek", email = "tim@openwebui.com" }
|
||||
]
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = [
|
||||
"fastapi==0.111.0",
|
||||
"uvicorn[standard]==0.22.0",
|
||||
"pydantic==2.7.1",
|
||||
"python-multipart==0.0.9",
|
||||
|
||||
"Flask==3.0.3",
|
||||
"Flask-Cors==4.0.1",
|
||||
|
||||
"python-socketio==5.11.2",
|
||||
"python-jose==3.3.0",
|
||||
"passlib[bcrypt]==1.7.4",
|
||||
|
||||
"requests==2.32.2",
|
||||
"aiohttp==3.9.5",
|
||||
"peewee==3.17.5",
|
||||
"peewee-migrate==1.12.2",
|
||||
"psycopg2-binary==2.9.9",
|
||||
"PyMySQL==1.1.0",
|
||||
"bcrypt==4.1.3",
|
||||
|
||||
"boto3==1.34.110",
|
||||
|
||||
"argon2-cffi==23.1.0",
|
||||
"APScheduler==3.10.4",
|
||||
"google-generativeai==0.5.4",
|
||||
|
||||
"langchain==0.2.0",
|
||||
"langchain-community==0.2.0",
|
||||
"langchain-chroma==0.1.1",
|
||||
|
||||
"fake-useragent==1.5.1",
|
||||
"chromadb==0.5.0",
|
||||
"sentence-transformers==2.7.0",
|
||||
"pypdf==4.2.0",
|
||||
"docx2txt==0.8",
|
||||
"unstructured==0.14.0",
|
||||
"Markdown==3.6",
|
||||
"pypandoc==1.13",
|
||||
"pandas==2.2.2",
|
||||
"openpyxl==3.1.2",
|
||||
"pyxlsb==1.0.10",
|
||||
"xlrd==2.0.1",
|
||||
"validators==0.28.1",
|
||||
|
||||
"opencv-python-headless==4.9.0.80",
|
||||
"rapidocr-onnxruntime==1.3.22",
|
||||
|
||||
"fpdf2==2.7.9",
|
||||
"rank-bm25==0.2.2",
|
||||
|
||||
"faster-whisper==1.0.2",
|
||||
|
||||
"PyJWT[crypto]==2.8.0",
|
||||
|
||||
"black==24.4.2",
|
||||
"langfuse==2.33.0",
|
||||
"youtube-transcript-api==0.6.2",
|
||||
"pytube==15.0.0",
|
||||
"extract_msg",
|
||||
"pydub",
|
||||
"duckduckgo-search~=6.1.5"
|
||||
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = ">= 3.11, < 3.12.0a1"
|
||||
dynamic = ["version"]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Topic :: Communications :: Chat",
|
||||
"Topic :: Multimedia",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
open-webui = "open_webui:app"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.rye]
|
||||
managed = true
|
||||
dev-dependencies = []
|
||||
|
||||
[tool.hatch.metadata]
|
||||
allow-direct-references = true
|
||||
|
||||
[tool.hatch.version]
|
||||
path = "package.json"
|
||||
pattern = '"version":\s*"(?P<version>[^"]+)"'
|
||||
|
||||
[tool.hatch.build.hooks.custom] # keep this for reading hooks from `hatch_build.py`
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
sources = ["backend"]
|
||||
exclude = [
|
||||
".dockerignore",
|
||||
".gitignore",
|
||||
".webui_secret_key",
|
||||
"dev.sh",
|
||||
"requirements.txt",
|
||||
"start.sh",
|
||||
"start_windows.bat",
|
||||
"webui.db",
|
||||
"chroma.sqlite3",
|
||||
]
|
||||
force-include = { "CHANGELOG.md" = "open_webui/CHANGELOG.md", build = "open_webui/frontend" }
|
||||
@@ -0,0 +1,680 @@
|
||||
# generated by rye
|
||||
# use `rye lock` or `rye sync` to update this lockfile
|
||||
#
|
||||
# last locked with the following flags:
|
||||
# pre: false
|
||||
# features: []
|
||||
# all-features: false
|
||||
# with-sources: false
|
||||
# generate-hashes: false
|
||||
|
||||
-e file:.
|
||||
aiohttp==3.9.5
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via open-webui
|
||||
aiosignal==1.3.1
|
||||
# via aiohttp
|
||||
annotated-types==0.6.0
|
||||
# via pydantic
|
||||
anyio==4.3.0
|
||||
# via httpx
|
||||
# via starlette
|
||||
# via watchfiles
|
||||
apscheduler==3.10.4
|
||||
# via open-webui
|
||||
argon2-cffi==23.1.0
|
||||
# via open-webui
|
||||
argon2-cffi-bindings==21.2.0
|
||||
# via argon2-cffi
|
||||
asgiref==3.8.1
|
||||
# via opentelemetry-instrumentation-asgi
|
||||
attrs==23.2.0
|
||||
# via aiohttp
|
||||
av==11.0.0
|
||||
# via faster-whisper
|
||||
backoff==2.2.1
|
||||
# via langfuse
|
||||
# via posthog
|
||||
# via unstructured
|
||||
bcrypt==4.1.3
|
||||
# via chromadb
|
||||
# via open-webui
|
||||
# via passlib
|
||||
beautifulsoup4==4.12.3
|
||||
# via extract-msg
|
||||
# via unstructured
|
||||
bidict==0.23.1
|
||||
# via python-socketio
|
||||
black==24.4.2
|
||||
# via open-webui
|
||||
blinker==1.8.2
|
||||
# via flask
|
||||
boto3==1.34.110
|
||||
# via open-webui
|
||||
botocore==1.34.110
|
||||
# via boto3
|
||||
# via s3transfer
|
||||
build==1.2.1
|
||||
# via chromadb
|
||||
cachetools==5.3.3
|
||||
# via google-auth
|
||||
certifi==2024.2.2
|
||||
# via httpcore
|
||||
# via httpx
|
||||
# via kubernetes
|
||||
# via requests
|
||||
# via unstructured-client
|
||||
cffi==1.16.0
|
||||
# via argon2-cffi-bindings
|
||||
# via cryptography
|
||||
chardet==5.2.0
|
||||
# via unstructured
|
||||
charset-normalizer==3.3.2
|
||||
# via requests
|
||||
# via unstructured-client
|
||||
chroma-hnswlib==0.7.3
|
||||
# via chromadb
|
||||
chromadb==0.5.0
|
||||
# via langchain-chroma
|
||||
# via open-webui
|
||||
click==8.1.7
|
||||
# via black
|
||||
# via duckduckgo-search
|
||||
# via flask
|
||||
# via nltk
|
||||
# via peewee-migrate
|
||||
# via typer
|
||||
# via uvicorn
|
||||
colorclass==2.2.2
|
||||
# via oletools
|
||||
coloredlogs==15.0.1
|
||||
# via onnxruntime
|
||||
compressed-rtf==1.0.6
|
||||
# via extract-msg
|
||||
cryptography==42.0.7
|
||||
# via msoffcrypto-tool
|
||||
# via pyjwt
|
||||
ctranslate2==4.2.1
|
||||
# via faster-whisper
|
||||
dataclasses-json==0.6.6
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via unstructured
|
||||
# via unstructured-client
|
||||
deepdiff==7.0.1
|
||||
# via unstructured-client
|
||||
defusedxml==0.7.1
|
||||
# via fpdf2
|
||||
deprecated==1.2.14
|
||||
# via opentelemetry-api
|
||||
# via opentelemetry-exporter-otlp-proto-grpc
|
||||
dnspython==2.6.1
|
||||
# via email-validator
|
||||
docx2txt==0.8
|
||||
# via open-webui
|
||||
duckduckgo-search==6.1.5
|
||||
# via open-webui
|
||||
easygui==0.98.3
|
||||
# via oletools
|
||||
ebcdic==1.1.1
|
||||
# via extract-msg
|
||||
ecdsa==0.19.0
|
||||
# via python-jose
|
||||
email-validator==2.1.1
|
||||
# via fastapi
|
||||
emoji==2.11.1
|
||||
# via unstructured
|
||||
et-xmlfile==1.1.0
|
||||
# via openpyxl
|
||||
extract-msg==0.48.5
|
||||
# via open-webui
|
||||
fake-useragent==1.5.1
|
||||
# via open-webui
|
||||
fastapi==0.111.0
|
||||
# via chromadb
|
||||
# via langchain-chroma
|
||||
# via open-webui
|
||||
fastapi-cli==0.0.4
|
||||
# via fastapi
|
||||
faster-whisper==1.0.2
|
||||
# via open-webui
|
||||
filelock==3.14.0
|
||||
# via huggingface-hub
|
||||
# via torch
|
||||
# via transformers
|
||||
filetype==1.2.0
|
||||
# via unstructured
|
||||
flask==3.0.3
|
||||
# via flask-cors
|
||||
# via open-webui
|
||||
flask-cors==4.0.1
|
||||
# via open-webui
|
||||
flatbuffers==24.3.25
|
||||
# via onnxruntime
|
||||
fonttools==4.51.0
|
||||
# via fpdf2
|
||||
fpdf2==2.7.9
|
||||
# via open-webui
|
||||
frozenlist==1.4.1
|
||||
# via aiohttp
|
||||
# via aiosignal
|
||||
fsspec==2024.3.1
|
||||
# via huggingface-hub
|
||||
# via torch
|
||||
google-ai-generativelanguage==0.6.4
|
||||
# via google-generativeai
|
||||
google-api-core==2.19.0
|
||||
# via google-ai-generativelanguage
|
||||
# via google-api-python-client
|
||||
# via google-generativeai
|
||||
google-api-python-client==2.129.0
|
||||
# via google-generativeai
|
||||
google-auth==2.29.0
|
||||
# via google-ai-generativelanguage
|
||||
# via google-api-core
|
||||
# via google-api-python-client
|
||||
# via google-auth-httplib2
|
||||
# via google-generativeai
|
||||
# via kubernetes
|
||||
google-auth-httplib2==0.2.0
|
||||
# via google-api-python-client
|
||||
google-generativeai==0.5.4
|
||||
# via open-webui
|
||||
googleapis-common-protos==1.63.0
|
||||
# via google-api-core
|
||||
# via grpcio-status
|
||||
# via opentelemetry-exporter-otlp-proto-grpc
|
||||
grpcio==1.63.0
|
||||
# via chromadb
|
||||
# via google-api-core
|
||||
# via grpcio-status
|
||||
# via opentelemetry-exporter-otlp-proto-grpc
|
||||
grpcio-status==1.62.2
|
||||
# via google-api-core
|
||||
h11==0.14.0
|
||||
# via httpcore
|
||||
# via uvicorn
|
||||
# via wsproto
|
||||
httpcore==1.0.5
|
||||
# via httpx
|
||||
httplib2==0.22.0
|
||||
# via google-api-python-client
|
||||
# via google-auth-httplib2
|
||||
httptools==0.6.1
|
||||
# via uvicorn
|
||||
httpx==0.27.0
|
||||
# via fastapi
|
||||
# via langfuse
|
||||
huggingface-hub==0.23.0
|
||||
# via faster-whisper
|
||||
# via sentence-transformers
|
||||
# via tokenizers
|
||||
# via transformers
|
||||
humanfriendly==10.0
|
||||
# via coloredlogs
|
||||
idna==3.7
|
||||
# via anyio
|
||||
# via email-validator
|
||||
# via httpx
|
||||
# via langfuse
|
||||
# via requests
|
||||
# via unstructured-client
|
||||
# via yarl
|
||||
importlib-metadata==7.0.0
|
||||
# via opentelemetry-api
|
||||
importlib-resources==6.4.0
|
||||
# via chromadb
|
||||
itsdangerous==2.2.0
|
||||
# via flask
|
||||
jinja2==3.1.4
|
||||
# via fastapi
|
||||
# via flask
|
||||
# via torch
|
||||
jmespath==1.0.1
|
||||
# via boto3
|
||||
# via botocore
|
||||
joblib==1.4.2
|
||||
# via nltk
|
||||
# via scikit-learn
|
||||
jsonpatch==1.33
|
||||
# via langchain-core
|
||||
jsonpath-python==1.0.6
|
||||
# via unstructured-client
|
||||
jsonpointer==2.4
|
||||
# via jsonpatch
|
||||
kubernetes==29.0.0
|
||||
# via chromadb
|
||||
langchain==0.2.0
|
||||
# via langchain-community
|
||||
# via open-webui
|
||||
langchain-chroma==0.1.1
|
||||
# via open-webui
|
||||
langchain-community==0.2.0
|
||||
# via open-webui
|
||||
langchain-core==0.2.1
|
||||
# via langchain
|
||||
# via langchain-chroma
|
||||
# via langchain-community
|
||||
# via langchain-text-splitters
|
||||
langchain-text-splitters==0.2.0
|
||||
# via langchain
|
||||
langdetect==1.0.9
|
||||
# via unstructured
|
||||
langfuse==2.33.0
|
||||
# via open-webui
|
||||
langsmith==0.1.57
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via langchain-core
|
||||
lark==1.1.8
|
||||
# via rtfde
|
||||
lxml==5.2.2
|
||||
# via unstructured
|
||||
markdown==3.6
|
||||
# via open-webui
|
||||
markdown-it-py==3.0.0
|
||||
# via rich
|
||||
markupsafe==2.1.5
|
||||
# via jinja2
|
||||
# via werkzeug
|
||||
marshmallow==3.21.2
|
||||
# via dataclasses-json
|
||||
# via unstructured-client
|
||||
mdurl==0.1.2
|
||||
# via markdown-it-py
|
||||
mmh3==4.1.0
|
||||
# via chromadb
|
||||
monotonic==1.6
|
||||
# via posthog
|
||||
mpmath==1.3.0
|
||||
# via sympy
|
||||
msoffcrypto-tool==5.4.1
|
||||
# via oletools
|
||||
multidict==6.0.5
|
||||
# via aiohttp
|
||||
# via yarl
|
||||
mypy-extensions==1.0.0
|
||||
# via black
|
||||
# via typing-inspect
|
||||
# via unstructured-client
|
||||
networkx==3.3
|
||||
# via torch
|
||||
nltk==3.8.1
|
||||
# via unstructured
|
||||
numpy==1.26.4
|
||||
# via chroma-hnswlib
|
||||
# via chromadb
|
||||
# via ctranslate2
|
||||
# via langchain
|
||||
# via langchain-chroma
|
||||
# via langchain-community
|
||||
# via onnxruntime
|
||||
# via opencv-python
|
||||
# via opencv-python-headless
|
||||
# via pandas
|
||||
# via rank-bm25
|
||||
# via rapidocr-onnxruntime
|
||||
# via scikit-learn
|
||||
# via scipy
|
||||
# via sentence-transformers
|
||||
# via shapely
|
||||
# via transformers
|
||||
# via unstructured
|
||||
oauthlib==3.2.2
|
||||
# via kubernetes
|
||||
# via requests-oauthlib
|
||||
olefile==0.47
|
||||
# via extract-msg
|
||||
# via msoffcrypto-tool
|
||||
# via oletools
|
||||
oletools==0.60.1
|
||||
# via pcodedmp
|
||||
# via rtfde
|
||||
onnxruntime==1.17.3
|
||||
# via chromadb
|
||||
# via faster-whisper
|
||||
# via rapidocr-onnxruntime
|
||||
opencv-python==4.9.0.80
|
||||
# via rapidocr-onnxruntime
|
||||
opencv-python-headless==4.9.0.80
|
||||
# via open-webui
|
||||
openpyxl==3.1.2
|
||||
# via open-webui
|
||||
opentelemetry-api==1.24.0
|
||||
# via chromadb
|
||||
# via opentelemetry-exporter-otlp-proto-grpc
|
||||
# via opentelemetry-instrumentation
|
||||
# via opentelemetry-instrumentation-asgi
|
||||
# via opentelemetry-instrumentation-fastapi
|
||||
# via opentelemetry-sdk
|
||||
opentelemetry-exporter-otlp-proto-common==1.24.0
|
||||
# via opentelemetry-exporter-otlp-proto-grpc
|
||||
opentelemetry-exporter-otlp-proto-grpc==1.24.0
|
||||
# via chromadb
|
||||
opentelemetry-instrumentation==0.45b0
|
||||
# via opentelemetry-instrumentation-asgi
|
||||
# via opentelemetry-instrumentation-fastapi
|
||||
opentelemetry-instrumentation-asgi==0.45b0
|
||||
# via opentelemetry-instrumentation-fastapi
|
||||
opentelemetry-instrumentation-fastapi==0.45b0
|
||||
# via chromadb
|
||||
opentelemetry-proto==1.24.0
|
||||
# via opentelemetry-exporter-otlp-proto-common
|
||||
# via opentelemetry-exporter-otlp-proto-grpc
|
||||
opentelemetry-sdk==1.24.0
|
||||
# via chromadb
|
||||
# via opentelemetry-exporter-otlp-proto-grpc
|
||||
opentelemetry-semantic-conventions==0.45b0
|
||||
# via opentelemetry-instrumentation-asgi
|
||||
# via opentelemetry-instrumentation-fastapi
|
||||
# via opentelemetry-sdk
|
||||
opentelemetry-util-http==0.45b0
|
||||
# via opentelemetry-instrumentation-asgi
|
||||
# via opentelemetry-instrumentation-fastapi
|
||||
ordered-set==4.1.0
|
||||
# via deepdiff
|
||||
orjson==3.10.3
|
||||
# via chromadb
|
||||
# via duckduckgo-search
|
||||
# via fastapi
|
||||
# via langsmith
|
||||
overrides==7.7.0
|
||||
# via chromadb
|
||||
packaging==23.2
|
||||
# via black
|
||||
# via build
|
||||
# via huggingface-hub
|
||||
# via langchain-core
|
||||
# via langfuse
|
||||
# via marshmallow
|
||||
# via onnxruntime
|
||||
# via transformers
|
||||
# via unstructured-client
|
||||
pandas==2.2.2
|
||||
# via open-webui
|
||||
passlib==1.7.4
|
||||
# via open-webui
|
||||
pathspec==0.12.1
|
||||
# via black
|
||||
pcodedmp==1.2.6
|
||||
# via oletools
|
||||
peewee==3.17.5
|
||||
# via open-webui
|
||||
# via peewee-migrate
|
||||
peewee-migrate==1.12.2
|
||||
# via open-webui
|
||||
pillow==10.3.0
|
||||
# via fpdf2
|
||||
# via rapidocr-onnxruntime
|
||||
# via sentence-transformers
|
||||
platformdirs==4.2.1
|
||||
# via black
|
||||
posthog==3.5.0
|
||||
# via chromadb
|
||||
proto-plus==1.23.0
|
||||
# via google-ai-generativelanguage
|
||||
# via google-api-core
|
||||
protobuf==4.25.3
|
||||
# via google-ai-generativelanguage
|
||||
# via google-api-core
|
||||
# via google-generativeai
|
||||
# via googleapis-common-protos
|
||||
# via grpcio-status
|
||||
# via onnxruntime
|
||||
# via opentelemetry-proto
|
||||
# via proto-plus
|
||||
psycopg2-binary==2.9.9
|
||||
# via open-webui
|
||||
pyasn1==0.6.0
|
||||
# via pyasn1-modules
|
||||
# via python-jose
|
||||
# via rsa
|
||||
pyasn1-modules==0.4.0
|
||||
# via google-auth
|
||||
pyclipper==1.3.0.post5
|
||||
# via rapidocr-onnxruntime
|
||||
pycparser==2.22
|
||||
# via cffi
|
||||
pydantic==2.7.1
|
||||
# via chromadb
|
||||
# via fastapi
|
||||
# via google-generativeai
|
||||
# via langchain
|
||||
# via langchain-core
|
||||
# via langfuse
|
||||
# via langsmith
|
||||
# via open-webui
|
||||
pydantic-core==2.18.2
|
||||
# via pydantic
|
||||
pydub==0.25.1
|
||||
# via open-webui
|
||||
pygments==2.18.0
|
||||
# via rich
|
||||
pyjwt==2.8.0
|
||||
# via open-webui
|
||||
pymysql==1.1.0
|
||||
# via open-webui
|
||||
pypandoc==1.13
|
||||
# via open-webui
|
||||
pyparsing==2.4.7
|
||||
# via httplib2
|
||||
# via oletools
|
||||
pypdf==4.2.0
|
||||
# via open-webui
|
||||
# via unstructured-client
|
||||
pypika==0.48.9
|
||||
# via chromadb
|
||||
pyproject-hooks==1.1.0
|
||||
# via build
|
||||
pyreqwest-impersonate==0.4.7
|
||||
# via duckduckgo-search
|
||||
python-dateutil==2.9.0.post0
|
||||
# via botocore
|
||||
# via kubernetes
|
||||
# via pandas
|
||||
# via posthog
|
||||
# via unstructured-client
|
||||
python-dotenv==1.0.1
|
||||
# via uvicorn
|
||||
python-engineio==4.9.0
|
||||
# via python-socketio
|
||||
python-iso639==2024.4.27
|
||||
# via unstructured
|
||||
python-jose==3.3.0
|
||||
# via open-webui
|
||||
python-magic==0.4.27
|
||||
# via unstructured
|
||||
python-multipart==0.0.9
|
||||
# via fastapi
|
||||
# via open-webui
|
||||
python-socketio==5.11.2
|
||||
# via open-webui
|
||||
pytube==15.0.0
|
||||
# via open-webui
|
||||
pytz==2024.1
|
||||
# via apscheduler
|
||||
# via pandas
|
||||
pyxlsb==1.0.10
|
||||
# via open-webui
|
||||
pyyaml==6.0.1
|
||||
# via chromadb
|
||||
# via ctranslate2
|
||||
# via huggingface-hub
|
||||
# via kubernetes
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via langchain-core
|
||||
# via rapidocr-onnxruntime
|
||||
# via transformers
|
||||
# via uvicorn
|
||||
rank-bm25==0.2.2
|
||||
# via open-webui
|
||||
rapidfuzz==3.9.0
|
||||
# via unstructured
|
||||
rapidocr-onnxruntime==1.3.22
|
||||
# via open-webui
|
||||
red-black-tree-mod==1.20
|
||||
# via extract-msg
|
||||
regex==2024.5.10
|
||||
# via nltk
|
||||
# via transformers
|
||||
requests==2.32.2
|
||||
# via chromadb
|
||||
# via google-api-core
|
||||
# via huggingface-hub
|
||||
# via kubernetes
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via langsmith
|
||||
# via open-webui
|
||||
# via posthog
|
||||
# via requests-oauthlib
|
||||
# via transformers
|
||||
# via unstructured
|
||||
# via unstructured-client
|
||||
# via youtube-transcript-api
|
||||
requests-oauthlib==2.0.0
|
||||
# via kubernetes
|
||||
rich==13.7.1
|
||||
# via typer
|
||||
rsa==4.9
|
||||
# via google-auth
|
||||
# via python-jose
|
||||
rtfde==0.1.1
|
||||
# via extract-msg
|
||||
s3transfer==0.10.1
|
||||
# via boto3
|
||||
safetensors==0.4.3
|
||||
# via transformers
|
||||
scikit-learn==1.4.2
|
||||
# via sentence-transformers
|
||||
scipy==1.13.0
|
||||
# via scikit-learn
|
||||
# via sentence-transformers
|
||||
sentence-transformers==2.7.0
|
||||
# via open-webui
|
||||
setuptools==69.5.1
|
||||
# via ctranslate2
|
||||
# via opentelemetry-instrumentation
|
||||
shapely==2.0.4
|
||||
# via rapidocr-onnxruntime
|
||||
shellingham==1.5.4
|
||||
# via typer
|
||||
simple-websocket==1.0.0
|
||||
# via python-engineio
|
||||
six==1.16.0
|
||||
# via apscheduler
|
||||
# via ecdsa
|
||||
# via kubernetes
|
||||
# via langdetect
|
||||
# via posthog
|
||||
# via python-dateutil
|
||||
# via rapidocr-onnxruntime
|
||||
# via unstructured-client
|
||||
sniffio==1.3.1
|
||||
# via anyio
|
||||
# via httpx
|
||||
soupsieve==2.5
|
||||
# via beautifulsoup4
|
||||
sqlalchemy==2.0.30
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
starlette==0.37.2
|
||||
# via fastapi
|
||||
sympy==1.12
|
||||
# via onnxruntime
|
||||
# via torch
|
||||
tabulate==0.9.0
|
||||
# via unstructured
|
||||
tenacity==8.3.0
|
||||
# via chromadb
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via langchain-core
|
||||
threadpoolctl==3.5.0
|
||||
# via scikit-learn
|
||||
tokenizers==0.15.2
|
||||
# via chromadb
|
||||
# via faster-whisper
|
||||
# via transformers
|
||||
torch==2.3.0
|
||||
# via sentence-transformers
|
||||
tqdm==4.66.4
|
||||
# via chromadb
|
||||
# via google-generativeai
|
||||
# via huggingface-hub
|
||||
# via nltk
|
||||
# via sentence-transformers
|
||||
# via transformers
|
||||
transformers==4.39.3
|
||||
# via sentence-transformers
|
||||
typer==0.12.3
|
||||
# via chromadb
|
||||
# via fastapi-cli
|
||||
typing-extensions==4.11.0
|
||||
# via chromadb
|
||||
# via fastapi
|
||||
# via google-generativeai
|
||||
# via huggingface-hub
|
||||
# via opentelemetry-sdk
|
||||
# via pydantic
|
||||
# via pydantic-core
|
||||
# via sqlalchemy
|
||||
# via torch
|
||||
# via typer
|
||||
# via typing-inspect
|
||||
# via unstructured
|
||||
# via unstructured-client
|
||||
typing-inspect==0.9.0
|
||||
# via dataclasses-json
|
||||
# via unstructured-client
|
||||
tzdata==2024.1
|
||||
# via pandas
|
||||
tzlocal==5.2
|
||||
# via apscheduler
|
||||
# via extract-msg
|
||||
ujson==5.10.0
|
||||
# via fastapi
|
||||
unstructured==0.14.0
|
||||
# via open-webui
|
||||
unstructured-client==0.22.0
|
||||
# via unstructured
|
||||
uritemplate==4.1.1
|
||||
# via google-api-python-client
|
||||
urllib3==2.2.1
|
||||
# via botocore
|
||||
# via kubernetes
|
||||
# via requests
|
||||
# via unstructured-client
|
||||
uvicorn==0.22.0
|
||||
# via chromadb
|
||||
# via fastapi
|
||||
# via open-webui
|
||||
uvloop==0.19.0
|
||||
# via uvicorn
|
||||
validators==0.28.1
|
||||
# via open-webui
|
||||
watchfiles==0.21.0
|
||||
# via uvicorn
|
||||
websocket-client==1.8.0
|
||||
# via kubernetes
|
||||
websockets==12.0
|
||||
# via uvicorn
|
||||
werkzeug==3.0.3
|
||||
# via flask
|
||||
wrapt==1.16.0
|
||||
# via deprecated
|
||||
# via langfuse
|
||||
# via opentelemetry-instrumentation
|
||||
# via unstructured
|
||||
wsproto==1.2.0
|
||||
# via simple-websocket
|
||||
xlrd==2.0.1
|
||||
# via open-webui
|
||||
yarl==1.9.4
|
||||
# via aiohttp
|
||||
youtube-transcript-api==0.6.2
|
||||
# via open-webui
|
||||
zipp==3.18.1
|
||||
# via importlib-metadata
|
||||
@@ -0,0 +1,680 @@
|
||||
# generated by rye
|
||||
# use `rye lock` or `rye sync` to update this lockfile
|
||||
#
|
||||
# last locked with the following flags:
|
||||
# pre: false
|
||||
# features: []
|
||||
# all-features: false
|
||||
# with-sources: false
|
||||
# generate-hashes: false
|
||||
|
||||
-e file:.
|
||||
aiohttp==3.9.5
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via open-webui
|
||||
aiosignal==1.3.1
|
||||
# via aiohttp
|
||||
annotated-types==0.6.0
|
||||
# via pydantic
|
||||
anyio==4.3.0
|
||||
# via httpx
|
||||
# via starlette
|
||||
# via watchfiles
|
||||
apscheduler==3.10.4
|
||||
# via open-webui
|
||||
argon2-cffi==23.1.0
|
||||
# via open-webui
|
||||
argon2-cffi-bindings==21.2.0
|
||||
# via argon2-cffi
|
||||
asgiref==3.8.1
|
||||
# via opentelemetry-instrumentation-asgi
|
||||
attrs==23.2.0
|
||||
# via aiohttp
|
||||
av==11.0.0
|
||||
# via faster-whisper
|
||||
backoff==2.2.1
|
||||
# via langfuse
|
||||
# via posthog
|
||||
# via unstructured
|
||||
bcrypt==4.1.3
|
||||
# via chromadb
|
||||
# via open-webui
|
||||
# via passlib
|
||||
beautifulsoup4==4.12.3
|
||||
# via extract-msg
|
||||
# via unstructured
|
||||
bidict==0.23.1
|
||||
# via python-socketio
|
||||
black==24.4.2
|
||||
# via open-webui
|
||||
blinker==1.8.2
|
||||
# via flask
|
||||
boto3==1.34.110
|
||||
# via open-webui
|
||||
botocore==1.34.110
|
||||
# via boto3
|
||||
# via s3transfer
|
||||
build==1.2.1
|
||||
# via chromadb
|
||||
cachetools==5.3.3
|
||||
# via google-auth
|
||||
certifi==2024.2.2
|
||||
# via httpcore
|
||||
# via httpx
|
||||
# via kubernetes
|
||||
# via requests
|
||||
# via unstructured-client
|
||||
cffi==1.16.0
|
||||
# via argon2-cffi-bindings
|
||||
# via cryptography
|
||||
chardet==5.2.0
|
||||
# via unstructured
|
||||
charset-normalizer==3.3.2
|
||||
# via requests
|
||||
# via unstructured-client
|
||||
chroma-hnswlib==0.7.3
|
||||
# via chromadb
|
||||
chromadb==0.5.0
|
||||
# via langchain-chroma
|
||||
# via open-webui
|
||||
click==8.1.7
|
||||
# via black
|
||||
# via duckduckgo-search
|
||||
# via flask
|
||||
# via nltk
|
||||
# via peewee-migrate
|
||||
# via typer
|
||||
# via uvicorn
|
||||
colorclass==2.2.2
|
||||
# via oletools
|
||||
coloredlogs==15.0.1
|
||||
# via onnxruntime
|
||||
compressed-rtf==1.0.6
|
||||
# via extract-msg
|
||||
cryptography==42.0.7
|
||||
# via msoffcrypto-tool
|
||||
# via pyjwt
|
||||
ctranslate2==4.2.1
|
||||
# via faster-whisper
|
||||
dataclasses-json==0.6.6
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via unstructured
|
||||
# via unstructured-client
|
||||
deepdiff==7.0.1
|
||||
# via unstructured-client
|
||||
defusedxml==0.7.1
|
||||
# via fpdf2
|
||||
deprecated==1.2.14
|
||||
# via opentelemetry-api
|
||||
# via opentelemetry-exporter-otlp-proto-grpc
|
||||
dnspython==2.6.1
|
||||
# via email-validator
|
||||
docx2txt==0.8
|
||||
# via open-webui
|
||||
duckduckgo-search==6.1.5
|
||||
# via open-webui
|
||||
easygui==0.98.3
|
||||
# via oletools
|
||||
ebcdic==1.1.1
|
||||
# via extract-msg
|
||||
ecdsa==0.19.0
|
||||
# via python-jose
|
||||
email-validator==2.1.1
|
||||
# via fastapi
|
||||
emoji==2.11.1
|
||||
# via unstructured
|
||||
et-xmlfile==1.1.0
|
||||
# via openpyxl
|
||||
extract-msg==0.48.5
|
||||
# via open-webui
|
||||
fake-useragent==1.5.1
|
||||
# via open-webui
|
||||
fastapi==0.111.0
|
||||
# via chromadb
|
||||
# via langchain-chroma
|
||||
# via open-webui
|
||||
fastapi-cli==0.0.4
|
||||
# via fastapi
|
||||
faster-whisper==1.0.2
|
||||
# via open-webui
|
||||
filelock==3.14.0
|
||||
# via huggingface-hub
|
||||
# via torch
|
||||
# via transformers
|
||||
filetype==1.2.0
|
||||
# via unstructured
|
||||
flask==3.0.3
|
||||
# via flask-cors
|
||||
# via open-webui
|
||||
flask-cors==4.0.1
|
||||
# via open-webui
|
||||
flatbuffers==24.3.25
|
||||
# via onnxruntime
|
||||
fonttools==4.51.0
|
||||
# via fpdf2
|
||||
fpdf2==2.7.9
|
||||
# via open-webui
|
||||
frozenlist==1.4.1
|
||||
# via aiohttp
|
||||
# via aiosignal
|
||||
fsspec==2024.3.1
|
||||
# via huggingface-hub
|
||||
# via torch
|
||||
google-ai-generativelanguage==0.6.4
|
||||
# via google-generativeai
|
||||
google-api-core==2.19.0
|
||||
# via google-ai-generativelanguage
|
||||
# via google-api-python-client
|
||||
# via google-generativeai
|
||||
google-api-python-client==2.129.0
|
||||
# via google-generativeai
|
||||
google-auth==2.29.0
|
||||
# via google-ai-generativelanguage
|
||||
# via google-api-core
|
||||
# via google-api-python-client
|
||||
# via google-auth-httplib2
|
||||
# via google-generativeai
|
||||
# via kubernetes
|
||||
google-auth-httplib2==0.2.0
|
||||
# via google-api-python-client
|
||||
google-generativeai==0.5.4
|
||||
# via open-webui
|
||||
googleapis-common-protos==1.63.0
|
||||
# via google-api-core
|
||||
# via grpcio-status
|
||||
# via opentelemetry-exporter-otlp-proto-grpc
|
||||
grpcio==1.63.0
|
||||
# via chromadb
|
||||
# via google-api-core
|
||||
# via grpcio-status
|
||||
# via opentelemetry-exporter-otlp-proto-grpc
|
||||
grpcio-status==1.62.2
|
||||
# via google-api-core
|
||||
h11==0.14.0
|
||||
# via httpcore
|
||||
# via uvicorn
|
||||
# via wsproto
|
||||
httpcore==1.0.5
|
||||
# via httpx
|
||||
httplib2==0.22.0
|
||||
# via google-api-python-client
|
||||
# via google-auth-httplib2
|
||||
httptools==0.6.1
|
||||
# via uvicorn
|
||||
httpx==0.27.0
|
||||
# via fastapi
|
||||
# via langfuse
|
||||
huggingface-hub==0.23.0
|
||||
# via faster-whisper
|
||||
# via sentence-transformers
|
||||
# via tokenizers
|
||||
# via transformers
|
||||
humanfriendly==10.0
|
||||
# via coloredlogs
|
||||
idna==3.7
|
||||
# via anyio
|
||||
# via email-validator
|
||||
# via httpx
|
||||
# via langfuse
|
||||
# via requests
|
||||
# via unstructured-client
|
||||
# via yarl
|
||||
importlib-metadata==7.0.0
|
||||
# via opentelemetry-api
|
||||
importlib-resources==6.4.0
|
||||
# via chromadb
|
||||
itsdangerous==2.2.0
|
||||
# via flask
|
||||
jinja2==3.1.4
|
||||
# via fastapi
|
||||
# via flask
|
||||
# via torch
|
||||
jmespath==1.0.1
|
||||
# via boto3
|
||||
# via botocore
|
||||
joblib==1.4.2
|
||||
# via nltk
|
||||
# via scikit-learn
|
||||
jsonpatch==1.33
|
||||
# via langchain-core
|
||||
jsonpath-python==1.0.6
|
||||
# via unstructured-client
|
||||
jsonpointer==2.4
|
||||
# via jsonpatch
|
||||
kubernetes==29.0.0
|
||||
# via chromadb
|
||||
langchain==0.2.0
|
||||
# via langchain-community
|
||||
# via open-webui
|
||||
langchain-chroma==0.1.1
|
||||
# via open-webui
|
||||
langchain-community==0.2.0
|
||||
# via open-webui
|
||||
langchain-core==0.2.1
|
||||
# via langchain
|
||||
# via langchain-chroma
|
||||
# via langchain-community
|
||||
# via langchain-text-splitters
|
||||
langchain-text-splitters==0.2.0
|
||||
# via langchain
|
||||
langdetect==1.0.9
|
||||
# via unstructured
|
||||
langfuse==2.33.0
|
||||
# via open-webui
|
||||
langsmith==0.1.57
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via langchain-core
|
||||
lark==1.1.8
|
||||
# via rtfde
|
||||
lxml==5.2.2
|
||||
# via unstructured
|
||||
markdown==3.6
|
||||
# via open-webui
|
||||
markdown-it-py==3.0.0
|
||||
# via rich
|
||||
markupsafe==2.1.5
|
||||
# via jinja2
|
||||
# via werkzeug
|
||||
marshmallow==3.21.2
|
||||
# via dataclasses-json
|
||||
# via unstructured-client
|
||||
mdurl==0.1.2
|
||||
# via markdown-it-py
|
||||
mmh3==4.1.0
|
||||
# via chromadb
|
||||
monotonic==1.6
|
||||
# via posthog
|
||||
mpmath==1.3.0
|
||||
# via sympy
|
||||
msoffcrypto-tool==5.4.1
|
||||
# via oletools
|
||||
multidict==6.0.5
|
||||
# via aiohttp
|
||||
# via yarl
|
||||
mypy-extensions==1.0.0
|
||||
# via black
|
||||
# via typing-inspect
|
||||
# via unstructured-client
|
||||
networkx==3.3
|
||||
# via torch
|
||||
nltk==3.8.1
|
||||
# via unstructured
|
||||
numpy==1.26.4
|
||||
# via chroma-hnswlib
|
||||
# via chromadb
|
||||
# via ctranslate2
|
||||
# via langchain
|
||||
# via langchain-chroma
|
||||
# via langchain-community
|
||||
# via onnxruntime
|
||||
# via opencv-python
|
||||
# via opencv-python-headless
|
||||
# via pandas
|
||||
# via rank-bm25
|
||||
# via rapidocr-onnxruntime
|
||||
# via scikit-learn
|
||||
# via scipy
|
||||
# via sentence-transformers
|
||||
# via shapely
|
||||
# via transformers
|
||||
# via unstructured
|
||||
oauthlib==3.2.2
|
||||
# via kubernetes
|
||||
# via requests-oauthlib
|
||||
olefile==0.47
|
||||
# via extract-msg
|
||||
# via msoffcrypto-tool
|
||||
# via oletools
|
||||
oletools==0.60.1
|
||||
# via pcodedmp
|
||||
# via rtfde
|
||||
onnxruntime==1.17.3
|
||||
# via chromadb
|
||||
# via faster-whisper
|
||||
# via rapidocr-onnxruntime
|
||||
opencv-python==4.9.0.80
|
||||
# via rapidocr-onnxruntime
|
||||
opencv-python-headless==4.9.0.80
|
||||
# via open-webui
|
||||
openpyxl==3.1.2
|
||||
# via open-webui
|
||||
opentelemetry-api==1.24.0
|
||||
# via chromadb
|
||||
# via opentelemetry-exporter-otlp-proto-grpc
|
||||
# via opentelemetry-instrumentation
|
||||
# via opentelemetry-instrumentation-asgi
|
||||
# via opentelemetry-instrumentation-fastapi
|
||||
# via opentelemetry-sdk
|
||||
opentelemetry-exporter-otlp-proto-common==1.24.0
|
||||
# via opentelemetry-exporter-otlp-proto-grpc
|
||||
opentelemetry-exporter-otlp-proto-grpc==1.24.0
|
||||
# via chromadb
|
||||
opentelemetry-instrumentation==0.45b0
|
||||
# via opentelemetry-instrumentation-asgi
|
||||
# via opentelemetry-instrumentation-fastapi
|
||||
opentelemetry-instrumentation-asgi==0.45b0
|
||||
# via opentelemetry-instrumentation-fastapi
|
||||
opentelemetry-instrumentation-fastapi==0.45b0
|
||||
# via chromadb
|
||||
opentelemetry-proto==1.24.0
|
||||
# via opentelemetry-exporter-otlp-proto-common
|
||||
# via opentelemetry-exporter-otlp-proto-grpc
|
||||
opentelemetry-sdk==1.24.0
|
||||
# via chromadb
|
||||
# via opentelemetry-exporter-otlp-proto-grpc
|
||||
opentelemetry-semantic-conventions==0.45b0
|
||||
# via opentelemetry-instrumentation-asgi
|
||||
# via opentelemetry-instrumentation-fastapi
|
||||
# via opentelemetry-sdk
|
||||
opentelemetry-util-http==0.45b0
|
||||
# via opentelemetry-instrumentation-asgi
|
||||
# via opentelemetry-instrumentation-fastapi
|
||||
ordered-set==4.1.0
|
||||
# via deepdiff
|
||||
orjson==3.10.3
|
||||
# via chromadb
|
||||
# via duckduckgo-search
|
||||
# via fastapi
|
||||
# via langsmith
|
||||
overrides==7.7.0
|
||||
# via chromadb
|
||||
packaging==23.2
|
||||
# via black
|
||||
# via build
|
||||
# via huggingface-hub
|
||||
# via langchain-core
|
||||
# via langfuse
|
||||
# via marshmallow
|
||||
# via onnxruntime
|
||||
# via transformers
|
||||
# via unstructured-client
|
||||
pandas==2.2.2
|
||||
# via open-webui
|
||||
passlib==1.7.4
|
||||
# via open-webui
|
||||
pathspec==0.12.1
|
||||
# via black
|
||||
pcodedmp==1.2.6
|
||||
# via oletools
|
||||
peewee==3.17.5
|
||||
# via open-webui
|
||||
# via peewee-migrate
|
||||
peewee-migrate==1.12.2
|
||||
# via open-webui
|
||||
pillow==10.3.0
|
||||
# via fpdf2
|
||||
# via rapidocr-onnxruntime
|
||||
# via sentence-transformers
|
||||
platformdirs==4.2.1
|
||||
# via black
|
||||
posthog==3.5.0
|
||||
# via chromadb
|
||||
proto-plus==1.23.0
|
||||
# via google-ai-generativelanguage
|
||||
# via google-api-core
|
||||
protobuf==4.25.3
|
||||
# via google-ai-generativelanguage
|
||||
# via google-api-core
|
||||
# via google-generativeai
|
||||
# via googleapis-common-protos
|
||||
# via grpcio-status
|
||||
# via onnxruntime
|
||||
# via opentelemetry-proto
|
||||
# via proto-plus
|
||||
psycopg2-binary==2.9.9
|
||||
# via open-webui
|
||||
pyasn1==0.6.0
|
||||
# via pyasn1-modules
|
||||
# via python-jose
|
||||
# via rsa
|
||||
pyasn1-modules==0.4.0
|
||||
# via google-auth
|
||||
pyclipper==1.3.0.post5
|
||||
# via rapidocr-onnxruntime
|
||||
pycparser==2.22
|
||||
# via cffi
|
||||
pydantic==2.7.1
|
||||
# via chromadb
|
||||
# via fastapi
|
||||
# via google-generativeai
|
||||
# via langchain
|
||||
# via langchain-core
|
||||
# via langfuse
|
||||
# via langsmith
|
||||
# via open-webui
|
||||
pydantic-core==2.18.2
|
||||
# via pydantic
|
||||
pydub==0.25.1
|
||||
# via open-webui
|
||||
pygments==2.18.0
|
||||
# via rich
|
||||
pyjwt==2.8.0
|
||||
# via open-webui
|
||||
pymysql==1.1.0
|
||||
# via open-webui
|
||||
pypandoc==1.13
|
||||
# via open-webui
|
||||
pyparsing==2.4.7
|
||||
# via httplib2
|
||||
# via oletools
|
||||
pypdf==4.2.0
|
||||
# via open-webui
|
||||
# via unstructured-client
|
||||
pypika==0.48.9
|
||||
# via chromadb
|
||||
pyproject-hooks==1.1.0
|
||||
# via build
|
||||
pyreqwest-impersonate==0.4.7
|
||||
# via duckduckgo-search
|
||||
python-dateutil==2.9.0.post0
|
||||
# via botocore
|
||||
# via kubernetes
|
||||
# via pandas
|
||||
# via posthog
|
||||
# via unstructured-client
|
||||
python-dotenv==1.0.1
|
||||
# via uvicorn
|
||||
python-engineio==4.9.0
|
||||
# via python-socketio
|
||||
python-iso639==2024.4.27
|
||||
# via unstructured
|
||||
python-jose==3.3.0
|
||||
# via open-webui
|
||||
python-magic==0.4.27
|
||||
# via unstructured
|
||||
python-multipart==0.0.9
|
||||
# via fastapi
|
||||
# via open-webui
|
||||
python-socketio==5.11.2
|
||||
# via open-webui
|
||||
pytube==15.0.0
|
||||
# via open-webui
|
||||
pytz==2024.1
|
||||
# via apscheduler
|
||||
# via pandas
|
||||
pyxlsb==1.0.10
|
||||
# via open-webui
|
||||
pyyaml==6.0.1
|
||||
# via chromadb
|
||||
# via ctranslate2
|
||||
# via huggingface-hub
|
||||
# via kubernetes
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via langchain-core
|
||||
# via rapidocr-onnxruntime
|
||||
# via transformers
|
||||
# via uvicorn
|
||||
rank-bm25==0.2.2
|
||||
# via open-webui
|
||||
rapidfuzz==3.9.0
|
||||
# via unstructured
|
||||
rapidocr-onnxruntime==1.3.22
|
||||
# via open-webui
|
||||
red-black-tree-mod==1.20
|
||||
# via extract-msg
|
||||
regex==2024.5.10
|
||||
# via nltk
|
||||
# via transformers
|
||||
requests==2.32.2
|
||||
# via chromadb
|
||||
# via google-api-core
|
||||
# via huggingface-hub
|
||||
# via kubernetes
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via langsmith
|
||||
# via open-webui
|
||||
# via posthog
|
||||
# via requests-oauthlib
|
||||
# via transformers
|
||||
# via unstructured
|
||||
# via unstructured-client
|
||||
# via youtube-transcript-api
|
||||
requests-oauthlib==2.0.0
|
||||
# via kubernetes
|
||||
rich==13.7.1
|
||||
# via typer
|
||||
rsa==4.9
|
||||
# via google-auth
|
||||
# via python-jose
|
||||
rtfde==0.1.1
|
||||
# via extract-msg
|
||||
s3transfer==0.10.1
|
||||
# via boto3
|
||||
safetensors==0.4.3
|
||||
# via transformers
|
||||
scikit-learn==1.4.2
|
||||
# via sentence-transformers
|
||||
scipy==1.13.0
|
||||
# via scikit-learn
|
||||
# via sentence-transformers
|
||||
sentence-transformers==2.7.0
|
||||
# via open-webui
|
||||
setuptools==69.5.1
|
||||
# via ctranslate2
|
||||
# via opentelemetry-instrumentation
|
||||
shapely==2.0.4
|
||||
# via rapidocr-onnxruntime
|
||||
shellingham==1.5.4
|
||||
# via typer
|
||||
simple-websocket==1.0.0
|
||||
# via python-engineio
|
||||
six==1.16.0
|
||||
# via apscheduler
|
||||
# via ecdsa
|
||||
# via kubernetes
|
||||
# via langdetect
|
||||
# via posthog
|
||||
# via python-dateutil
|
||||
# via rapidocr-onnxruntime
|
||||
# via unstructured-client
|
||||
sniffio==1.3.1
|
||||
# via anyio
|
||||
# via httpx
|
||||
soupsieve==2.5
|
||||
# via beautifulsoup4
|
||||
sqlalchemy==2.0.30
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
starlette==0.37.2
|
||||
# via fastapi
|
||||
sympy==1.12
|
||||
# via onnxruntime
|
||||
# via torch
|
||||
tabulate==0.9.0
|
||||
# via unstructured
|
||||
tenacity==8.3.0
|
||||
# via chromadb
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via langchain-core
|
||||
threadpoolctl==3.5.0
|
||||
# via scikit-learn
|
||||
tokenizers==0.15.2
|
||||
# via chromadb
|
||||
# via faster-whisper
|
||||
# via transformers
|
||||
torch==2.3.0
|
||||
# via sentence-transformers
|
||||
tqdm==4.66.4
|
||||
# via chromadb
|
||||
# via google-generativeai
|
||||
# via huggingface-hub
|
||||
# via nltk
|
||||
# via sentence-transformers
|
||||
# via transformers
|
||||
transformers==4.39.3
|
||||
# via sentence-transformers
|
||||
typer==0.12.3
|
||||
# via chromadb
|
||||
# via fastapi-cli
|
||||
typing-extensions==4.11.0
|
||||
# via chromadb
|
||||
# via fastapi
|
||||
# via google-generativeai
|
||||
# via huggingface-hub
|
||||
# via opentelemetry-sdk
|
||||
# via pydantic
|
||||
# via pydantic-core
|
||||
# via sqlalchemy
|
||||
# via torch
|
||||
# via typer
|
||||
# via typing-inspect
|
||||
# via unstructured
|
||||
# via unstructured-client
|
||||
typing-inspect==0.9.0
|
||||
# via dataclasses-json
|
||||
# via unstructured-client
|
||||
tzdata==2024.1
|
||||
# via pandas
|
||||
tzlocal==5.2
|
||||
# via apscheduler
|
||||
# via extract-msg
|
||||
ujson==5.10.0
|
||||
# via fastapi
|
||||
unstructured==0.14.0
|
||||
# via open-webui
|
||||
unstructured-client==0.22.0
|
||||
# via unstructured
|
||||
uritemplate==4.1.1
|
||||
# via google-api-python-client
|
||||
urllib3==2.2.1
|
||||
# via botocore
|
||||
# via kubernetes
|
||||
# via requests
|
||||
# via unstructured-client
|
||||
uvicorn==0.22.0
|
||||
# via chromadb
|
||||
# via fastapi
|
||||
# via open-webui
|
||||
uvloop==0.19.0
|
||||
# via uvicorn
|
||||
validators==0.28.1
|
||||
# via open-webui
|
||||
watchfiles==0.21.0
|
||||
# via uvicorn
|
||||
websocket-client==1.8.0
|
||||
# via kubernetes
|
||||
websockets==12.0
|
||||
# via uvicorn
|
||||
werkzeug==3.0.3
|
||||
# via flask
|
||||
wrapt==1.16.0
|
||||
# via deprecated
|
||||
# via langfuse
|
||||
# via opentelemetry-instrumentation
|
||||
# via unstructured
|
||||
wsproto==1.2.0
|
||||
# via simple-websocket
|
||||
xlrd==2.0.1
|
||||
# via open-webui
|
||||
yarl==1.9.4
|
||||
# via aiohttp
|
||||
youtube-transcript-api==0.6.2
|
||||
# via open-webui
|
||||
zipp==3.18.1
|
||||
# via importlib-metadata
|
||||
+21
@@ -92,10 +92,18 @@ select {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.scrollbar-hidden::-webkit-scrollbar-corner {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.scrollbar-none::-webkit-scrollbar {
|
||||
display: none; /* for Chrome, Safari and Opera */
|
||||
}
|
||||
|
||||
.scrollbar-none::-webkit-scrollbar-corner {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.scrollbar-none {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
@@ -111,3 +119,16 @@ input::-webkit-inner-spin-button {
|
||||
input[type='number'] {
|
||||
-moz-appearance: textfield; /* Firefox */
|
||||
}
|
||||
|
||||
.cm-editor {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cm-scroller {
|
||||
@apply scrollbar-hidden;
|
||||
}
|
||||
|
||||
.cm-editor.cm-focused {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
+115
-11
@@ -32,6 +32,9 @@
|
||||
} 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') {
|
||||
document.documentElement.classList.add('dark');
|
||||
document.documentElement.classList.add('her');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
@@ -59,15 +62,7 @@
|
||||
|
||||
<div
|
||||
id="splash-screen"
|
||||
style="
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
background: #fff;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
"
|
||||
style="position: fixed; z-index: 100; top: 0; left: 0; width: 100%; height: 100%"
|
||||
>
|
||||
<style type="text/css" nonce="">
|
||||
html {
|
||||
@@ -76,20 +71,129 @@
|
||||
</style>
|
||||
|
||||
<img
|
||||
id="logo"
|
||||
style="
|
||||
position: absolute;
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
top: 46%;
|
||||
top: 41%;
|
||||
left: 50%;
|
||||
margin: -40px 0 0 -40px;
|
||||
margin-left: -3rem;
|
||||
"
|
||||
src="/logo.svg"
|
||||
/>
|
||||
|
||||
<img
|
||||
id="logo-her"
|
||||
style="
|
||||
position: absolute;
|
||||
width: 13rem;
|
||||
height: 13rem;
|
||||
top: 33%;
|
||||
left: 50%;
|
||||
margin-left: -6.5rem;
|
||||
"
|
||||
src="/logo.svg"
|
||||
class="animate-pulse-fast"
|
||||
/>
|
||||
|
||||
<div
|
||||
id="progress-background"
|
||||
style="
|
||||
position: absolute;
|
||||
top: 58%;
|
||||
left: 50%;
|
||||
|
||||
margin-left: -12rem;
|
||||
|
||||
width: 24rem;
|
||||
height: 0.75rem;
|
||||
border-radius: 9999px;
|
||||
background-color: #fafafa9a;
|
||||
"
|
||||
class="bg-white"
|
||||
></div>
|
||||
|
||||
<div
|
||||
id="progress-bar"
|
||||
style="
|
||||
position: absolute;
|
||||
top: 58%;
|
||||
left: 50%;
|
||||
|
||||
margin-left: -12rem;
|
||||
|
||||
height: 0.75rem;
|
||||
border-radius: 9999px;
|
||||
background-color: #fff;
|
||||
|
||||
width: 0rem;
|
||||
"
|
||||
class="bg-white"
|
||||
></div>
|
||||
|
||||
<!-- <span style="position: absolute; bottom: 32px; left: 50%; margin: -36px 0 0 -36px">
|
||||
Footer content
|
||||
</span> -->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style type="text/css" nonce="">
|
||||
html {
|
||||
overflow-y: hidden !important;
|
||||
}
|
||||
|
||||
#splash-screen {
|
||||
background: #fff;
|
||||
}
|
||||
html.dark #splash-screen {
|
||||
background: #000;
|
||||
}
|
||||
|
||||
html.dark #splash-screen img {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
html.her #splash-screen {
|
||||
background: #983724;
|
||||
}
|
||||
|
||||
#logo-her {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#progress-background {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#progress-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html.her #logo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html.her #logo-her {
|
||||
display: block;
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
html.her #progress-background {
|
||||
display: block;
|
||||
}
|
||||
|
||||
html.her #progress-bar {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
50% {
|
||||
opacity: 0.65;
|
||||
}
|
||||
}
|
||||
.animate-pulse-fast {
|
||||
animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -98,7 +98,7 @@ export const synthesizeOpenAISpeech = async (
|
||||
token: string = '',
|
||||
speaker: string = 'alloy',
|
||||
text: string = '',
|
||||
model: string = 'tts-1'
|
||||
model?: string
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
@@ -109,9 +109,9 @@ export const synthesizeOpenAISpeech = async (
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: model,
|
||||
input: text,
|
||||
voice: speaker
|
||||
voice: speaker,
|
||||
...(model && { model })
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
|
||||
@@ -1,5 +1,87 @@
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
export const getAdminDetails = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/admin/details`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getAdminConfig = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/admin/config`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const updateAdminConfig = async (token: string, body: object) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/admin/config`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getSessionUser = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
|
||||
@@ -162,6 +162,37 @@ export const getAllChats = async (token: string) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getAllArchivedChats = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/all/archived`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err;
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getAllUserChats = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
@@ -325,6 +356,44 @@ export const getChatByShareId = async (token: string, share_id: string) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const cloneChatById = async (token: string, id: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/clone`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err;
|
||||
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
} else {
|
||||
error = err;
|
||||
}
|
||||
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const shareChatById = async (token: string, id: string) => {
|
||||
let error = null;
|
||||
|
||||
@@ -654,3 +723,35 @@ export const deleteAllChats = async (token: string) => {
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const archiveAllChats = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/archive/all`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
import type { Banner } from '$lib/types';
|
||||
|
||||
export const setDefaultModels = async (token: string, models: string) => {
|
||||
let error = null;
|
||||
@@ -59,3 +60,60 @@ export const setDefaultPromptSuggestions = async (token: string, promptSuggestio
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getBanners = async (token: string): Promise<Banner[]> => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/banners`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const setBanners = async (token: string, banners: Banner[]) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/banners`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
banners: banners
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user