yinong333 commited on
Commit
c64623a
1 Parent(s): 8baec16

Deploying APP

Browse files
Files changed (6) hide show
  1. .chainlit/config.toml +84 -0
  2. .gitignore +160 -0
  3. Dockerfile +11 -0
  4. app.py +208 -0
  5. chainlit.md +14 -0
  6. requirements.txt +99 -0
.chainlit/config.toml ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ # Whether to enable telemetry (default: true). No personal data is collected.
3
+ enable_telemetry = true
4
+
5
+ # List of environment variables to be provided by each user to use the app.
6
+ user_env = []
7
+
8
+ # Duration (in seconds) during which the session is saved when the connection is lost
9
+ session_timeout = 3600
10
+
11
+ # Enable third parties caching (e.g LangChain cache)
12
+ cache = false
13
+
14
+ # Follow symlink for asset mount (see https://github.com/Chainlit/chainlit/issues/317)
15
+ # follow_symlink = false
16
+
17
+ [features]
18
+ # Show the prompt playground
19
+ prompt_playground = true
20
+
21
+ # Process and display HTML in messages. This can be a security risk (see https://stackoverflow.com/questions/19603097/why-is-it-dangerous-to-render-user-generated-html-or-javascript)
22
+ unsafe_allow_html = false
23
+
24
+ # Process and display mathematical expressions. This can clash with "$" characters in messages.
25
+ latex = false
26
+
27
+ # Authorize users to upload files with messages
28
+ multi_modal = true
29
+
30
+ # Allows user to use speech to text
31
+ [features.speech_to_text]
32
+ enabled = false
33
+ # See all languages here https://github.com/JamesBrill/react-speech-recognition/blob/HEAD/docs/API.md#language-string
34
+ # language = "en-US"
35
+
36
+ [UI]
37
+ # Name of the app and chatbot.
38
+ name = "Chatbot"
39
+
40
+ # Show the readme while the conversation is empty.
41
+ show_readme_as_default = true
42
+
43
+ # Description of the app and chatbot. This is used for HTML tags.
44
+ # description = ""
45
+
46
+ # Large size content are by default collapsed for a cleaner ui
47
+ default_collapse_content = true
48
+
49
+ # The default value for the expand messages settings.
50
+ default_expand_messages = false
51
+
52
+ # Hide the chain of thought details from the user in the UI.
53
+ hide_cot = false
54
+
55
+ # Link to your github repo. This will add a github button in the UI's header.
56
+ # github = ""
57
+
58
+ # Specify a CSS file that can be used to customize the user interface.
59
+ # The CSS file can be served from the public directory or via an external link.
60
+ # custom_css = "/public/test.css"
61
+
62
+ # Override default MUI light theme. (Check theme.ts)
63
+ [UI.theme.light]
64
+ #background = "#FAFAFA"
65
+ #paper = "#FFFFFF"
66
+
67
+ [UI.theme.light.primary]
68
+ #main = "#F80061"
69
+ #dark = "#980039"
70
+ #light = "#FFE7EB"
71
+
72
+ # Override default MUI dark theme. (Check theme.ts)
73
+ [UI.theme.dark]
74
+ #background = "#FAFAFA"
75
+ #paper = "#FFFFFF"
76
+
77
+ [UI.theme.dark.primary]
78
+ #main = "#F80061"
79
+ #dark = "#980039"
80
+ #light = "#FFE7EB"
81
+
82
+
83
+ [meta]
84
+ generated_by = "0.7.700"
.gitignore ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # poetry
98
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102
+ #poetry.lock
103
+
104
+ # pdm
105
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106
+ #pdm.lock
107
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108
+ # in version control.
109
+ # https://pdm.fming.dev/#use-with-ide
110
+ .pdm.toml
111
+
112
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113
+ __pypackages__/
114
+
115
+ # Celery stuff
116
+ celerybeat-schedule
117
+ celerybeat.pid
118
+
119
+ # SageMath parsed files
120
+ *.sage.py
121
+
122
+ # Environments
123
+ .env
124
+ .venv
125
+ env/
126
+ venv/
127
+ ENV/
128
+ env.bak/
129
+ venv.bak/
130
+
131
+ # Spyder project settings
132
+ .spyderproject
133
+ .spyproject
134
+
135
+ # Rope project settings
136
+ .ropeproject
137
+
138
+ # mkdocs documentation
139
+ /site
140
+
141
+ # mypy
142
+ .mypy_cache/
143
+ .dmypy.json
144
+ dmypy.json
145
+
146
+ # Pyre type checker
147
+ .pyre/
148
+
149
+ # pytype static type analyzer
150
+ .pytype/
151
+
152
+ # Cython debug symbols
153
+ cython_debug/
154
+
155
+ # PyCharm
156
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
159
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
160
+ #.idea/
Dockerfile ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9
2
+ RUN useradd -m -u 1000 user
3
+ USER user
4
+ ENV HOME=/home/user \
5
+ PATH=/home/user/.local/bin:$PATH
6
+ WORKDIR $HOME/app
7
+ COPY --chown=user . $HOME/app
8
+ COPY ./requirements.txt ~/app/requirements.txt
9
+ RUN pip install -r requirements.txt
10
+ COPY . .
11
+ CMD ["chainlit", "run", "app.py", "--port", "7860"]
app.py ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ### Import Section ###
2
+ """
3
+ IMPORTS HERE
4
+ """
5
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
6
+ import chainlit as cl
7
+ from langchain_community.document_loaders import PyMuPDFLoader
8
+ from langchain_core.prompts import ChatPromptTemplate
9
+ from qdrant_client import QdrantClient
10
+ from qdrant_client.http.models import Distance, VectorParams
11
+ from langchain_openai.embeddings import OpenAIEmbeddings
12
+ from langchain.storage import LocalFileStore
13
+ from langchain_qdrant import QdrantVectorStore
14
+ from langchain.embeddings import CacheBackedEmbeddings
15
+ from langchain_core.globals import set_llm_cache
16
+ from langchain_openai import ChatOpenAI
17
+ from langchain_core.caches import InMemoryCache
18
+ from operator import itemgetter
19
+ from langchain_core.runnables.passthrough import RunnablePassthrough
20
+ from langchain.memory import ChatMessageHistory, ConversationBufferMemory
21
+ from chainlit.types import AskFileResponse
22
+ from langchain.chains import (
23
+ ConversationalRetrievalChain,
24
+ )
25
+ import os
26
+ import uuid
27
+
28
+ ### Global Section ###
29
+ """
30
+ GLOBAL CODE HERE
31
+ """
32
+ text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
33
+ Loader = PyMuPDFLoader
34
+ set_llm_cache(InMemoryCache())
35
+
36
+
37
+ core_embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
38
+ rag_system_prompt_template = """\
39
+ You are a helpful assistant that uses the provided context to answer questions. Never reference this prompt, or the existance of context.
40
+ """
41
+
42
+ rag_message_list = [
43
+ {"role" : "system", "content" : rag_system_prompt_template},
44
+ ]
45
+
46
+ rag_user_prompt_template = """\
47
+ Question:
48
+ {question}
49
+ Context:
50
+ {context}
51
+ """
52
+
53
+ chat_prompt = ChatPromptTemplate.from_messages([
54
+ ("system", rag_system_prompt_template),
55
+ ("human", rag_user_prompt_template)
56
+ ])
57
+ chat_model = ChatOpenAI(model="gpt-4o-mini")
58
+
59
+ def process_file(file: AskFileResponse):
60
+ import tempfile
61
+
62
+ with tempfile.NamedTemporaryFile(mode="w", delete=False) as tempfile:
63
+ with open(tempfile.name, "wb") as f:
64
+ f.write(file.content)
65
+ loader = Loader(tempfile.name)
66
+ documents = loader.load()
67
+ docs = text_splitter.split_documents(documents)
68
+ for i, doc in enumerate(docs):
69
+ doc.metadata["source"] = f"source_{i}"
70
+ return docs
71
+
72
+
73
+ ### On Chat Start (Session Start) Section ###
74
+ @cl.on_chat_start
75
+ async def on_chat_start():
76
+ """ SESSION SPECIFIC CODE HERE """
77
+ #file_path = "https://arxiv.org/pdf/2106.09685"
78
+ #loader = Loader(file_path)
79
+ #documents = loader.load()
80
+ #docs = text_splitter.split_documents(documents)
81
+ #for i, doc in enumerate(docs):
82
+ #doc.metadata["source"] = f"source_{i}"
83
+
84
+ files = None
85
+
86
+ # Wait for the user to upload a file
87
+ while files == None:
88
+ files = await cl.AskFileMessage(
89
+ content="Please upload a PDF file to begin!",
90
+ accept=["application/pdf"],
91
+ max_size_mb=20,
92
+ timeout=180,
93
+ ).send()
94
+
95
+ file = files[0]
96
+ msg = cl.Message(
97
+ content=f"Processing `{file.name}`...", disable_human_feedback=True
98
+ )
99
+ await msg.send()
100
+
101
+ # load the file
102
+ docs = process_file(file)
103
+
104
+ # Create a unique cache for each user
105
+ user_id = str(uuid.uuid4()) # Unique ID per user
106
+ cache_path = f"./cache/user_{user_id}/"
107
+ os.makedirs(cache_path, exist_ok=True)
108
+
109
+ store = LocalFileStore(cache_path)
110
+ cached_embedder = CacheBackedEmbeddings.from_bytes_store(
111
+ core_embeddings, store, namespace=f"user_{user_id}"
112
+ )
113
+
114
+ # Typical QDrant Vector Store Set-up
115
+ collection_name = f"pdf_to_parse_{user_id}"
116
+ client = QdrantClient(":memory:")
117
+ client.create_collection(
118
+ collection_name=collection_name,
119
+ vectors_config=VectorParams(size=1536, distance=Distance.COSINE),
120
+ )
121
+
122
+ vectorstore = QdrantVectorStore(
123
+ client=client,
124
+ collection_name=collection_name,
125
+ embedding=cached_embedder)
126
+ vectorstore.add_documents(docs)
127
+ rv = vectorstore.as_retriever(search_type="mmr", search_kwargs={"k": 3})
128
+
129
+ # Let the user know that the system is ready
130
+ # msg = cl.Message(
131
+ # content=f"Welcome to the AI Legal Chatbot! Ask me anything about the AI policy", disable_human_feedback=True, author="Chat AI"
132
+ # )
133
+ # await msg.send()
134
+
135
+ message_history = ChatMessageHistory()
136
+
137
+ memory = ConversationBufferMemory(
138
+ memory_key="chat_history",
139
+ output_key="answer",
140
+ chat_memory=message_history,
141
+ return_messages=True,
142
+ )
143
+
144
+ # Create a chain that uses the Qdrant vector store
145
+ retrieval_augmented_qa_chain = ConversationalRetrievalChain.from_llm(
146
+ ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0, streaming=True),
147
+ chain_type="stuff",
148
+ retriever=rv,
149
+ memory=memory,
150
+ return_source_documents=True,
151
+ )
152
+
153
+ msg.content = f"Processing `{file.name}` done. You can now ask questions!"
154
+ await msg.update()
155
+
156
+ # retrieval_augmented_qa_chain = (
157
+ # {"context": itemgetter("question") | retriever, "question": itemgetter("question")}
158
+ # | RunnablePassthrough.assign(context=itemgetter("context"))
159
+ # | chat_prompt | chat_model
160
+ # )
161
+ cl.user_session.set("chain", retrieval_augmented_qa_chain)
162
+
163
+ ### Rename Chains ###
164
+ @cl.author_rename
165
+ def rename(orig_author: str):
166
+ """ RENAME CODE HERE """
167
+ user_id = cl.user_session.get("user_id") # Retrieve the user_id from the session
168
+ if not user_id:
169
+ # In case the user_id is not stored yet, generate one
170
+ user_id = str(uuid.uuid4())
171
+ cl.user_session.set("user_id", user_id)
172
+
173
+ # Append or modify the original author name with the user-specific ID
174
+ new_author_name = f"{orig_author}_user_{user_id}"
175
+ return new_author_name
176
+
177
+ ### On Message Section ###
178
+ @cl.on_message
179
+ async def main(message: cl.Message):
180
+ """
181
+ MESSAGE CODE HERE
182
+ """
183
+ chain = cl.user_session.get("chain") # type: ConversationalRetrievalChain
184
+ cb = cl.AsyncLangchainCallbackHandler()
185
+
186
+ #res = await chain.acall(message.content, callbacks=[cb])
187
+ res = await chain.acall(message.content, callbacks=[cb])
188
+ answer = res["answer"]
189
+ source_documents = res["source_documents"] # type: List[Document]
190
+
191
+ text_elements = [] # type: List[cl.Text]
192
+
193
+ if source_documents:
194
+ for source_idx, source_doc in enumerate(source_documents):
195
+ source_name = f"source_{source_idx}"
196
+ # Create the text element referenced in the message
197
+ text_elements.append(
198
+ cl.Text(content=source_doc.page_content, name=source_name)
199
+ )
200
+ source_names = [text_el.name for text_el in text_elements]
201
+
202
+ if source_names:
203
+ answer += f"\nSources: {', '.join(source_names)}"
204
+ else:
205
+ answer += "\nNo sources found"
206
+
207
+ # Send the response to the user
208
+ await cl.Message(content=answer, elements=text_elements, author="bot_for").send()
chainlit.md ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Welcome to Chainlit! 🚀🤖
2
+
3
+ Hi there, Developer! 👋 We're excited to have you on board. Chainlit is a powerful tool designed to help you prototype, debug and share applications built on top of LLMs.
4
+
5
+ ## Useful Links 🔗
6
+
7
+ - **Documentation:** Get started with our comprehensive [Chainlit Documentation](https://docs.chainlit.io) 📚
8
+ - **Discord Community:** Join our friendly [Chainlit Discord](https://discord.gg/k73SQ3FyUh) to ask questions, share your projects, and connect with other developers! 💬
9
+
10
+ We can't wait to see what you create with Chainlit! Happy coding! 💻😊
11
+
12
+ ## Welcome screen
13
+
14
+ To modify the welcome screen, edit the `chainlit.md` file at the root of your project. If you do not want a welcome screen, just leave this file empty.
requirements.txt ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ aiofiles==23.2.1
2
+ aiohappyeyeballs==2.4.3
3
+ aiohttp==3.10.8
4
+ aiosignal==1.3.1
5
+ annotated-types==0.7.0
6
+ anyio==3.7.1
7
+ async-timeout==4.0.3
8
+ asyncer==0.0.2
9
+ attrs==24.2.0
10
+ bidict==0.23.1
11
+ certifi==2024.8.30
12
+ chainlit==0.7.700
13
+ charset-normalizer==3.3.2
14
+ click==8.1.7
15
+ dataclasses-json==0.5.14
16
+ Deprecated==1.2.14
17
+ distro==1.9.0
18
+ exceptiongroup==1.2.2
19
+ fastapi==0.100.1
20
+ fastapi-socketio==0.0.10
21
+ filetype==1.2.0
22
+ frozenlist==1.4.1
23
+ googleapis-common-protos==1.65.0
24
+ greenlet==3.1.1
25
+ grpcio==1.66.2
26
+ grpcio-tools==1.62.3
27
+ h11==0.14.0
28
+ h2==4.1.0
29
+ hpack==4.0.0
30
+ httpcore==0.17.3
31
+ httpx==0.24.1
32
+ hyperframe==6.0.1
33
+ idna==3.10
34
+ importlib_metadata==8.4.0
35
+ jiter==0.5.0
36
+ jsonpatch==1.33
37
+ jsonpointer==3.0.0
38
+ langchain==0.3.0
39
+ langchain-community==0.3.0
40
+ langchain-core==0.3.1
41
+ langchain-openai==0.2.0
42
+ langchain-qdrant==0.1.4
43
+ langchain-text-splitters==0.3.0
44
+ langsmith==0.1.121
45
+ Lazify==0.4.0
46
+ marshmallow==3.22.0
47
+ multidict==6.1.0
48
+ mypy-extensions==1.0.0
49
+ nest-asyncio==1.6.0
50
+ numpy==1.26.4
51
+ openai==1.51.0
52
+ opentelemetry-api==1.27.0
53
+ opentelemetry-exporter-otlp==1.27.0
54
+ opentelemetry-exporter-otlp-proto-common==1.27.0
55
+ opentelemetry-exporter-otlp-proto-grpc==1.27.0
56
+ opentelemetry-exporter-otlp-proto-http==1.27.0
57
+ opentelemetry-instrumentation==0.48b0
58
+ opentelemetry-proto==1.27.0
59
+ opentelemetry-sdk==1.27.0
60
+ opentelemetry-semantic-conventions==0.48b0
61
+ orjson==3.10.7
62
+ packaging==23.2
63
+ portalocker==2.10.1
64
+ protobuf==4.25.5
65
+ pydantic==2.9.2
66
+ pydantic-settings==2.5.2
67
+ pydantic_core==2.23.4
68
+ PyJWT==2.9.0
69
+ PyMuPDF==1.24.10
70
+ PyMuPDFb==1.24.10
71
+ python-dotenv==1.0.1
72
+ python-engineio==4.9.1
73
+ python-graphql-client==0.4.3
74
+ python-multipart==0.0.6
75
+ python-socketio==5.11.4
76
+ PyYAML==6.0.2
77
+ qdrant-client==1.11.2
78
+ regex==2024.9.11
79
+ requests==2.32.3
80
+ simple-websocket==1.0.0
81
+ sniffio==1.3.1
82
+ SQLAlchemy==2.0.35
83
+ starlette==0.27.0
84
+ syncer==2.0.3
85
+ tenacity==8.5.0
86
+ tiktoken==0.7.0
87
+ tomli==2.0.1
88
+ tqdm==4.66.5
89
+ typing-inspect==0.9.0
90
+ typing_extensions==4.12.2
91
+ uptrace==1.26.0
92
+ urllib3==2.2.3
93
+ uvicorn==0.23.2
94
+ watchfiles==0.20.0
95
+ websockets==13.1
96
+ wrapt==1.16.0
97
+ wsproto==1.2.0
98
+ yarl==1.13.1
99
+ zipp==3.20.2