├── .gitignore ├── LICENSE ├── README.md ├── app ├── Dockerfile ├── Pipfile ├── Pipfile.lock ├── default.ai ├── log_conf.yaml └── main.py ├── docker-compose.yaml ├── img ├── banner.png ├── lasers.gif └── lollm-demo-1.gif ├── justfile └── test ├── msg.json └── ollama.json /.gitignore: -------------------------------------------------------------------------------- 1 | app/.env 2 | app/media/* 3 | app/context.txt 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Justin Garrison 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](/img/banner.png) 2 | 3 | This app allows you to send iMessages to the genAI models running in your own computer. 4 | See it in action with this video 👇. 5 | 6 | [![Video overview](https://img.youtube.com/vi/rlD4FtIXV8E/0.jpg)](https://youtu.be/rlD4FtIXV8E) 7 | 8 | https://youtu.be/rlD4FtIXV8E 9 | 10 | This project uses Docker compose to run the required services locally. 11 | It uses [sendblue](https://sendblue.co/) to handle iMessages and [ollama](https://ollama.ai/) to install and manage AI models. 12 | 13 | If you add an OpenAI key you can also use [ChatGPT](https://openai.com/). 14 | 15 | ## Usage 16 | 17 | You use the app by sending messages to the bot. 18 | See the setup instructions on how to configure sendblue. 19 | 20 | There is a `/help` message that lists available commands. 21 | Some examples include: 22 | ``` 23 | /help - list help commands 24 | /list - list available models 25 | /install - install a model from ollama 26 | /default - set a default model 27 | ``` 28 | Message the Bot with any text. 29 | 30 | ![messaging the bot with the question "what is the air speed velocity of a swallow"](/img/lollm-demo-1.gif) 31 | 32 | Messages have key words than may trigger special styles. 33 | In the future it will also support sending styles as part of the message. 34 | Reactions and marking messages as read currently does not work. 35 | 36 | ![a message being sent with a laser effect](/img/lasers.gif) 37 | 38 | The default AI model is stored in `default.ai` text. 39 | If you want to change the default you can send the command `/default gpt-3.5`. 40 | 41 | If you want to message a different bot for a single message you can use `@` with the model name. 42 | ``` 43 | @llama how does electricity work 44 | ``` 45 | This will use the closest match for `llama` in your model list to ask the question. 46 | If you have multiple models that start with `llama` you need to be more specific about which model you want to use. 47 | 48 | Context is kept in `app/context.txt` and only 25 lines of context is stored. 49 | 50 | 51 | ## Setup 52 | 53 | Clone the repo and run the app locally. 54 | The app comes with 3 parts: 55 | 1. sendblue bridge (lollm) 56 | 1. ngrok 57 | 1. ollama 58 | 59 | Ollama will manage the models for you. 60 | Ngrok will give you a public webhook to use with sendblue. 61 | Lollm is the bridge that relays messages between the AI models and sendblue. 62 | 63 | ``` 64 | git clone git@github.com:rothgar/local-llm.git 65 | cd local-llm 66 | docker compose up 67 | ``` 68 | The first time you run the application if you don't have an OpenAI key then the [`llama2:latest`](https://ollama.ai/library/llama2) model will be installed for ollama. 69 | This image is about 4GB in size so be prepared for it to take a while. 70 | 71 | Watch the output for your ngrok endpoint. 72 | It should look like this: 73 | ``` 74 | local-llm-messenger-ngrok-1 | t=2023-11-03T23:15:11+0000 lvl=info msg="tunnel session started" obj=tunnels.session 75 | local-llm-messenger-ngrok-1 | t=2023-11-03T23:15:11+0000 lvl=info msg="started tunnel" obj=tunnels name=command_line addr=http:// 76 | localhost:8000 url=https://6336-47-149-36-168.ngrok.io 77 | ``` 78 | The url host `https://6336-47-149-36-168.ngrok.io` is what you're going to put into the [sendblue api dashboard](https://app.sendblue.co/api-dashboard). 79 | You need to add `/msg` to the end of the receive webhook so the full url should look like: 80 | ``` 81 | https://6336-47-149-36-168.ngrok.io/msg 82 | ``` 83 | 84 | Save the configuration and copy the API Key and API secret from the dashboard. 85 | Put them in the `app/.env` file: 86 | ``` 87 | SENDBLUE_API_KEY=xxxxxx.... 88 | SENDBLUE_API_SECRET=xxxxx.... 89 | ``` 90 | You also need to put the OLLAMA_ENDPOINT into the .env file. 91 | This default will work with docker compose 92 | ``` 93 | OLLAMA_API_ENDPOINT=https://ollama:11434/api 94 | ``` 95 | If you want to use ChatGPT you also can put your OpenAI key. 96 | You can create one in your [user settings](https://platform.openai.com/account/api-keys). 97 | ``` 98 | OPENAI_API_KEY=xxxxx.... 99 | ``` 100 | You now need to add your phone number as a sendblue contact. 101 | Navigate to the [contacts dashboard](https://app.sendblue.co/message-dashboard) and click Add Contact. 102 | 103 | You should test that you can receive the messages by sending a test message from the dashboard. 104 | 105 | After your contact is set up you should be able to send messages to the sendblue number and they will be forwarded to lollm. 106 | 107 | ## Limitations 108 | You can use ChatGPT with your own API key but currently only gpt-3.5-turbo and dalle-2 are available from the API. 109 | When gpt-4 and dalle-3 are added to the API we can add them to the app. 110 | 111 | Ollama does not yet support any AI models that have image generation or image processing. 112 | When models are available we can add the ability to send and receive images. 113 | If you send images to the bot they are downloaded into `app/media/` but are currently not used for anything until image processing or generation is available. 114 | 115 | Ollama does not yet support message context (only supported with openAI). 116 | 117 | You can sign up for a sendblue indehacker account by emailing their support and requesting it for personal use. 118 | It is limited to 100 messages a month and always appends `-Sent using sendblue.co` to your messages. 119 | -------------------------------------------------------------------------------- /app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-bookworm 2 | 3 | # don't create a venv with pipenv 4 | ENV PIPENV_SYSTEM=1 5 | 6 | COPY . /app 7 | WORKDIR /app 8 | RUN pip install pipenv 9 | RUN pipenv install 10 | 11 | CMD ["uvicorn", "--host", "0.0.0.0", "main:app"] 12 | # CMD ["python3", "main.py"] 13 | #CMD ["uvicorn", "main:app", "--reload ", "--reload-include", "'default.ai'", "--reload-exclude", "'.git'", "--log-config", "log_conf.yaml"] 14 | -------------------------------------------------------------------------------- /app/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | sendblue = "*" 8 | openai = "=1.0.0rc3" 9 | uvicorn = {extras = ["standard"], version = "*"} 10 | fastapi = "*" 11 | requests = "*" 12 | 13 | [dev-packages] 14 | 15 | [requires] 16 | python_version = "3.11" 17 | 18 | [pipenv] 19 | allow_prereleases = true 20 | -------------------------------------------------------------------------------- /app/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "1f53a84164288f23c6e9d00b5c419c7283cc11bd0f42d96c449281c1f2a2e1ab" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.11" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "annotated-types": { 20 | "hashes": [ 21 | "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43", 22 | "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d" 23 | ], 24 | "markers": "python_version >= '3.8'", 25 | "version": "==0.6.0" 26 | }, 27 | "anyio": { 28 | "hashes": [ 29 | "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780", 30 | "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5" 31 | ], 32 | "markers": "python_version >= '3.7'", 33 | "version": "==3.7.1" 34 | }, 35 | "certifi": { 36 | "hashes": [ 37 | "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082", 38 | "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9" 39 | ], 40 | "markers": "python_version >= '3.6'", 41 | "version": "==2023.7.22" 42 | }, 43 | "charset-normalizer": { 44 | "hashes": [ 45 | "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", 46 | "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", 47 | "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", 48 | "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", 49 | "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", 50 | "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", 51 | "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", 52 | "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", 53 | "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", 54 | "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", 55 | "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", 56 | "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", 57 | "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", 58 | "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", 59 | "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", 60 | "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", 61 | "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", 62 | "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", 63 | "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", 64 | "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", 65 | "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", 66 | "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", 67 | "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", 68 | "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", 69 | "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", 70 | "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", 71 | "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", 72 | "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", 73 | "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", 74 | "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", 75 | "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", 76 | "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", 77 | "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", 78 | "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", 79 | "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", 80 | "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", 81 | "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", 82 | "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", 83 | "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", 84 | "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", 85 | "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", 86 | "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", 87 | "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", 88 | "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", 89 | "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", 90 | "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", 91 | "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", 92 | "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", 93 | "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", 94 | "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", 95 | "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", 96 | "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", 97 | "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", 98 | "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", 99 | "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", 100 | "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", 101 | "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", 102 | "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", 103 | "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", 104 | "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", 105 | "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", 106 | "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", 107 | "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", 108 | "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", 109 | "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", 110 | "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", 111 | "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", 112 | "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", 113 | "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", 114 | "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", 115 | "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", 116 | "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", 117 | "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", 118 | "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", 119 | "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", 120 | "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", 121 | "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", 122 | "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", 123 | "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", 124 | "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", 125 | "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", 126 | "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", 127 | "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", 128 | "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", 129 | "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", 130 | "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", 131 | "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", 132 | "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", 133 | "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", 134 | "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" 135 | ], 136 | "markers": "python_full_version >= '3.7.0'", 137 | "version": "==3.3.2" 138 | }, 139 | "click": { 140 | "hashes": [ 141 | "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", 142 | "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" 143 | ], 144 | "markers": "python_version >= '3.7'", 145 | "version": "==8.1.7" 146 | }, 147 | "distro": { 148 | "hashes": [ 149 | "sha256:02e111d1dc6a50abb8eed6bf31c3e48ed8b0830d1ea2a1b78c61765c2513fdd8", 150 | "sha256:99522ca3e365cac527b44bde033f64c6945d90eb9f769703caaec52b09bbd3ff" 151 | ], 152 | "markers": "python_version >= '3.6'", 153 | "version": "==1.8.0" 154 | }, 155 | "fastapi": { 156 | "hashes": [ 157 | "sha256:752dc31160cdbd0436bb93bad51560b57e525cbb1d4bbf6f4904ceee75548241", 158 | "sha256:e5e4540a7c5e1dcfbbcf5b903c234feddcdcd881f191977a1c5dfd917487e7ae" 159 | ], 160 | "index": "pypi", 161 | "markers": "python_version >= '3.8'", 162 | "version": "==0.104.1" 163 | }, 164 | "h11": { 165 | "hashes": [ 166 | "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", 167 | "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761" 168 | ], 169 | "markers": "python_version >= '3.7'", 170 | "version": "==0.14.0" 171 | }, 172 | "httpcore": { 173 | "hashes": [ 174 | "sha256:13b5e5cd1dca1a6636a6aaea212b19f4f85cd88c366a2b82304181b769aab3c9", 175 | "sha256:adc5398ee0a476567bf87467063ee63584a8bce86078bf748e48754f60202ced" 176 | ], 177 | "markers": "python_version >= '3.8'", 178 | "version": "==0.18.0" 179 | }, 180 | "httptools": { 181 | "hashes": [ 182 | "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563", 183 | "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142", 184 | "sha256:0cf2372e98406efb42e93bfe10f2948e467edfd792b015f1b4ecd897903d3e8d", 185 | "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b", 186 | "sha256:3c3b214ce057c54675b00108ac42bacf2ab8f85c58e3f324a4e963bbc46424f4", 187 | "sha256:3e802e0b2378ade99cd666b5bffb8b2a7cc8f3d28988685dc300469ea8dd86cb", 188 | "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658", 189 | "sha256:405784577ba6540fa7d6ff49e37daf104e04f4b4ff2d1ac0469eaa6a20fde084", 190 | "sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2", 191 | "sha256:4bd3e488b447046e386a30f07af05f9b38d3d368d1f7b4d8f7e10af85393db97", 192 | "sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837", 193 | "sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3", 194 | "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58", 195 | "sha256:678fcbae74477a17d103b7cae78b74800d795d702083867ce160fc202104d0da", 196 | "sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d", 197 | "sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90", 198 | "sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0", 199 | "sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1", 200 | "sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2", 201 | "sha256:7ebaec1bf683e4bf5e9fbb49b8cc36da482033596a415b3e4ebab5a4c0d7ec5e", 202 | "sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0", 203 | "sha256:8ae5b97f690badd2ca27cbf668494ee1b6d34cf1c464271ef7bfa9ca6b83ffaf", 204 | "sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc", 205 | "sha256:8e216a038d2d52ea13fdd9b9c9c7459fb80d78302b257828285eca1c773b99b3", 206 | "sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503", 207 | "sha256:95658c342529bba4e1d3d2b1a874db16c7cca435e8827422154c9da76ac4e13a", 208 | "sha256:95fb92dd3649f9cb139e9c56604cc2d7c7bf0fc2e7c8d7fbd58f96e35eddd2a3", 209 | "sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949", 210 | "sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84", 211 | "sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb", 212 | "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a", 213 | "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f", 214 | "sha256:dcbab042cc3ef272adc11220517278519adf8f53fd3056d0e68f0a6f891ba94e", 215 | "sha256:e0b281cf5a125c35f7f6722b65d8542d2e57331be573e9e88bc8b0115c4a7a81", 216 | "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185", 217 | "sha256:fe467eb086d80217b7584e61313ebadc8d187a4d95bb62031b7bab4b205c3ba3" 218 | ], 219 | "version": "==0.6.1" 220 | }, 221 | "httpx": { 222 | "hashes": [ 223 | "sha256:181ea7f8ba3a82578be86ef4171554dd45fec26a02556a744db029a0a27b7100", 224 | "sha256:47ecda285389cb32bb2691cc6e069e3ab0205956f681c5b2ad2325719751d875" 225 | ], 226 | "markers": "python_version >= '3.8'", 227 | "version": "==0.25.0" 228 | }, 229 | "idna": { 230 | "hashes": [ 231 | "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", 232 | "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" 233 | ], 234 | "markers": "python_version >= '3.5'", 235 | "version": "==3.4" 236 | }, 237 | "openai": { 238 | "hashes": [ 239 | "sha256:a2075ef79acdae200798999b33782e99a48a64fd6ca61f7691d5b25a179fdc1c", 240 | "sha256:f082b0158529f5d826155b065493994bcdfd66d46045805668a4ad57fdfc0b24" 241 | ], 242 | "index": "pypi", 243 | "markers": "python_full_version >= '3.7.1'", 244 | "version": "==1.0.0rc1" 245 | }, 246 | "pydantic": { 247 | "hashes": [ 248 | "sha256:94f336138093a5d7f426aac732dcfe7ab4eb4da243c88f891d65deb4a2556ee7", 249 | "sha256:bc3ddf669d234f4220e6e1c4d96b061abe0998185a8d7855c0126782b7abc8c1" 250 | ], 251 | "markers": "python_version >= '3.7'", 252 | "version": "==2.4.2" 253 | }, 254 | "pydantic-core": { 255 | "hashes": [ 256 | "sha256:042462d8d6ba707fd3ce9649e7bf268633a41018d6a998fb5fbacb7e928a183e", 257 | "sha256:0523aeb76e03f753b58be33b26540880bac5aa54422e4462404c432230543f33", 258 | "sha256:05560ab976012bf40f25d5225a58bfa649bb897b87192a36c6fef1ab132540d7", 259 | "sha256:0675ba5d22de54d07bccde38997e780044dcfa9a71aac9fd7d4d7a1d2e3e65f7", 260 | "sha256:073d4a470b195d2b2245d0343569aac7e979d3a0dcce6c7d2af6d8a920ad0bea", 261 | "sha256:07ec6d7d929ae9c68f716195ce15e745b3e8fa122fc67698ac6498d802ed0fa4", 262 | "sha256:0880e239827b4b5b3e2ce05e6b766a7414e5f5aedc4523be6b68cfbc7f61c5d0", 263 | "sha256:0c27f38dc4fbf07b358b2bc90edf35e82d1703e22ff2efa4af4ad5de1b3833e7", 264 | "sha256:0d8a8adef23d86d8eceed3e32e9cca8879c7481c183f84ed1a8edc7df073af94", 265 | "sha256:0e2a35baa428181cb2270a15864ec6286822d3576f2ed0f4cd7f0c1708472aff", 266 | "sha256:0f8682dbdd2f67f8e1edddcbffcc29f60a6182b4901c367fc8c1c40d30bb0a82", 267 | "sha256:0fa467fd300a6f046bdb248d40cd015b21b7576c168a6bb20aa22e595c8ffcdd", 268 | "sha256:128552af70a64660f21cb0eb4876cbdadf1a1f9d5de820fed6421fa8de07c893", 269 | "sha256:1396e81b83516b9d5c9e26a924fa69164156c148c717131f54f586485ac3c15e", 270 | "sha256:149b8a07712f45b332faee1a2258d8ef1fb4a36f88c0c17cb687f205c5dc6e7d", 271 | "sha256:14ac492c686defc8e6133e3a2d9eaf5261b3df26b8ae97450c1647286750b901", 272 | "sha256:14cfbb00959259e15d684505263d5a21732b31248a5dd4941f73a3be233865b9", 273 | "sha256:14e09ff0b8fe6e46b93d36a878f6e4a3a98ba5303c76bb8e716f4878a3bee92c", 274 | "sha256:154ea7c52e32dce13065dbb20a4a6f0cc012b4f667ac90d648d36b12007fa9f7", 275 | "sha256:15d6bca84ffc966cc9976b09a18cf9543ed4d4ecbd97e7086f9ce9327ea48891", 276 | "sha256:1d40f55222b233e98e3921df7811c27567f0e1a4411b93d4c5c0f4ce131bc42f", 277 | "sha256:25bd966103890ccfa028841a8f30cebcf5875eeac8c4bde4fe221364c92f0c9a", 278 | "sha256:2cf5bb4dd67f20f3bbc1209ef572a259027c49e5ff694fa56bed62959b41e1f9", 279 | "sha256:2e0e2959ef5d5b8dc9ef21e1a305a21a36e254e6a34432d00c72a92fdc5ecda5", 280 | "sha256:320f14bd4542a04ab23747ff2c8a778bde727158b606e2661349557f0770711e", 281 | "sha256:3625578b6010c65964d177626fde80cf60d7f2e297d56b925cb5cdeda6e9925a", 282 | "sha256:39215d809470f4c8d1881758575b2abfb80174a9e8daf8f33b1d4379357e417c", 283 | "sha256:3f0ac9fb8608dbc6eaf17956bf623c9119b4db7dbb511650910a82e261e6600f", 284 | "sha256:417243bf599ba1f1fef2bb8c543ceb918676954734e2dcb82bf162ae9d7bd514", 285 | "sha256:420a692b547736a8d8703c39ea935ab5d8f0d2573f8f123b0a294e49a73f214b", 286 | "sha256:443fed67d33aa85357464f297e3d26e570267d1af6fef1c21ca50921d2976302", 287 | "sha256:48525933fea744a3e7464c19bfede85df4aba79ce90c60b94d8b6e1eddd67096", 288 | "sha256:485a91abe3a07c3a8d1e082ba29254eea3e2bb13cbbd4351ea4e5a21912cc9b0", 289 | "sha256:4a5be350f922430997f240d25f8219f93b0c81e15f7b30b868b2fddfc2d05f27", 290 | "sha256:4d966c47f9dd73c2d32a809d2be529112d509321c5310ebf54076812e6ecd884", 291 | "sha256:524ff0ca3baea164d6d93a32c58ac79eca9f6cf713586fdc0adb66a8cdeab96a", 292 | "sha256:53df009d1e1ba40f696f8995683e067e3967101d4bb4ea6f667931b7d4a01357", 293 | "sha256:5994985da903d0b8a08e4935c46ed8daf5be1cf217489e673910951dc533d430", 294 | "sha256:5cabb9710f09d5d2e9e2748c3e3e20d991a4c5f96ed8f1132518f54ab2967221", 295 | "sha256:5fdb39f67c779b183b0c853cd6b45f7db84b84e0571b3ef1c89cdb1dfc367325", 296 | "sha256:600d04a7b342363058b9190d4e929a8e2e715c5682a70cc37d5ded1e0dd370b4", 297 | "sha256:631cb7415225954fdcc2a024119101946793e5923f6c4d73a5914d27eb3d3a05", 298 | "sha256:63974d168b6233b4ed6a0046296803cb13c56637a7b8106564ab575926572a55", 299 | "sha256:64322bfa13e44c6c30c518729ef08fda6026b96d5c0be724b3c4ae4da939f875", 300 | "sha256:655f8f4c8d6a5963c9a0687793da37b9b681d9ad06f29438a3b2326d4e6b7970", 301 | "sha256:6835451b57c1b467b95ffb03a38bb75b52fb4dc2762bb1d9dbed8de31ea7d0fc", 302 | "sha256:6db2eb9654a85ada248afa5a6db5ff1cf0f7b16043a6b070adc4a5be68c716d6", 303 | "sha256:7c4d1894fe112b0864c1fa75dffa045720a194b227bed12f4be7f6045b25209f", 304 | "sha256:7eb037106f5c6b3b0b864ad226b0b7ab58157124161d48e4b30c4a43fef8bc4b", 305 | "sha256:8282bab177a9a3081fd3d0a0175a07a1e2bfb7fcbbd949519ea0980f8a07144d", 306 | "sha256:82f55187a5bebae7d81d35b1e9aaea5e169d44819789837cdd4720d768c55d15", 307 | "sha256:8572cadbf4cfa95fb4187775b5ade2eaa93511f07947b38f4cd67cf10783b118", 308 | "sha256:8cdbbd92154db2fec4ec973d45c565e767ddc20aa6dbaf50142676484cbff8ee", 309 | "sha256:8f6e6aed5818c264412ac0598b581a002a9f050cb2637a84979859e70197aa9e", 310 | "sha256:92f675fefa977625105708492850bcbc1182bfc3e997f8eecb866d1927c98ae6", 311 | "sha256:962ed72424bf1f72334e2f1e61b68f16c0e596f024ca7ac5daf229f7c26e4208", 312 | "sha256:9badf8d45171d92387410b04639d73811b785b5161ecadabf056ea14d62d4ede", 313 | "sha256:9c120c9ce3b163b985a3b966bb701114beb1da4b0468b9b236fc754783d85aa3", 314 | "sha256:9f6f3e2598604956480f6c8aa24a3384dbf6509fe995d97f6ca6103bb8c2534e", 315 | "sha256:a1254357f7e4c82e77c348dabf2d55f1d14d19d91ff025004775e70a6ef40ada", 316 | "sha256:a1392e0638af203cee360495fd2cfdd6054711f2db5175b6e9c3c461b76f5175", 317 | "sha256:a1c311fd06ab3b10805abb72109f01a134019739bd3286b8ae1bc2fc4e50c07a", 318 | "sha256:a5cb87bdc2e5f620693148b5f8f842d293cae46c5f15a1b1bf7ceeed324a740c", 319 | "sha256:a7a7902bf75779bc12ccfc508bfb7a4c47063f748ea3de87135d433a4cca7a2f", 320 | "sha256:aad7bd686363d1ce4ee930ad39f14e1673248373f4a9d74d2b9554f06199fb58", 321 | "sha256:aafdb89fdeb5fe165043896817eccd6434aee124d5ee9b354f92cd574ba5e78f", 322 | "sha256:ae8a8843b11dc0b03b57b52793e391f0122e740de3df1474814c700d2622950a", 323 | "sha256:b00bc4619f60c853556b35f83731bd817f989cba3e97dc792bb8c97941b8053a", 324 | "sha256:b1f22a9ab44de5f082216270552aa54259db20189e68fc12484873d926426921", 325 | "sha256:b3c01c2fb081fced3bbb3da78510693dc7121bb893a1f0f5f4b48013201f362e", 326 | "sha256:b3dcd587b69bbf54fc04ca157c2323b8911033e827fffaecf0cafa5a892a0904", 327 | "sha256:b4a6db486ac8e99ae696e09efc8b2b9fea67b63c8f88ba7a1a16c24a057a0776", 328 | "sha256:bec7dd208a4182e99c5b6c501ce0b1f49de2802448d4056091f8e630b28e9a52", 329 | "sha256:c0877239307b7e69d025b73774e88e86ce82f6ba6adf98f41069d5b0b78bd1bf", 330 | "sha256:caa48fc31fc7243e50188197b5f0c4228956f97b954f76da157aae7f67269ae8", 331 | "sha256:cfe1090245c078720d250d19cb05d67e21a9cd7c257698ef139bc41cf6c27b4f", 332 | "sha256:d43002441932f9a9ea5d6f9efaa2e21458221a3a4b417a14027a1d530201ef1b", 333 | "sha256:d64728ee14e667ba27c66314b7d880b8eeb050e58ffc5fec3b7a109f8cddbd63", 334 | "sha256:d6495008733c7521a89422d7a68efa0a0122c99a5861f06020ef5b1f51f9ba7c", 335 | "sha256:d8f1ebca515a03e5654f88411420fea6380fc841d1bea08effb28184e3d4899f", 336 | "sha256:d99277877daf2efe074eae6338453a4ed54a2d93fb4678ddfe1209a0c93a2468", 337 | "sha256:da01bec0a26befab4898ed83b362993c844b9a607a86add78604186297eb047e", 338 | "sha256:db9a28c063c7c00844ae42a80203eb6d2d6bbb97070cfa00194dff40e6f545ab", 339 | "sha256:dda81e5ec82485155a19d9624cfcca9be88a405e2857354e5b089c2a982144b2", 340 | "sha256:e357571bb0efd65fd55f18db0a2fb0ed89d0bb1d41d906b138f088933ae618bb", 341 | "sha256:e544246b859f17373bed915182ab841b80849ed9cf23f1f07b73b7c58baee5fb", 342 | "sha256:e562617a45b5a9da5be4abe72b971d4f00bf8555eb29bb91ec2ef2be348cd132", 343 | "sha256:e570ffeb2170e116a5b17e83f19911020ac79d19c96f320cbfa1fa96b470185b", 344 | "sha256:e6f31a17acede6a8cd1ae2d123ce04d8cca74056c9d456075f4f6f85de055607", 345 | "sha256:e9121b4009339b0f751955baf4543a0bfd6bc3f8188f8056b1a25a2d45099934", 346 | "sha256:ebedb45b9feb7258fac0a268a3f6bec0a2ea4d9558f3d6f813f02ff3a6dc6698", 347 | "sha256:ecaac27da855b8d73f92123e5f03612b04c5632fd0a476e469dfc47cd37d6b2e", 348 | "sha256:ecdbde46235f3d560b18be0cb706c8e8ad1b965e5c13bbba7450c86064e96561", 349 | "sha256:ed550ed05540c03f0e69e6d74ad58d026de61b9eaebebbaaf8873e585cbb18de", 350 | "sha256:eeb3d3d6b399ffe55f9a04e09e635554012f1980696d6b0aca3e6cf42a17a03b", 351 | "sha256:ef337945bbd76cce390d1b2496ccf9f90b1c1242a3a7bc242ca4a9fc5993427a", 352 | "sha256:f1365e032a477c1430cfe0cf2856679529a2331426f8081172c4a74186f1d595", 353 | "sha256:f23b55eb5464468f9e0e9a9935ce3ed2a870608d5f534025cd5536bca25b1402", 354 | "sha256:f2e9072d71c1f6cfc79a36d4484c82823c560e6f5599c43c1ca6b5cdbd54f881", 355 | "sha256:f323306d0556351735b54acbf82904fe30a27b6a7147153cbe6e19aaaa2aa429", 356 | "sha256:f36a3489d9e28fe4b67be9992a23029c3cec0babc3bd9afb39f49844a8c721c5", 357 | "sha256:f64f82cc3443149292b32387086d02a6c7fb39b8781563e0ca7b8d7d9cf72bd7", 358 | "sha256:f6defd966ca3b187ec6c366604e9296f585021d922e666b99c47e78738b5666c", 359 | "sha256:f7c2b8eb9fc872e68b46eeaf835e86bccc3a58ba57d0eedc109cbb14177be531", 360 | "sha256:fa7db7558607afeccb33c0e4bf1c9a9a835e26599e76af6fe2fcea45904083a6", 361 | "sha256:fcb83175cc4936a5425dde3356f079ae03c0802bbdf8ff82c035f8a54b333521" 362 | ], 363 | "markers": "python_version >= '3.7'", 364 | "version": "==2.10.1" 365 | }, 366 | "python-dotenv": { 367 | "hashes": [ 368 | "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba", 369 | "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a" 370 | ], 371 | "version": "==1.0.0" 372 | }, 373 | "pyyaml": { 374 | "hashes": [ 375 | "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", 376 | "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", 377 | "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", 378 | "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", 379 | "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", 380 | "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", 381 | "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", 382 | "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", 383 | "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", 384 | "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", 385 | "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", 386 | "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", 387 | "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", 388 | "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", 389 | "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", 390 | "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", 391 | "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", 392 | "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", 393 | "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", 394 | "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", 395 | "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", 396 | "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", 397 | "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", 398 | "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", 399 | "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", 400 | "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", 401 | "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", 402 | "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", 403 | "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", 404 | "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", 405 | "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", 406 | "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", 407 | "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", 408 | "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", 409 | "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", 410 | "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", 411 | "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", 412 | "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", 413 | "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", 414 | "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", 415 | "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", 416 | "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", 417 | "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", 418 | "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", 419 | "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", 420 | "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", 421 | "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", 422 | "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", 423 | "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", 424 | "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" 425 | ], 426 | "version": "==6.0.1" 427 | }, 428 | "requests": { 429 | "hashes": [ 430 | "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", 431 | "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" 432 | ], 433 | "index": "pypi", 434 | "markers": "python_version >= '3.7'", 435 | "version": "==2.31.0" 436 | }, 437 | "sendblue": { 438 | "hashes": [ 439 | "sha256:9ea648e5a87eea7da5fec36b188dcac2350948c3f4538fd0fe77e91853a6bc90", 440 | "sha256:ee38a6d920dc6e054a4a18205376f4dca657ef1f1794da67c268dfea275ee201" 441 | ], 442 | "index": "pypi", 443 | "version": "==0.1.2" 444 | }, 445 | "sniffio": { 446 | "hashes": [ 447 | "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101", 448 | "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384" 449 | ], 450 | "markers": "python_version >= '3.7'", 451 | "version": "==1.3.0" 452 | }, 453 | "starlette": { 454 | "hashes": [ 455 | "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75", 456 | "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91" 457 | ], 458 | "markers": "python_version >= '3.7'", 459 | "version": "==0.27.0" 460 | }, 461 | "tqdm": { 462 | "hashes": [ 463 | "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386", 464 | "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7" 465 | ], 466 | "markers": "python_version >= '3.7'", 467 | "version": "==4.66.1" 468 | }, 469 | "typing-extensions": { 470 | "hashes": [ 471 | "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0", 472 | "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef" 473 | ], 474 | "markers": "python_version >= '3.8'", 475 | "version": "==4.8.0" 476 | }, 477 | "urllib3": { 478 | "hashes": [ 479 | "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84", 480 | "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e" 481 | ], 482 | "markers": "python_version >= '3.7'", 483 | "version": "==2.0.7" 484 | }, 485 | "uvicorn": { 486 | "extras": [ 487 | "standard" 488 | ], 489 | "hashes": [ 490 | "sha256:1f9be6558f01239d4fdf22ef8126c39cb1ad0addf76c40e760549d2c2f43ab53", 491 | "sha256:4d3cc12d7727ba72b64d12d3cc7743124074c0a69f7b201512fc50c3e3f1569a" 492 | ], 493 | "markers": "python_version >= '3.8'", 494 | "version": "==0.23.2" 495 | }, 496 | "uvloop": { 497 | "hashes": [ 498 | "sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd", 499 | "sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec", 500 | "sha256:13dfdf492af0aa0a0edf66807d2b465607d11c4fa48f4a1fd41cbea5b18e8e8b", 501 | "sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc", 502 | "sha256:271718e26b3e17906b28b67314c45d19106112067205119dddbd834c2b7ce797", 503 | "sha256:2df95fca285a9f5bfe730e51945ffe2fa71ccbfdde3b0da5772b4ee4f2e770d5", 504 | "sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2", 505 | "sha256:34175c9fd2a4bc3adc1380e1261f60306344e3407c20a4d684fd5f3be010fa3d", 506 | "sha256:45bf4c24c19fb8a50902ae37c5de50da81de4922af65baf760f7c0c42e1088be", 507 | "sha256:472d61143059c84947aa8bb74eabbace30d577a03a1805b77933d6bd13ddebbd", 508 | "sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12", 509 | "sha256:492e2c32c2af3f971473bc22f086513cedfc66a130756145a931a90c3958cb17", 510 | "sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef", 511 | "sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24", 512 | "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428", 513 | "sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1", 514 | "sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849", 515 | "sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593", 516 | "sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd", 517 | "sha256:6e3d4e85ac060e2342ff85e90d0c04157acb210b9ce508e784a944f852a40e67", 518 | "sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6", 519 | "sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3", 520 | "sha256:78ab247f0b5671cc887c31d33f9b3abfb88d2614b84e4303f1a63b46c046c8bd", 521 | "sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8", 522 | "sha256:8ca4956c9ab567d87d59d49fa3704cf29e37109ad348f2d5223c9bf761a332e7", 523 | "sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533", 524 | "sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957", 525 | "sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650", 526 | "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e", 527 | "sha256:e27f100e1ff17f6feeb1f33968bc185bf8ce41ca557deee9d9bbbffeb72030b7", 528 | "sha256:f467a5fd23b4fc43ed86342641f3936a68ded707f4627622fa3f82a120e18256" 529 | ], 530 | "version": "==0.19.0" 531 | }, 532 | "watchfiles": { 533 | "hashes": [ 534 | "sha256:02b73130687bc3f6bb79d8a170959042eb56eb3a42df3671c79b428cd73f17cc", 535 | "sha256:02d91cbac553a3ad141db016e3350b03184deaafeba09b9d6439826ee594b365", 536 | "sha256:06247538e8253975bdb328e7683f8515ff5ff041f43be6c40bff62d989b7d0b0", 537 | "sha256:08dca260e85ffae975448e344834d765983237ad6dc308231aa16e7933db763e", 538 | "sha256:0d9ac347653ebd95839a7c607608703b20bc07e577e870d824fa4801bc1cb124", 539 | "sha256:0dd5fad9b9c0dd89904bbdea978ce89a2b692a7ee8a0ce19b940e538c88a809c", 540 | "sha256:11cd0c3100e2233e9c53106265da31d574355c288e15259c0d40a4405cbae317", 541 | "sha256:18722b50783b5e30a18a8a5db3006bab146d2b705c92eb9a94f78c72beb94094", 542 | "sha256:18d5b4da8cf3e41895b34e8c37d13c9ed294954907929aacd95153508d5d89d7", 543 | "sha256:1ad7247d79f9f55bb25ab1778fd47f32d70cf36053941f07de0b7c4e96b5d235", 544 | "sha256:1b8d1eae0f65441963d805f766c7e9cd092f91e0c600c820c764a4ff71a0764c", 545 | "sha256:1bd467213195e76f838caf2c28cd65e58302d0254e636e7c0fca81efa4a2e62c", 546 | "sha256:1c9198c989f47898b2c22201756f73249de3748e0fc9de44adaf54a8b259cc0c", 547 | "sha256:1fd9a5205139f3c6bb60d11f6072e0552f0a20b712c85f43d42342d162be1235", 548 | "sha256:214cee7f9e09150d4fb42e24919a1e74d8c9b8a9306ed1474ecaddcd5479c293", 549 | "sha256:27b4035013f1ea49c6c0b42d983133b136637a527e48c132d368eb19bf1ac6aa", 550 | "sha256:3a23092a992e61c3a6a70f350a56db7197242f3490da9c87b500f389b2d01eef", 551 | "sha256:3ad692bc7792be8c32918c699638b660c0de078a6cbe464c46e1340dadb94c19", 552 | "sha256:3ccceb50c611c433145502735e0370877cced72a6c70fd2410238bcbc7fe51d8", 553 | "sha256:3d0f32ebfaa9c6011f8454994f86108c2eb9c79b8b7de00b36d558cadcedaa3d", 554 | "sha256:3f92944efc564867bbf841c823c8b71bb0be75e06b8ce45c084b46411475a915", 555 | "sha256:40bca549fdc929b470dd1dbfcb47b3295cb46a6d2c90e50588b0a1b3bd98f429", 556 | "sha256:43babacef21c519bc6631c5fce2a61eccdfc011b4bcb9047255e9620732c8097", 557 | "sha256:4566006aa44cb0d21b8ab53baf4b9c667a0ed23efe4aaad8c227bfba0bf15cbe", 558 | "sha256:49f56e6ecc2503e7dbe233fa328b2be1a7797d31548e7a193237dcdf1ad0eee0", 559 | "sha256:4c48a10d17571d1275701e14a601e36959ffada3add8cdbc9e5061a6e3579a5d", 560 | "sha256:4ea10a29aa5de67de02256a28d1bf53d21322295cb00bd2d57fcd19b850ebd99", 561 | "sha256:511f0b034120cd1989932bf1e9081aa9fb00f1f949fbd2d9cab6264916ae89b1", 562 | "sha256:51ddac60b96a42c15d24fbdc7a4bfcd02b5a29c047b7f8bf63d3f6f5a860949a", 563 | "sha256:57d430f5fb63fea141ab71ca9c064e80de3a20b427ca2febcbfcef70ff0ce895", 564 | "sha256:59137c0c6826bd56c710d1d2bda81553b5e6b7c84d5a676747d80caf0409ad94", 565 | "sha256:5a03651352fc20975ee2a707cd2d74a386cd303cc688f407296064ad1e6d1562", 566 | "sha256:5eb86c6acb498208e7663ca22dbe68ca2cf42ab5bf1c776670a50919a56e64ab", 567 | "sha256:642d66b75eda909fd1112d35c53816d59789a4b38c141a96d62f50a3ef9b3360", 568 | "sha256:6674b00b9756b0af620aa2a3346b01f8e2a3dc729d25617e1b89cf6af4a54eb1", 569 | "sha256:668c265d90de8ae914f860d3eeb164534ba2e836811f91fecc7050416ee70aa7", 570 | "sha256:66fac0c238ab9a2e72d026b5fb91cb902c146202bbd29a9a1a44e8db7b710b6f", 571 | "sha256:6c107ea3cf2bd07199d66f156e3ea756d1b84dfd43b542b2d870b77868c98c03", 572 | "sha256:6c889025f59884423428c261f212e04d438de865beda0b1e1babab85ef4c0f01", 573 | "sha256:6cb8fdc044909e2078c248986f2fc76f911f72b51ea4a4fbbf472e01d14faa58", 574 | "sha256:6e9be3ef84e2bb9710f3f777accce25556f4a71e15d2b73223788d528fcc2052", 575 | "sha256:7f762a1a85a12cc3484f77eee7be87b10f8c50b0b787bb02f4e357403cad0c0e", 576 | "sha256:83a696da8922314ff2aec02987eefb03784f473281d740bf9170181829133765", 577 | "sha256:853853cbf7bf9408b404754b92512ebe3e3a83587503d766d23e6bf83d092ee6", 578 | "sha256:8ad3fe0a3567c2f0f629d800409cd528cb6251da12e81a1f765e5c5345fd0137", 579 | "sha256:8c6ed10c2497e5fedadf61e465b3ca12a19f96004c15dcffe4bd442ebadc2d85", 580 | "sha256:8d5f400326840934e3507701f9f7269247f7c026d1b6cfd49477d2be0933cfca", 581 | "sha256:927c589500f9f41e370b0125c12ac9e7d3a2fd166b89e9ee2828b3dda20bfe6f", 582 | "sha256:9a0aa47f94ea9a0b39dd30850b0adf2e1cd32a8b4f9c7aa443d852aacf9ca214", 583 | "sha256:9b37a7ba223b2f26122c148bb8d09a9ff312afca998c48c725ff5a0a632145f7", 584 | "sha256:9c873345680c1b87f1e09e0eaf8cf6c891b9851d8b4d3645e7efe2ec20a20cc7", 585 | "sha256:9d09869f2c5a6f2d9df50ce3064b3391d3ecb6dced708ad64467b9e4f2c9bef3", 586 | "sha256:9d353c4cfda586db2a176ce42c88f2fc31ec25e50212650c89fdd0f560ee507b", 587 | "sha256:a1e3014a625bcf107fbf38eece0e47fa0190e52e45dc6eee5a8265ddc6dc5ea7", 588 | "sha256:a3b9bec9579a15fb3ca2d9878deae789df72f2b0fdaf90ad49ee389cad5edab6", 589 | "sha256:ab03a90b305d2588e8352168e8c5a1520b721d2d367f31e9332c4235b30b8994", 590 | "sha256:aff06b2cac3ef4616e26ba17a9c250c1fe9dd8a5d907d0193f84c499b1b6e6a9", 591 | "sha256:b3cab0e06143768499384a8a5efb9c4dc53e19382952859e4802f294214f36ec", 592 | "sha256:b4a21f71885aa2744719459951819e7bf5a906a6448a6b2bbce8e9cc9f2c8128", 593 | "sha256:b6d45d9b699ecbac6c7bd8e0a2609767491540403610962968d258fd6405c17c", 594 | "sha256:be6dd5d52b73018b21adc1c5d28ac0c68184a64769052dfeb0c5d9998e7f56a2", 595 | "sha256:c550a56bf209a3d987d5a975cdf2063b3389a5d16caf29db4bdddeae49f22078", 596 | "sha256:c76c635fabf542bb78524905718c39f736a98e5ab25b23ec6d4abede1a85a6a3", 597 | "sha256:c81818595eff6e92535ff32825f31c116f867f64ff8cdf6562cd1d6b2e1e8f3e", 598 | "sha256:cfb92d49dbb95ec7a07511bc9efb0faff8fe24ef3805662b8d6808ba8409a71a", 599 | "sha256:d23bcd6c8eaa6324fe109d8cac01b41fe9a54b8c498af9ce464c1aeeb99903d6", 600 | "sha256:d5b1dc0e708fad9f92c296ab2f948af403bf201db8fb2eb4c8179db143732e49", 601 | "sha256:d78f30cbe8b2ce770160d3c08cff01b2ae9306fe66ce899b73f0409dc1846c1b", 602 | "sha256:d8f57c4461cd24fda22493109c45b3980863c58a25b8bec885ca8bea6b8d4b28", 603 | "sha256:d9792dff410f266051025ecfaa927078b94cc7478954b06796a9756ccc7e14a9", 604 | "sha256:e7941bbcfdded9c26b0bf720cb7e6fd803d95a55d2c14b4bd1f6a2772230c586", 605 | "sha256:ebe684d7d26239e23d102a2bad2a358dedf18e462e8808778703427d1f584400", 606 | "sha256:ec8c8900dc5c83650a63dd48c4d1d245343f904c4b64b48798c67a3767d7e165", 607 | "sha256:f564bf68404144ea6b87a78a3f910cc8de216c6b12a4cf0b27718bf4ec38d303", 608 | "sha256:fd7ac678b92b29ba630d8c842d8ad6c555abda1b9ef044d6cc092dacbfc9719d" 609 | ], 610 | "version": "==0.21.0" 611 | }, 612 | "websockets": { 613 | "hashes": [ 614 | "sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b", 615 | "sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6", 616 | "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df", 617 | "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b", 618 | "sha256:1a9d160fd080c6285e202327aba140fc9a0d910b09e423afff4ae5cbbf1c7205", 619 | "sha256:1bf386089178ea69d720f8db6199a0504a406209a0fc23e603b27b300fdd6892", 620 | "sha256:1df2fbd2c8a98d38a66f5238484405b8d1d16f929bb7a33ed73e4801222a6f53", 621 | "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2", 622 | "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed", 623 | "sha256:23509452b3bc38e3a057382c2e941d5ac2e01e251acce7adc74011d7d8de434c", 624 | "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd", 625 | "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b", 626 | "sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931", 627 | "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30", 628 | "sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370", 629 | "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be", 630 | "sha256:2e5fc14ec6ea568200ea4ef46545073da81900a2b67b3e666f04adf53ad452ec", 631 | "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf", 632 | "sha256:3c6cc1360c10c17463aadd29dd3af332d4a1adaa8796f6b0e9f9df1fdb0bad62", 633 | "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b", 634 | "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402", 635 | "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f", 636 | "sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123", 637 | "sha256:46e71dbbd12850224243f5d2aeec90f0aaa0f2dde5aeeb8fc8df21e04d99eff9", 638 | "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603", 639 | "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45", 640 | "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558", 641 | "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4", 642 | "sha256:5f6ffe2c6598f7f7207eef9a1228b6f5c818f9f4d53ee920aacd35cec8110438", 643 | "sha256:604428d1b87edbf02b233e2c207d7d528460fa978f9e391bd8aaf9c8311de137", 644 | "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480", 645 | "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447", 646 | "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8", 647 | "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04", 648 | "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c", 649 | "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb", 650 | "sha256:7fa3d25e81bfe6a89718e9791128398a50dec6d57faf23770787ff441d851967", 651 | "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b", 652 | "sha256:8572132c7be52632201a35f5e08348137f658e5ffd21f51f94572ca6c05ea81d", 653 | "sha256:87b4aafed34653e465eb77b7c93ef058516cb5acf3eb21e42f33928616172def", 654 | "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c", 655 | "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92", 656 | "sha256:9edf3fc590cc2ec20dc9d7a45108b5bbaf21c0d89f9fd3fd1685e223771dc0b2", 657 | "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113", 658 | "sha256:a02413bc474feda2849c59ed2dfb2cddb4cd3d2f03a2fedec51d6e959d9b608b", 659 | "sha256:a1d9697f3337a89691e3bd8dc56dea45a6f6d975f92e7d5f773bc715c15dde28", 660 | "sha256:a571f035a47212288e3b3519944f6bf4ac7bc7553243e41eac50dd48552b6df7", 661 | "sha256:ab3d732ad50a4fbd04a4490ef08acd0517b6ae6b77eb967251f4c263011a990d", 662 | "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f", 663 | "sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468", 664 | "sha256:b2ee7288b85959797970114deae81ab41b731f19ebcd3bd499ae9ca0e3f1d2c8", 665 | "sha256:b81f90dcc6c85a9b7f29873beb56c94c85d6f0dac2ea8b60d995bd18bf3e2aae", 666 | "sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611", 667 | "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d", 668 | "sha256:bbe6013f9f791944ed31ca08b077e26249309639313fff132bfbf3ba105673b9", 669 | "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca", 670 | "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f", 671 | "sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2", 672 | "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077", 673 | "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2", 674 | "sha256:cbe83a6bbdf207ff0541de01e11904827540aa069293696dd528a6640bd6a5f6", 675 | "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374", 676 | "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc", 677 | "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e", 678 | "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53", 679 | "sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399", 680 | "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547", 681 | "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3", 682 | "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870", 683 | "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5", 684 | "sha256:fc4e7fa5414512b481a2483775a8e8be7803a35b30ca805afa4998a84f9fd9e8", 685 | "sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7" 686 | ], 687 | "version": "==12.0" 688 | } 689 | }, 690 | "develop": {} 691 | } 692 | -------------------------------------------------------------------------------- /app/default.ai: -------------------------------------------------------------------------------- 1 | llama2:latest -------------------------------------------------------------------------------- /app/log_conf.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | disable_existing_loggers: False 3 | formatters: 4 | default: 5 | # "()": uvicorn.logging.DefaultFormatter 6 | format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s' 7 | access: 8 | # "()": uvicorn.logging.AccessFormatter 9 | format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s' 10 | handlers: 11 | default: 12 | formatter: default 13 | class: logging.StreamHandler 14 | stream: ext://sys.stderr 15 | access: 16 | formatter: access 17 | class: logging.StreamHandler 18 | stream: ext://sys.stdout 19 | loggers: 20 | uvicorn.error: 21 | level: INFO 22 | handlers: 23 | - default 24 | propagate: no 25 | uvicorn.access: 26 | level: INFO 27 | handlers: 28 | - access 29 | propagate: no 30 | root: 31 | level: DEBUG 32 | handlers: 33 | - default 34 | propagate: no 35 | -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | import os, requests, time, openai, json, logging 2 | from pprint import pprint 3 | from typing import Union, List 4 | 5 | from fastapi import FastAPI 6 | from pydantic import BaseModel 7 | 8 | from sendblue import Sendblue 9 | 10 | SENDBLUE_API_KEY = os.environ.get("SENDBLUE_API_KEY") 11 | SENDBLUE_API_SECRET = os.environ.get("SENDBLUE_API_SECRET") 12 | openai.api_key = os.environ.get("OPENAI_API_KEY") 13 | OLLAMA_API = os.environ.get("OLLAMA_API_ENDPOINT", "http://ollama:11434/api") 14 | # could also use request.headers.get('referer') to do dynamically 15 | CALLBACK_URL = os.environ.get("CALLBACK_URL") 16 | MAX_WORDS = os.environ.get("MAX_WORDS") 17 | 18 | sendblue = Sendblue(SENDBLUE_API_KEY, SENDBLUE_API_SECRET) 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | 23 | def set_default_model(model: str): 24 | try: 25 | with open("default.ai", "w") as f: 26 | f.write(model) 27 | f.close() 28 | return 29 | except IOError: 30 | logger.error("Could not open file") 31 | exit(1) 32 | 33 | 34 | def get_default_model() -> str: 35 | try: 36 | with open("default.ai") as f: 37 | default = f.readline().strip("\n") 38 | f.close() 39 | if default != "": 40 | return default 41 | else: 42 | set_default_model("llama2:latest") 43 | return "" 44 | except IOError: 45 | logger.error("Could not open file") 46 | exit(1) 47 | 48 | 49 | def validate_model(model: str) -> bool: 50 | available_models = get_model_list() 51 | if model in available_models: 52 | return True 53 | else: 54 | return False 55 | 56 | 57 | def get_ollama_model_list() -> List[str]: 58 | available_models = [] 59 | # for i in range(0, 20): 60 | # # crude loop to wait for ollama endpoint 61 | # # this doesn't work as expected 62 | # try: 63 | # tags = requests.get(OLLAMA_API + "/tags") 64 | # tags.raise_for_status() 65 | # break 66 | # except requests.exceptions.HTTPError as e: 67 | # print("FAILED TO GET OLLAMA TAGS. " + e.args[0]) 68 | # time.sleep(2) 69 | # except ConnectionError as e: 70 | # print("FAILED TO GET OLLAMA TAGS. " + e.args[0]) 71 | # time.sleep(2) 72 | 73 | tags = requests.get(OLLAMA_API + "/tags") 74 | all_models = json.loads(tags.text) 75 | for model in all_models["models"]: 76 | available_models.append(model["name"]) 77 | return available_models 78 | 79 | 80 | def get_openai_model_list() -> List[str]: 81 | return ["gpt-3.5-turbo", "dall-e-2"] 82 | 83 | 84 | def get_model_list() -> List[str]: 85 | ollama_models = [] 86 | openai_models = [] 87 | all_models = [] 88 | if "OPENAI_API_KEY" in os.environ: 89 | # print(openai.Model.list()) 90 | openai_models = get_openai_model_list() 91 | 92 | ollama_models = get_ollama_model_list() 93 | all_models = ollama_models + openai_models 94 | return all_models 95 | 96 | 97 | DEFAULT_MODEL = get_default_model() 98 | 99 | if DEFAULT_MODEL == "": 100 | # This is probably the first run so we need to install a model 101 | if "OPENAI_API_KEY" in os.environ: 102 | print("No default model set. openai is enabled. using gpt-3.5-turbo") 103 | DEFAULT_MODEL = "gpt-3.5-turbo" 104 | else: 105 | print("No model found and openai not enabled. Installing llama2:latest") 106 | pull_data = '{"name": "llama2:latest","stream": false}' 107 | try: 108 | pull_resp = requests.post(OLLAMA_API + "/pull", data=pull_data) 109 | pull_resp.raise_for_status() 110 | except requests.exceptions.HTTPError as err: 111 | raise SystemExit(err) 112 | set_default_model("llama2:latest") 113 | DEFAULT_MODEL = "llama2:latest" 114 | 115 | if validate_model(DEFAULT_MODEL): 116 | logger.info("Using model: " + DEFAULT_MODEL) 117 | else: 118 | logger.error("Model " + DEFAULT_MODEL + " not available.") 119 | logger.info(get_model_list()) 120 | 121 | pull_data = '{"name": "' + DEFAULT_MODEL + '","stream": false}' 122 | try: 123 | pull_resp = requests.post(OLLAMA_API + "/pull", data=pull_data) 124 | pull_resp.raise_for_status() 125 | except requests.exceptions.HTTPError as err: 126 | raise SystemExit(err) 127 | 128 | 129 | def set_msg_send_style(received_msg: str): 130 | """Will return a style for the message to send based on matched words in received message""" 131 | celebration_match = ["happy"] 132 | shooting_star_match = ["star", "stars"] 133 | fireworks_match = ["celebrate", "firework"] 134 | lasers_match = ["cool", "lasers", "laser"] 135 | love_match = ["love"] 136 | confetti_match = ["yay"] 137 | balloons_match = ["party"] 138 | echo_match = ["what did you say"] 139 | invisible_match = ["quietly"] 140 | gentle_match = [] 141 | loud_match = ["hear"] 142 | slam_match = [] 143 | 144 | received_msg_lower = received_msg.lower() 145 | if any(x in received_msg_lower for x in celebration_match): 146 | return "celebration" 147 | elif any(x in received_msg_lower for x in shooting_star_match): 148 | return "shooting_star" 149 | elif any(x in received_msg_lower for x in fireworks_match): 150 | return "fireworks" 151 | elif any(x in received_msg_lower for x in lasers_match): 152 | return "lasers" 153 | elif any(x in received_msg_lower for x in love_match): 154 | return "love" 155 | elif any(x in received_msg_lower for x in confetti_match): 156 | return "confetti" 157 | elif any(x in received_msg_lower for x in balloons_match): 158 | return "balloons" 159 | elif any(x in received_msg_lower for x in echo_match): 160 | return "echo" 161 | elif any(x in received_msg_lower for x in invisible_match): 162 | return "invisible" 163 | elif any(x in received_msg_lower for x in gentle_match): 164 | return "gentle" 165 | elif any(x in received_msg_lower for x in loud_match): 166 | return "loud" 167 | elif any(x in received_msg_lower for x in slam_match): 168 | return "slam" 169 | else: 170 | return 171 | 172 | 173 | class Msg(BaseModel): 174 | accountEmail: str 175 | content: str 176 | media_url: str 177 | is_outbound: bool 178 | status: str 179 | error_code: int | None = None 180 | error_message: str | None = None 181 | message_handle: str 182 | date_sent: str 183 | date_updated: str 184 | from_number: str 185 | number: str 186 | to_number: str 187 | was_downgraded: bool | None = None 188 | plan: str 189 | 190 | 191 | class Callback(BaseModel): 192 | accountEmail: str 193 | content: str 194 | is_outbound: bool 195 | status: str 196 | error_code: int | None = None 197 | error_message: str | None = None 198 | message_handle: str 199 | date_sent: str 200 | date_updated: str 201 | from_number: str 202 | number: str 203 | to_number: str 204 | was_downgraded: bool | None = None 205 | plan: str 206 | 207 | 208 | def msg_openai(msg: Msg, model=DEFAULT_MODEL): 209 | """Sends a message to openai""" 210 | message_with_context = create_messages_from_context("openai") 211 | 212 | gpt_resp = openai.ChatCompletion.create( 213 | model=model, 214 | messages=message_with_context, 215 | ) 216 | append_context("system", gpt_resp.choices[0].message.content) 217 | msg_response = sendblue.send_message( 218 | msg.from_number, 219 | { 220 | "content": gpt_resp.choices[0].message.content, 221 | "status_callback": CALLBACK_URL, 222 | }, 223 | ) 224 | return 225 | 226 | 227 | def msg_ollama(msg: Msg, model=DEFAULT_MODEL): 228 | """Sends a message to the ollama endpoint""" 229 | ollama_headers = {"Content-Type": "application/json"} 230 | ollama_data = ( 231 | '{"model":"' 232 | + model 233 | + '", "stream": false, "prompt":"' 234 | + msg.content 235 | + " in under " 236 | + MAX_WORDS 237 | + ' words"}' 238 | ) 239 | ollama_resp = requests.post( 240 | OLLAMA_API + "/generate", headers=ollama_headers, data=ollama_data 241 | ) 242 | response_dict = json.loads(ollama_resp.text) 243 | if ollama_resp.ok: 244 | send_style = set_msg_send_style(msg.content) 245 | append_context("system", response_dict["response"]) 246 | msg_response = sendblue.send_message( 247 | msg.from_number, 248 | { 249 | "content": response_dict["response"], 250 | "status_callback": CALLBACK_URL, 251 | "send_style": send_style, 252 | }, 253 | ) 254 | else: 255 | msg_response = sendblue.send_message( 256 | msg.from_number, 257 | { 258 | "content": "I'm sorry, I had a problem processing that question. Please try again.", 259 | "status_callback": CALLBACK_URL, 260 | }, 261 | ) 262 | 263 | return 264 | 265 | 266 | def send_typing_indicator(msg: Msg): 267 | """This just sends a typing indicator to let them know we're working on a reply""" 268 | # sendblue.send_typing_indicator(msg.from_number) 269 | sb_headers = { 270 | "sb-api-key-id": os.environ.get("SENDBLUE_API_KEY"), 271 | "sb-api-secret-key": os.environ.get("SENDBLUE_API_SECRET"), 272 | "Content-Type": "application/json", 273 | } 274 | typing_data = '{"number":"' + msg.from_number + '"}' 275 | typing_resp = requests.post( 276 | "https://api.sendblue.co/api/send-typing-indicator", 277 | headers=sb_headers, 278 | data=typing_data, 279 | ) 280 | 281 | 282 | def append_context(source: str, content: str): 283 | """Appends the current content to a file to send to the model with new requests. 284 | Uses the format 285 | user,question""" 286 | MAX_CONTEXT = os.environ.get("MAX_CONTEXT", 20) 287 | f = open("context.txt", "a") 288 | f.write(source + "," + content + "\n") 289 | f.close() 290 | f = open("context.txt", "r") 291 | context = f.readlines() 292 | trunk_context = context[-abs(int(MAX_CONTEXT)) :] 293 | f.close() 294 | f = open("context.txt", "w") 295 | for line in trunk_context: 296 | f.write(line) 297 | f.close() 298 | 299 | 300 | def create_messages_from_context(provider_api: str): 301 | """Reads the context file and creates properly formatted messages""" 302 | messages = [] 303 | f = open("context.txt", "r") 304 | lines = f.readlines() 305 | if provider_api == "ollama": 306 | # generate data for ollama 307 | print("ollama context not supported") 308 | 309 | elif provider_api == "openai": 310 | # generate data for openai 311 | for line in lines: 312 | line_arr = line.split(",") 313 | # each message in the array should look like 314 | # {"role": "user|system", "content": "the message"} 315 | messages.append( 316 | '{"role":"' 317 | + line_arr[0] 318 | + '", "content": "' 319 | + ",".join(line_arr[1:]) 320 | + '"}' 321 | ) 322 | return messages 323 | 324 | 325 | def match_closest_model(model: str) -> str: 326 | """Match a model when provided incomplete info""" 327 | available_models = get_model_list() 328 | for this_model in available_models: 329 | if this_model.startswith(model): 330 | return this_model 331 | return "" 332 | 333 | 334 | app = FastAPI() 335 | 336 | print("OLLAMA API IS " + OLLAMA_API) 337 | 338 | 339 | @app.post("/msg") 340 | async def create_msg(msg: Msg): 341 | privided_model = "" 342 | logger.info(msg) 343 | 344 | # run commands 345 | if msg.content.startswith("/"): 346 | command(msg) 347 | return 348 | 349 | # change model via @ message 350 | if msg.content.startswith("@"): 351 | provided_model = msg.content.strip("@").lower().split(" ")[0] 352 | model = match_closest_model(provided_model) 353 | print("using temp model " + model + "from provided model " + provided_model) 354 | msg.content = " ".join(msg.content.split(" ")[1:]) 355 | else: 356 | model = DEFAULT_MODEL 357 | 358 | if model == "": 359 | msg_response = sendblue.send_message( 360 | msg.from_number, 361 | { 362 | "content": "Model " 363 | + provided_model 364 | + " not found. Try one of these \n" 365 | + "\n".join(get_model_list()), 366 | "status_callback": CALLBACK_URL, 367 | }, 368 | ) 369 | return 370 | 371 | # Save media files 372 | if msg.media_url != "": 373 | r = requests.get(msg.media_url, allow_redirects=True) 374 | file_name = msg.media_url.split("/")[-1] 375 | with open("media/" + file_name, "wb") as f: 376 | print("saving file " + file_name) 377 | f.write(r.content) 378 | 379 | # don't run anything if there's no text 380 | if msg.content == "": 381 | return 382 | 383 | # write the content to our context file 384 | append_context("user", msg.content) 385 | send_typing_indicator(msg) 386 | 387 | # get the models to know which model we should use 388 | openai_models = get_openai_model_list() 389 | ollama_models = get_ollama_model_list() 390 | 391 | # The model should never be in both 392 | if model in openai_models: 393 | msg_openai(msg, model=model) 394 | if model in ollama_models: 395 | msg_ollama(msg, model=model) 396 | return 397 | 398 | 399 | @app.post("/callback") 400 | async def create_callback(callback: Callback): 401 | """This is a callback URL for sendblue. It doesn't do anything except 402 | return when sendblue sends a message status""" 403 | # TODO: make this track messages 404 | logger.info(callback) 405 | return 406 | 407 | 408 | @app.get("/") 409 | def health(): 410 | """This just returns text for a health check""" 411 | return "hello" 412 | 413 | 414 | def command(msg: Msg): 415 | """This is for slash commands that can be helpful from within messages. 416 | None of these commands should interact with a model""" 417 | 418 | commands = ["help", "list", "install", "default"] 419 | cmd = msg.content.strip("/").lower().split(" ")[0] 420 | match cmd: 421 | case "help": 422 | help_response = sendblue.send_message( 423 | msg.from_number, 424 | { 425 | "content": "Available commands:\n/" + "\n/".join(commands), 426 | "status_callback": CALLBACK_URL, 427 | }, 428 | ) 429 | case "list": 430 | # list ai againts 431 | available_models = get_model_list() 432 | default_model = get_default_model() 433 | available_models = [ 434 | m.replace(default_model, default_model + "*") for m in available_models 435 | ] 436 | list_response = sendblue.send_message( 437 | msg.from_number, 438 | { 439 | "content": "Available models:\n" + "\n".join(available_models), 440 | "status_callback": CALLBACK_URL, 441 | }, 442 | ) 443 | case "install": 444 | # install ollama 445 | args = msg.content.lower().split(" ")[1] 446 | pull_data = '{"name": "' + args + '","stream": false}' 447 | install_response = sendblue.send_message( 448 | msg.from_number, 449 | {"content": "Installing " + args, "status_callback": CALLBACK_URL}, 450 | ) 451 | try: 452 | pull_resp = requests.post(OLLAMA_API + "/pull", data=pull_data) 453 | pull_resp.raise_for_status() 454 | except requests.exceptions.HTTPError as err: 455 | raise SystemExit(err) 456 | done_response = sendblue.send_message( 457 | msg.from_number, 458 | { 459 | "content": "Installed " + args + " Use it with /default", 460 | "status_callback": CALLBACK_URL, 461 | }, 462 | ) 463 | case "default": 464 | # set default model 465 | args = msg.content.lower().split(" ")[1] 466 | matched_model = match_closest_model(args) 467 | print("setting default model " + matched_model) 468 | set_default_model(matched_model) 469 | case _: 470 | help_response = sendblue.send_message( 471 | msg.from_number, 472 | { 473 | "content": "Command " + msg.content + " not available.", 474 | "status_callback": CALLBACK_URL, 475 | }, 476 | ) 477 | return 478 | 479 | 480 | if __name__ == "__main__": 481 | uvicorn.run(app, host="0.0.0.0", port=8000) 482 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | lollm: 4 | build: ./app 5 | # command: 6 | # - sleep 7 | # - 1d 8 | ports: 9 | - 8000:8000 10 | env_file: ./app/.env 11 | volumes: 12 | - ./run/lollm:/run/lollm 13 | depends_on: 14 | - ollama 15 | restart: unless-stopped 16 | #network_mode: "host" 17 | ngrok: 18 | image: ngrok/ngrok:alpine 19 | command: 20 | - "http" 21 | - "8000" 22 | - "--log" 23 | - "stdout" 24 | network_mode: "host" 25 | ollama: 26 | image: ollama/ollama 27 | ports: 28 | - 11434:11434 29 | volumes: 30 | - ./run/ollama:/home/ollama 31 | #network_mode: "host" 32 | deploy: 33 | resources: 34 | reservations: 35 | devices: 36 | - driver: nvidia 37 | count: 1 38 | capabilities: [gpu] 39 | -------------------------------------------------------------------------------- /img/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rothgar/local-llm-messenger/00c867976355eadb6562266c74a4e723ad5f41ea/img/banner.png -------------------------------------------------------------------------------- /img/lasers.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rothgar/local-llm-messenger/00c867976355eadb6562266c74a4e723ad5f41ea/img/lasers.gif -------------------------------------------------------------------------------- /img/lollm-demo-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rothgar/local-llm-messenger/00c867976355eadb6562266c74a4e723ad5f41ea/img/lollm-demo-1.gif -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | serve: 2 | cd app && uvicorn main:app \ 3 | --host 0.0.0.0 \ 4 | --port 8000 \ 5 | --env-file .env \ 6 | --reload \ 7 | --reload-include default.ai \ 8 | --log-config=log_conf.yaml 9 | -------------------------------------------------------------------------------- /test/msg.json: -------------------------------------------------------------------------------- 1 | { 2 | "accountEmail": "support@sendblue.co", 3 | "content": "Ahoy Developer!", 4 | "media_url": "some_cdn_link.png", 5 | "is_outbound": false, 6 | "status": "RECEIVED", 7 | "error_code": null, 8 | "error_message": null, 9 | "message_handle": "xxxxx", 10 | "date_sent": "2020-09-10T06:15:05.962Z", 11 | "date_updated": "2020-09-10T06:15:05.962Z", 12 | "from_number": "+19998887777", 13 | "number": "+19998887777", 14 | "to_number": "+15122164639", 15 | "was_downgraded": false, 16 | "plan": "blue" 17 | } 18 | -------------------------------------------------------------------------------- /test/ollama.json: -------------------------------------------------------------------------------- 1 | { 2 | "model": "llama2", 3 | "prompt":"Why is the sky blue?", 4 | "stream": false 5 | } 6 | --------------------------------------------------------------------------------