├── data └── README.md ├── .env ├── requirements.txt ├── main.py ├── myproject.toml ├── web.py ├── LICENSE ├── README.md ├── bot.py ├── babu_lohar.py └── poetry.lock /data/README.md: -------------------------------------------------------------------------------- 1 | # This path contains PDFs -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= 2 | PINECONE_API_KEY= 3 | PINECONE_ENV= 4 | SLACK_BOT_CLIENT= 5 | SLACK_SIGNING_SECRET= 6 | SLACK_APP_TOKEN= 7 | BOT_APP_ID= 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | langchain 2 | openai 3 | tiktoken 4 | pinecone-client 5 | chromadb 6 | pymupdf 7 | pypdf 8 | pycryptodome 9 | requests 10 | slackeventsapi 11 | slack 12 | waitress 13 | flask 14 | slackclient 15 | docx2txt 16 | youtube-transcript-api 17 | bs4 18 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | ##### main.py ##### 2 | 3 | def main(): 4 | print("[Starting script...]") 5 | from bot import start_bot_server 6 | from web import keep_alive 7 | 8 | keep_alive() 9 | start_bot_server() 10 | 11 | try: 12 | main() 13 | except ImportError: 14 | from os import system as cmd 15 | print("[+] Installing from requirements.txt") 16 | cmd("pip install -r requirements.txt") 17 | main() 18 | -------------------------------------------------------------------------------- /myproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "python-template" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Madhav Kumar "] 6 | 7 | [tool.poetry.dependencies] 8 | python = ">=3.10.0,<3.11" 9 | numpy = "^1.22.2" 10 | replit = "^3.2.4" 11 | Flask = "^2.2.0" 12 | urllib3 = "^1.26.12" 13 | slack = "^0.0.2" 14 | slackeventsapi = "^3.0.1" 15 | waitress = "^2.1.2" 16 | 17 | [tool.poetry.dev-dependencies] 18 | debugpy = "^1.6.2" 19 | replit-python-lsp-server = {extras = ["yapf", "rope", "pyflakes"], version = "^1.5.9"} 20 | 21 | [build-system] 22 | requires = ["poetry-core>=1.0.0"] 23 | build-backend = "poetry.core.masonry.api" 24 | -------------------------------------------------------------------------------- /web.py: -------------------------------------------------------------------------------- 1 | # ##### web.py ##### 2 | 3 | from flask import Flask, request, make_response 4 | from threading import Thread 5 | from waitress import serve 6 | from bot import handle_events 7 | 8 | # Initializing Flask app 9 | app = Flask(__name__) 10 | 11 | 12 | # for keeping the bot alive 13 | @app.route('/') 14 | def home(): 15 | return "Web-server for centauri slack bot in crypto<>llm is online" 16 | 17 | 18 | # listening for events from the bot 19 | @app.route("/listening", methods=["GET", "POST"]) 20 | def hears(): 21 | slack_event = request.get_json() 22 | 23 | # auth 24 | if "challenge" in slack_event: 25 | return make_response(slack_event["challenge"], 200, 26 | {"content_type": "application/json"}) 27 | 28 | # events handler 29 | if "event" in slack_event and slack_event["event"]["type"] == "message": 30 | handle_events(slack_event) 31 | 32 | # response 33 | return make_response("Event received", 200) 34 | 35 | 36 | # main run function 37 | def run(): 38 | serve(app, host="0.0.0.0", port=8080) 39 | 40 | 41 | def keep_alive(): 42 | t = Thread(target=run) 43 | t.start() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Madhav Kumar 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 | # Babu-Lohar: A Slack Bot Powered by Advanced LLM Model for Interacting with Uploaded Documents 2 | 3 | ## Introduction 4 | Babu-Lohar is a versatile Slack Bot that is powered by an advanced Large Language Model (LLM). It is designed to interact with your uploaded Documents, extract useful information, and assist in analyzing and managing content. 5 | 6 | ## Features 7 | - Documents Uploading: Upload your Documents files to the Slack channel and Babu-Lohar will automatically process them. 8 | - Chat with your documents. 9 | - Documents summarization. 10 | - Advanced Language Understanding: Powered by the latest language understanding model, Babu-Lohar can understand and execute complex commands, and even engage in casual conversation. 11 | 12 | ## Installation 13 | 14 | 1. **Clone the Repository** 15 | 16 | First, clone the Babu-Lohar repository from GitHub to your local machine. Use the following command: 17 | 18 | ``` 19 | git clone https://github.com/Madhav-MKNC/Babu-LOHAR.git; 20 | cd Babu-LOHAR/; 21 | ``` 22 | 23 | 2. **Setup Environment** 24 | 25 | Install the required dependencies for Babu-Lohar. It is recommended to use a virtual environment to keep your project organized. 26 | 27 | ``` 28 | pip install -r requirements.txt 29 | ``` 30 | 31 | 3. **Configure Your Slack Workspace** 32 | 33 | Create a new bot in your Slack workspace and obtain your `Bot User OAuth Token`. 34 | 35 | 4. **Set Up Environment Variables** 36 | 37 | You will need to set up all the environment variables mentioned in the .env file. (Make sure that this part remains as secure as possible) 38 | 39 | 5. **Run the Bot** 40 | 41 | Once you have everything set up, you can run Babu-Lohar using the following command: 42 | 43 | ``` 44 | python main.py 45 | ``` 46 | 47 | ## Usage 48 | Once Babu-Lohar is up and running, you can begin uploading Documents to your Slack workspace. The bot will automatically process any Document uploaded to channels it is a member of. 49 | 50 | To interact with Babu-Lohar, simply mention the bot in a message. 51 | 52 | ## License 53 | Babu-Lohar is licensed under the [MIT License](LICENSE.md). 54 | 55 | ## Acknowledgements 56 | We are grateful to @OpenAI for their incredible GPT models which power Babu-Lohar. Additionally, we would like to express our thanks to @hwchase17 for the Langchain framework, which has greatly contributed to the development of our project. 57 | 58 | ## Contact 59 | For any questions or concerns, please open an issue on GitHub or contact the maintainers directly. 60 | 61 | Enjoy using Babu-Lohar! 62 | -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | # ##### bot.py ##### 2 | 3 | import os 4 | from slackeventsapi import SlackEventAdapter 5 | from slack import WebClient 6 | from babu_lohar import BabuLohar 7 | import requests 8 | 9 | # Credentials 10 | slack_token = os.environ["SLACK_BOT_CLIENT"] 11 | slack_signing_secret = os.environ['SLACK_SIGNING_SECRET'] 12 | slack_app_token = os.environ["SLACK_APP_TOKEN"] 13 | openai_api_key = os.environ["OPENAI_API_KEY"] 14 | pinecone_api_key = os.environ["PINECONE_API_KEY"] 15 | pinecone_environment = os.environ["PINECONE_ENV"] 16 | BOT_CHANNEL = "C05ENBGTJUT" 17 | 18 | # Initializing BabuLohar model 19 | babulohar = BabuLohar(openai_api=openai_api_key, 20 | pinecone_api=pinecone_api_key, 21 | pinecone_env=pinecone_environment) 22 | 23 | # Initializing slack web client and slack_events_adapter 24 | slack_web_client = WebClient(token=slack_token) 25 | slack_events_adapter = SlackEventAdapter(signing_secret=slack_signing_secret, 26 | endpoint="/listening") 27 | bot_app_id = slack_web_client.api_call("auth.test")['user_id'] 28 | 29 | 30 | # send message to channel 31 | def send_message(channel="test_bot", text="I am online!"): 32 | slack_web_client.chat_postMessage(channel=channel, text=text) 33 | 34 | 35 | # reply to 36 | def bot_reply(channel, reply, thread_ts): 37 | slack_web_client.chat_postMessage(channel=channel, 38 | text=reply, 39 | thread_ts=thread_ts) 40 | 41 | 42 | # download files from attachments 43 | # allowed files: pdf, txt, doc, docx 44 | def handle_attachments(attachment, channel, ts): 45 | allowed_files = ["pdf", "txt", "doc", "docx"] 46 | 47 | uploaded_pdfs = "./uploaded" 48 | if not os.path.exists(uploaded_pdfs): 49 | os.makedirs(uploaded_pdfs) 50 | print(0) 51 | 52 | file_name = attachment.get("title") 53 | file_path = os.path.join(uploaded_pdfs, file_name) 54 | print(1) 55 | print(file_name) 56 | print(file_path) 57 | 58 | if file_name.split('.')[-1].lower() not in allowed_files: 59 | return bot_reply(channel, "Error: Filetype not allowed", ts) 60 | print(2) 61 | 62 | download_url = attachment.get("url_private") 63 | print(attachment) 64 | print(2.1) 65 | headers = {"Authorization": f"Bearer {slack_token}"} 66 | print(2.2) 67 | response = requests.get(download_url, headers=headers) 68 | print(3) 69 | 70 | if response.status_code == 200: 71 | print(31) 72 | with open(file_path, "wb") as downloaded: 73 | downloaded.write(response.content) 74 | print(f"Attachment downloaded to: {file_path}") 75 | print(f"loading {file_name}", channel, ts) 76 | # send_message(channel, "loading") 77 | # send_message(channel, babulohar.summarize(file_path)) 78 | bot_reply(channel, "Loading...", ts) 79 | bot_reply(channel, babulohar.summarize(file_path), ts) 80 | else: 81 | print(30) 82 | print("Failed to download attachment.") 83 | bot_reply(channel, f"Failed to load {file_name}", ts) 84 | print("file handled") 85 | 86 | 87 | # event handler 88 | # NOTE: jugaad for stopping multiple replies 89 | previous_timestamp = 0 90 | 91 | 92 | def handle_events(slack_event): 93 | message = slack_event["event"] 94 | channel = message["channel"] 95 | user = message.get("user") 96 | text = message.get("text") 97 | thread = message.get("ts") 98 | attachments = message.get("files") 99 | 100 | print() 101 | print(message) 102 | print() 103 | 104 | print(channel, BOT_CHANNEL) 105 | 106 | if channel != BOT_CHANNEL: # channel is BOT_CHANNEL 107 | return 108 | 109 | if user != bot_app_id: # if the sender is not bot 110 | global previous_timestamp 111 | if thread == previous_timestamp: return 112 | else: previous_timestamp = thread 113 | 114 | try: 115 | if message["blocks"][0]["elements"][0]["elements"][0][ 116 | "user_id"] == bot_app_id: 117 | if attachments: 118 | print("found attachments") 119 | handle_attachments(attachments[0], channel, thread) 120 | else: 121 | flag = False 122 | try: 123 | url = message["blocks"][0]["elements"][0]["elements"][2]["url"] 124 | flag = True 125 | except: 126 | pass 127 | 128 | if flag: # if url found, summarize the url 129 | bot_reply(channel, "Loading URL...", thread) 130 | bot_reply(channel=channel, 131 | reply=babulohar.summarize(url), 132 | thread_ts=thread) 133 | print("\nsummarized\n") 134 | else: # qna with docs from ./data 135 | bot_reply(channel=channel, 136 | reply=babulohar.get_response(text), 137 | thread_ts=thread) 138 | except Exception as e: 139 | print(e) 140 | 141 | 142 | # start bot server 143 | def start_bot_server(): 144 | send_message() 145 | slack_events_adapter.start(port=7000) 146 | 147 | 148 | # Start the server to receive Slack events 149 | if __name__ == "__main__": 150 | start_bot_server() 151 | -------------------------------------------------------------------------------- /babu_lohar.py: -------------------------------------------------------------------------------- 1 | # ##### babu_lohar.py #### 2 | 3 | import os 4 | import sys 5 | 6 | import pinecone 7 | 8 | from langchain.document_loaders import (PyPDFLoader, PyMuPDFLoader, TextLoader, 9 | Docx2txtLoader, WebBaseLoader, 10 | YoutubeLoader) 11 | 12 | from langchain.text_splitter import RecursiveCharacterTextSplitter 13 | from langchain.vectorstores import Chroma 14 | from langchain.embeddings import OpenAIEmbeddings 15 | from langchain.chat_models import ChatOpenAI 16 | from langchain import OpenAI 17 | from langchain.chains import RetrievalQA 18 | from langchain.chains.summarize import load_summarize_chain 19 | 20 | 21 | # The Model 22 | class BabuLohar: 23 | 24 | def __init__(self, openai_api='', pinecone_api='', pinecone_env=''): 25 | if openai_api == '' or pinecone_api == '' == pinecone_env == '': 26 | print("[!] API Keys not found!!") 27 | exit() 28 | 29 | # Set the OpenAI API key 30 | os.environ["OPENAI_API_KEY"] = openai_api 31 | 32 | # Initialize Pinecone 33 | pinecone.init(api_key=pinecone_api, environment=pinecone_env) 34 | 35 | # model 36 | self.documents = [] 37 | self.QA = self.process("./data") 38 | 39 | # summarizer 40 | self.summarizer = Summarizer() 41 | 42 | # Loading from a directory 43 | def load_PDFs_from_dir(self, dir_path='.'): 44 | nodata = True 45 | for file in os.listdir(dir_path): 46 | if file.endswith('.pdf'): 47 | print(f"[*] loading {file}") 48 | pdf_path = os.path.join(dir_path, file) 49 | doc = PyMuPDFLoader(pdf_path).load() 50 | self.documents.extend(doc) 51 | print(f"[+] loaded '{pdf_path}'") 52 | nodata = False 53 | 54 | if nodata: 55 | print("[!] No data Loaded") 56 | sys.exit() 57 | print(f"[+] '{dir_path}' directory loaded") 58 | return self.documents 59 | 60 | def process(self, dir_path): 61 | # Create an instance of OpenAIEmbeddings 62 | embeddings = OpenAIEmbeddings() 63 | 64 | # loading PDF documents from a directory 65 | self.load_PDFs_from_dir(dir_path=dir_path) 66 | 67 | # splitting the data 68 | text_splitter = RecursiveCharacterTextSplitter(chunk_size=512, 69 | chunk_overlap=10) 70 | self.documents = text_splitter.split_documents(self.documents) 71 | 72 | # NOTE: Using ChromaDB here 73 | # Vector Store (chromadb) 74 | vectordb = Chroma.from_documents(documents=self.documents, 75 | embedding=embeddings, 76 | persist_directory="./persistence") 77 | 78 | # Persist the vector store to the specified directory 79 | vectordb.persist() 80 | 81 | # Create a retriever from the vector store with search parameters 82 | retriever = vectordb.as_retriever( 83 | search_kwargs={"k": 1} # k=3 was throwing a WARNING 84 | ) 85 | 86 | # Create a RetrievalQA model from the - 87 | # language model, chain type, and retriever 88 | qa = RetrievalQA.from_chain_type( 89 | llm=ChatOpenAI(model_name='gpt-3.5-turbo'), 90 | chain_type="stuff", 91 | retriever=retriever) 92 | 93 | # return the model 94 | return qa 95 | 96 | # I/O 97 | def get_response(self, query='hello'): 98 | response = self.QA(query)["result"] 99 | return response 100 | 101 | # summarizer 102 | def summarize(self, content): 103 | # url input 104 | if 'http' in content: 105 | 106 | # youtube url 107 | if content.startswith("https://www.youtube.com"): 108 | return self.summarizer.summarize_yt(content) 109 | 110 | # other urls 111 | else: 112 | return self.summarizer.summarize_web(content) 113 | 114 | # file input 115 | else: 116 | return self.summarizer.summarize_file(content) 117 | 118 | 119 | # Summarizer 120 | class Summarizer: 121 | 122 | def __init__(self): 123 | self.llm = OpenAI(temperature=0) 124 | 125 | def summarize_file(self, filepath): 126 | 127 | if filepath.endswith('.pdf'): 128 | loader = PyPDFLoader(filepath) 129 | 130 | elif filepath.endswith('.txt'): 131 | loader = TextLoader(filepath) 132 | 133 | elif filepath.endswith('.doc'): 134 | loader = Docx2txtLoader(filepath) 135 | 136 | elif filepath.endswith('.docx'): 137 | loader = Docx2txtLoader(filepath) 138 | 139 | else: 140 | return "Error: This File Type is not supported" 141 | 142 | docs = loader.load_and_split() 143 | print(docs) 144 | chain = load_summarize_chain(llm=self.llm, chain_type="map_reduce") 145 | summary = chain.run(docs) 146 | return summary 147 | 148 | def summarize_web(self, url): 149 | loader = WebBaseLoader(url) 150 | docs = loader.load_and_split() 151 | print(docs) 152 | chain = load_summarize_chain(llm=self.llm, chain_type="map_reduce") 153 | summary = chain.run(docs) 154 | return summary 155 | 156 | def summarize_yt(self, url): 157 | if '=' not in url: return "Error: No YouTube video found" 158 | loader = YoutubeLoader(video_id=url.split("=")[-1]) 159 | docs = loader.load_and_split() 160 | chain = load_summarize_chain(llm=self.llm, chain_type="map_reduce") 161 | summary = chain.run(docs) 162 | return summary 163 | 164 | 165 | # main 166 | if __name__ == "__main__": 167 | x = Summarizer() 168 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "aiohttp" 3 | version = "3.8.3" 4 | description = "Async http client/server framework (asyncio)" 5 | category = "main" 6 | optional = false 7 | python-versions = ">=3.6" 8 | 9 | [package.dependencies] 10 | aiosignal = ">=1.1.2" 11 | async-timeout = ">=4.0.0a3,<5.0" 12 | attrs = ">=17.3.0" 13 | charset-normalizer = ">=2.0,<3.0" 14 | frozenlist = ">=1.1.1" 15 | multidict = ">=4.5,<7.0" 16 | yarl = ">=1.0,<2.0" 17 | 18 | [package.extras] 19 | speedups = ["aiodns", "brotli", "cchardet"] 20 | 21 | [[package]] 22 | name = "aiosignal" 23 | version = "1.3.1" 24 | description = "aiosignal: a list of registered asynchronous callbacks" 25 | category = "main" 26 | optional = false 27 | python-versions = ">=3.7" 28 | 29 | [package.dependencies] 30 | frozenlist = ">=1.1.0" 31 | 32 | [[package]] 33 | name = "argon2-cffi" 34 | version = "21.3.0" 35 | description = "The secure Argon2 password hashing algorithm." 36 | category = "main" 37 | optional = false 38 | python-versions = ">=3.6" 39 | 40 | [package.dependencies] 41 | argon2-cffi-bindings = "*" 42 | 43 | [package.extras] 44 | dev = ["pre-commit", "cogapp", "tomli", "coverage[toml] (>=5.0.2)", "hypothesis", "pytest", "sphinx", "sphinx-notfound-page", "furo"] 45 | docs = ["sphinx", "sphinx-notfound-page", "furo"] 46 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pytest"] 47 | 48 | [[package]] 49 | name = "argon2-cffi-bindings" 50 | version = "21.2.0" 51 | description = "Low-level CFFI bindings for Argon2" 52 | category = "main" 53 | optional = false 54 | python-versions = ">=3.6" 55 | 56 | [package.dependencies] 57 | cffi = ">=1.0.1" 58 | 59 | [package.extras] 60 | dev = ["pytest", "cogapp", "pre-commit", "wheel"] 61 | tests = ["pytest"] 62 | 63 | [[package]] 64 | name = "async-timeout" 65 | version = "4.0.2" 66 | description = "Timeout context manager for asyncio programs" 67 | category = "main" 68 | optional = false 69 | python-versions = ">=3.6" 70 | 71 | [[package]] 72 | name = "attrs" 73 | version = "22.2.0" 74 | description = "Classes Without Boilerplate" 75 | category = "main" 76 | optional = false 77 | python-versions = ">=3.6" 78 | 79 | [package.extras] 80 | cov = ["attrs", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] 81 | dev = ["attrs"] 82 | docs = ["furo", "sphinx", "myst-parser", "zope.interface", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] 83 | tests = ["attrs", "zope.interface"] 84 | tests-no-zope = ["hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist", "cloudpickle", "mypy (>=0.971,<0.990)", "pytest-mypy-plugins"] 85 | tests_no_zope = ["hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist", "cloudpickle", "mypy (>=0.971,<0.990)", "pytest-mypy-plugins"] 86 | 87 | [[package]] 88 | name = "certifi" 89 | version = "2022.12.7" 90 | description = "Python package for providing Mozilla's CA Bundle." 91 | category = "main" 92 | optional = false 93 | python-versions = ">=3.6" 94 | 95 | [[package]] 96 | name = "cffi" 97 | version = "1.15.1" 98 | description = "Foreign Function Interface for Python calling C code." 99 | category = "main" 100 | optional = false 101 | python-versions = "*" 102 | 103 | [package.dependencies] 104 | pycparser = "*" 105 | 106 | [[package]] 107 | name = "charset-normalizer" 108 | version = "2.1.1" 109 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 110 | category = "main" 111 | optional = false 112 | python-versions = ">=3.6.0" 113 | 114 | [package.extras] 115 | unicode_backport = ["unicodedata2"] 116 | 117 | [[package]] 118 | name = "click" 119 | version = "8.1.3" 120 | description = "Composable command line interface toolkit" 121 | category = "main" 122 | optional = false 123 | python-versions = ">=3.7" 124 | 125 | [package.dependencies] 126 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 127 | 128 | [[package]] 129 | name = "colorama" 130 | version = "0.4.6" 131 | description = "Cross-platform colored terminal text." 132 | category = "main" 133 | optional = false 134 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 135 | 136 | [[package]] 137 | name = "cryptography" 138 | version = "38.0.4" 139 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 140 | category = "main" 141 | optional = false 142 | python-versions = ">=3.6" 143 | 144 | [package.dependencies] 145 | cffi = ">=1.12" 146 | 147 | [package.extras] 148 | docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] 149 | docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] 150 | pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] 151 | sdist = ["setuptools-rust (>=0.11.4)"] 152 | ssh = ["bcrypt (>=3.1.5)"] 153 | test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] 154 | 155 | [[package]] 156 | name = "debugpy" 157 | version = "1.6.5" 158 | description = "An implementation of the Debug Adapter Protocol for Python" 159 | category = "dev" 160 | optional = false 161 | python-versions = ">=3.7" 162 | 163 | [[package]] 164 | name = "flask" 165 | version = "2.2.2" 166 | description = "A simple framework for building complex web applications." 167 | category = "main" 168 | optional = false 169 | python-versions = ">=3.7" 170 | 171 | [package.dependencies] 172 | click = ">=8.0" 173 | itsdangerous = ">=2.0" 174 | Jinja2 = ">=3.0" 175 | Werkzeug = ">=2.2.2" 176 | 177 | [package.extras] 178 | async = ["asgiref (>=3.2)"] 179 | dotenv = ["python-dotenv"] 180 | 181 | [[package]] 182 | name = "frozenlist" 183 | version = "1.3.3" 184 | description = "A list-like structure which implements collections.abc.MutableSequence" 185 | category = "main" 186 | optional = false 187 | python-versions = ">=3.7" 188 | 189 | [[package]] 190 | name = "idna" 191 | version = "3.4" 192 | description = "Internationalized Domain Names in Applications (IDNA)" 193 | category = "main" 194 | optional = false 195 | python-versions = ">=3.5" 196 | 197 | [[package]] 198 | name = "iso8601" 199 | version = "1.1.0" 200 | description = "Simple module to parse ISO 8601 dates" 201 | category = "main" 202 | optional = false 203 | python-versions = ">=3.6.2,<4.0" 204 | 205 | [[package]] 206 | name = "itsdangerous" 207 | version = "2.1.2" 208 | description = "Safely pass data to untrusted environments and back." 209 | category = "main" 210 | optional = false 211 | python-versions = ">=3.7" 212 | 213 | [[package]] 214 | name = "jedi" 215 | version = "0.18.2" 216 | description = "An autocompletion tool for Python that can be used for text editors." 217 | category = "dev" 218 | optional = false 219 | python-versions = ">=3.6" 220 | 221 | [package.dependencies] 222 | parso = ">=0.8.0,<0.9.0" 223 | 224 | [package.extras] 225 | docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx-rtd-theme (==0.4.3)", "sphinx (==1.8.5)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] 226 | qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] 227 | testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] 228 | 229 | [[package]] 230 | name = "jinja2" 231 | version = "3.1.2" 232 | description = "A very fast and expressive template engine." 233 | category = "main" 234 | optional = false 235 | python-versions = ">=3.7" 236 | 237 | [package.dependencies] 238 | MarkupSafe = ">=2.0" 239 | 240 | [package.extras] 241 | i18n = ["Babel (>=2.7)"] 242 | 243 | [[package]] 244 | name = "markupsafe" 245 | version = "2.1.2" 246 | description = "Safely add untrusted strings to HTML/XML markup." 247 | category = "main" 248 | optional = false 249 | python-versions = ">=3.7" 250 | 251 | [[package]] 252 | name = "multidict" 253 | version = "6.0.4" 254 | description = "multidict implementation" 255 | category = "main" 256 | optional = false 257 | python-versions = ">=3.7" 258 | 259 | [[package]] 260 | name = "numpy" 261 | version = "1.24.1" 262 | description = "Fundamental package for array computing in Python" 263 | category = "main" 264 | optional = false 265 | python-versions = ">=3.8" 266 | 267 | [[package]] 268 | name = "packaging" 269 | version = "23.0" 270 | description = "Core utilities for Python packages" 271 | category = "dev" 272 | optional = false 273 | python-versions = ">=3.7" 274 | 275 | [[package]] 276 | name = "parso" 277 | version = "0.8.3" 278 | description = "A Python Parser" 279 | category = "dev" 280 | optional = false 281 | python-versions = ">=3.6" 282 | 283 | [package.extras] 284 | qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] 285 | testing = ["docopt", "pytest (<6.0.0)"] 286 | 287 | [[package]] 288 | name = "passlib" 289 | version = "1.7.4" 290 | description = "comprehensive password hashing framework supporting over 30 schemes" 291 | category = "main" 292 | optional = false 293 | python-versions = "*" 294 | 295 | [package.dependencies] 296 | argon2-cffi = {version = ">=18.2.0", optional = true, markers = "extra == \"argon2\""} 297 | 298 | [package.extras] 299 | argon2 = ["argon2-cffi (>=18.2.0)"] 300 | bcrypt = ["bcrypt (>=3.1.0)"] 301 | build_docs = ["sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)", "cloud-sptheme (>=1.10.1)"] 302 | totp = ["cryptography"] 303 | 304 | [[package]] 305 | name = "platformdirs" 306 | version = "2.6.2" 307 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 308 | category = "dev" 309 | optional = false 310 | python-versions = ">=3.7" 311 | 312 | [package.extras] 313 | docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx-autodoc-typehints (>=1.19.5)", "sphinx (>=5.3)"] 314 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest (>=7.2)"] 315 | 316 | [[package]] 317 | name = "pluggy" 318 | version = "1.0.0" 319 | description = "plugin and hook calling mechanisms for python" 320 | category = "dev" 321 | optional = false 322 | python-versions = ">=3.6" 323 | 324 | [package.extras] 325 | testing = ["pytest-benchmark", "pytest"] 326 | dev = ["tox", "pre-commit"] 327 | 328 | [[package]] 329 | name = "protobuf" 330 | version = "4.21.12" 331 | description = "" 332 | category = "main" 333 | optional = false 334 | python-versions = ">=3.7" 335 | 336 | [[package]] 337 | name = "pycparser" 338 | version = "2.21" 339 | description = "C parser in Python" 340 | category = "main" 341 | optional = false 342 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 343 | 344 | [[package]] 345 | name = "pycryptodomex" 346 | version = "3.16.0" 347 | description = "Cryptographic library for Python" 348 | category = "main" 349 | optional = false 350 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 351 | 352 | [[package]] 353 | name = "pyee" 354 | version = "11.0.0" 355 | description = "A rough port of Node.js's EventEmitter to Python with a few tricks of its own" 356 | category = "main" 357 | optional = false 358 | python-versions = ">=3.8" 359 | 360 | [package.dependencies] 361 | typing-extensions = "*" 362 | 363 | [package.extras] 364 | dev = ["flake8", "flake8-black", "pytest", "black", "isort", "jupyter-console", "mkdocs", "mkdocs-include-markdown-plugin", "mkdocstrings", "toml", "tox", "trio", "twine", "twisted", "validate-pyproject", "trio", "trio-typing", "pytest-asyncio", "pytest-trio"] 365 | 366 | [[package]] 367 | name = "pyflakes" 368 | version = "2.5.0" 369 | description = "passive checker of Python programs" 370 | category = "dev" 371 | optional = false 372 | python-versions = ">=3.6" 373 | 374 | [[package]] 375 | name = "pyseto" 376 | version = "1.7.0" 377 | description = "A Python implementation of PASETO/PASERK." 378 | category = "main" 379 | optional = false 380 | python-versions = ">=3.7,<4.0" 381 | 382 | [package.dependencies] 383 | cryptography = ">=36,<39" 384 | iso8601 = ">=1.0.2,<2.0.0" 385 | passlib = {version = ">=1.7.4,<2.0.0", extras = ["argon2"]} 386 | pycryptodomex = ">=3.12.0,<4.0.0" 387 | 388 | [package.extras] 389 | docs = ["Sphinx[docs] (>=4.3.2,<6.0.0)", "sphinx-autodoc-typehints[docs] (==1.12.0)", "sphinx-rtd-theme[docs] (>=1.0.0,<2.0.0)"] 390 | 391 | [[package]] 392 | name = "python-lsp-jsonrpc" 393 | version = "1.0.0" 394 | description = "JSON RPC 2.0 server library" 395 | category = "dev" 396 | optional = false 397 | python-versions = "*" 398 | 399 | [package.dependencies] 400 | ujson = ">=3.0.0" 401 | 402 | [package.extras] 403 | test = ["coverage", "pytest-cov", "pytest", "pyflakes", "pycodestyle", "pylint"] 404 | 405 | [[package]] 406 | name = "pytoolconfig" 407 | version = "1.2.5" 408 | description = "Python tool configuration" 409 | category = "dev" 410 | optional = false 411 | python-versions = ">=3.7" 412 | 413 | [package.dependencies] 414 | packaging = ">=22.0" 415 | platformdirs = {version = ">=1.4.4", optional = true, markers = "extra == \"global\""} 416 | tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} 417 | 418 | [package.extras] 419 | doc = ["tabulate (>=0.8.9)", "sphinx (>=4.5.0)"] 420 | gendocs = ["sphinx (>=4.5.0)", "sphinx-autodoc-typehints (>=1.18.1)", "sphinx-rtd-theme (>=1.0.0)", "pytoolconfig"] 421 | global = ["platformdirs (>=1.4.4)"] 422 | validation = ["pydantic (>=1.7.4)"] 423 | 424 | [[package]] 425 | name = "replit" 426 | version = "3.2.5" 427 | description = "A library for interacting with features of repl.it" 428 | category = "main" 429 | optional = false 430 | python-versions = ">=3.8,<4.0" 431 | 432 | [package.dependencies] 433 | aiohttp = ">=3.6.2,<4.0.0" 434 | Flask = ">=2.0.0,<3.0.0" 435 | protobuf = ">=4.21.8,<5.0.0" 436 | pyseto = ">=1.6.11,<2.0.0" 437 | requests = ">=2.25.1,<3.0.0" 438 | typing_extensions = ">=3.7.4,<4.0.0" 439 | Werkzeug = ">=2.0.0,<3.0.0" 440 | 441 | [[package]] 442 | name = "replit-python-lsp-server" 443 | version = "1.15.9" 444 | description = "Python Language Server for the Language Server Protocol" 445 | category = "dev" 446 | optional = false 447 | python-versions = ">=3.7" 448 | 449 | [package.dependencies] 450 | jedi = ">=0.17.2,<0.19.0" 451 | pluggy = ">=1.0.0" 452 | pyflakes = {version = ">=2.5.0,<2.6.0", optional = true, markers = "extra == \"pyflakes\""} 453 | python-lsp-jsonrpc = ">=1.0.0" 454 | rope = {version = ">0.10.5", optional = true, markers = "extra == \"rope\""} 455 | toml = ">=0.10.2" 456 | ujson = ">=3.0.0" 457 | whatthepatch = {version = ">=1.0.2,<2.0.0", optional = true, markers = "extra == \"yapf\""} 458 | yapf = {version = "*", optional = true, markers = "extra == \"yapf\""} 459 | 460 | [package.extras] 461 | all = ["autopep8 (>=1.6.0,<1.7.0)", "flake8 (>=5.0.0,<5.1.0)", "mccabe (>=0.7.0,<0.8.0)", "pycodestyle (>=2.9.0,<2.10.0)", "pydocstyle (>=2.0.0)", "pyflakes (>=2.5.0,<2.6.0)", "pylint (>=2.5.0)", "rope (>=0.10.5)", "yapf", "whatthepatch"] 462 | autopep8 = ["autopep8 (>=1.6.0,<1.7.0)"] 463 | flake8 = ["flake8 (>=5.0.0,<5.1.0)"] 464 | mccabe = ["mccabe (>=0.7.0,<0.8.0)"] 465 | pycodestyle = ["pycodestyle (>=2.9.0,<2.10.0)"] 466 | pydocstyle = ["pydocstyle (>=2.0.0)"] 467 | pyflakes = ["pyflakes (>=2.5.0,<2.6.0)"] 468 | pylint = ["pylint (>=2.5.0)"] 469 | rope = ["rope (>0.10.5)"] 470 | test = ["pylint (>=2.5.0)", "pytest", "pytest-cov", "coverage", "numpy (<1.23)", "pandas", "matplotlib", "pyqt5", "flaky"] 471 | websockets = ["websockets (>=10.3)"] 472 | yapf = ["yapf", "whatthepatch (>=1.0.2,<2.0.0)"] 473 | 474 | [[package]] 475 | name = "requests" 476 | version = "2.28.2" 477 | description = "Python HTTP for Humans." 478 | category = "main" 479 | optional = false 480 | python-versions = ">=3.7, <4" 481 | 482 | [package.dependencies] 483 | certifi = ">=2017.4.17" 484 | charset-normalizer = ">=2,<4" 485 | idna = ">=2.5,<4" 486 | urllib3 = ">=1.21.1,<1.27" 487 | 488 | [package.extras] 489 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 490 | use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] 491 | 492 | [[package]] 493 | name = "rope" 494 | version = "1.7.0" 495 | description = "a python refactoring library..." 496 | category = "dev" 497 | optional = false 498 | python-versions = ">=3.7" 499 | 500 | [package.dependencies] 501 | pytoolconfig = {version = ">=1.2.2", extras = ["global"]} 502 | 503 | [package.extras] 504 | dev = ["pytest (>=7.0.1)", "pytest-timeout (>=2.1.0)", "build (>=0.7.0)", "pre-commit (>=2.20.0)"] 505 | doc = ["pytoolconfig", "sphinx (>=4.5.0)", "sphinx-autodoc-typehints (>=1.18.1)", "sphinx-rtd-theme (>=1.0.0)"] 506 | 507 | [[package]] 508 | name = "slack" 509 | version = "0.0.2" 510 | description = "a DI container" 511 | category = "main" 512 | optional = false 513 | python-versions = "*" 514 | 515 | [[package]] 516 | name = "slackeventsapi" 517 | version = "3.0.1" 518 | description = "Python Slack Events API adapter for Flask" 519 | category = "main" 520 | optional = false 521 | python-versions = ">=3.6" 522 | 523 | [package.dependencies] 524 | flask = ">=2,<3" 525 | pyee = ">=8" 526 | 527 | [[package]] 528 | name = "toml" 529 | version = "0.10.2" 530 | description = "Python Library for Tom's Obvious, Minimal Language" 531 | category = "dev" 532 | optional = false 533 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 534 | 535 | [[package]] 536 | name = "tomli" 537 | version = "2.0.1" 538 | description = "A lil' TOML parser" 539 | category = "dev" 540 | optional = false 541 | python-versions = ">=3.7" 542 | 543 | [[package]] 544 | name = "typing-extensions" 545 | version = "3.10.0.2" 546 | description = "Backported and Experimental Type Hints for Python 3.5+" 547 | category = "main" 548 | optional = false 549 | python-versions = "*" 550 | 551 | [[package]] 552 | name = "ujson" 553 | version = "5.7.0" 554 | description = "Ultra fast JSON encoder and decoder for Python" 555 | category = "dev" 556 | optional = false 557 | python-versions = ">=3.7" 558 | 559 | [[package]] 560 | name = "urllib3" 561 | version = "1.26.14" 562 | description = "HTTP library with thread-safe connection pooling, file post, and more." 563 | category = "main" 564 | optional = false 565 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 566 | 567 | [package.extras] 568 | brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] 569 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "urllib3-secure-extra", "ipaddress"] 570 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 571 | 572 | [[package]] 573 | name = "waitress" 574 | version = "2.1.2" 575 | description = "Waitress WSGI server" 576 | category = "main" 577 | optional = false 578 | python-versions = ">=3.7.0" 579 | 580 | [package.extras] 581 | docs = ["Sphinx (>=1.8.1)", "docutils", "pylons-sphinx-themes (>=1.0.9)"] 582 | testing = ["pytest", "pytest-cover", "coverage (>=5.0)"] 583 | 584 | [[package]] 585 | name = "werkzeug" 586 | version = "2.2.2" 587 | description = "The comprehensive WSGI web application library." 588 | category = "main" 589 | optional = false 590 | python-versions = ">=3.7" 591 | 592 | [package.dependencies] 593 | MarkupSafe = ">=2.1.1" 594 | 595 | [package.extras] 596 | watchdog = ["watchdog"] 597 | 598 | [[package]] 599 | name = "whatthepatch" 600 | version = "1.0.3" 601 | description = "A patch parsing and application library." 602 | category = "dev" 603 | optional = false 604 | python-versions = ">=3.7" 605 | 606 | [[package]] 607 | name = "yapf" 608 | version = "0.32.0" 609 | description = "A formatter for Python code." 610 | category = "dev" 611 | optional = false 612 | python-versions = "*" 613 | 614 | [[package]] 615 | name = "yarl" 616 | version = "1.8.2" 617 | description = "Yet another URL library" 618 | category = "main" 619 | optional = false 620 | python-versions = ">=3.7" 621 | 622 | [package.dependencies] 623 | idna = ">=2.0" 624 | multidict = ">=4.0" 625 | 626 | [metadata] 627 | lock-version = "1.1" 628 | python-versions = ">=3.10.0,<3.11" 629 | content-hash = "53443e8a28045c2eca082aa80e83cb0e00a2a52ef360a7d730c4f0804706d425" 630 | 631 | [metadata.files] 632 | aiohttp = [] 633 | aiosignal = [] 634 | argon2-cffi = [] 635 | argon2-cffi-bindings = [] 636 | async-timeout = [] 637 | attrs = [] 638 | certifi = [] 639 | cffi = [] 640 | charset-normalizer = [] 641 | click = [] 642 | colorama = [] 643 | cryptography = [] 644 | debugpy = [] 645 | flask = [] 646 | frozenlist = [] 647 | idna = [] 648 | iso8601 = [] 649 | itsdangerous = [] 650 | jedi = [] 651 | jinja2 = [] 652 | markupsafe = [] 653 | multidict = [] 654 | numpy = [] 655 | packaging = [] 656 | parso = [] 657 | passlib = [] 658 | platformdirs = [] 659 | pluggy = [] 660 | protobuf = [] 661 | pycparser = [] 662 | pycryptodomex = [] 663 | pyee = [] 664 | pyflakes = [] 665 | pyseto = [] 666 | python-lsp-jsonrpc = [] 667 | pytoolconfig = [] 668 | replit = [] 669 | replit-python-lsp-server = [] 670 | requests = [] 671 | rope = [] 672 | slack = [] 673 | slackeventsapi = [] 674 | toml = [] 675 | tomli = [] 676 | typing-extensions = [] 677 | ujson = [] 678 | urllib3 = [] 679 | waitress = [] 680 | werkzeug = [] 681 | whatthepatch = [] 682 | yapf = [] 683 | yarl = [] 684 | --------------------------------------------------------------------------------