Spaces:
Runtime error
Runtime error
Upload folder using huggingface_hub
Browse files- chatmodel.py +89 -0
- interactive_test.py +46 -24
- models.py +19 -8
- requirements.txt +60 -1
chatmodel.py
ADDED
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Dict, List, Literal, TypedDict
|
2 |
+
|
3 |
+
|
4 |
+
from models import Model
|
5 |
+
from pybars import Compiler
|
6 |
+
compiler = Compiler()
|
7 |
+
|
8 |
+
class Turn(TypedDict):
|
9 |
+
role: Literal["user", "assistant", "system"]
|
10 |
+
content: str
|
11 |
+
|
12 |
+
def chatmsg(message:str, role:Literal["user", "assistant", "system"]):
|
13 |
+
return {"role": role, "content": message}
|
14 |
+
|
15 |
+
conversation=List[Turn]
|
16 |
+
|
17 |
+
class ChatModel:
|
18 |
+
def __init__(self,model:Model,sysprompt:str):
|
19 |
+
self.setModel(model)
|
20 |
+
self.setSysPrompt(sysprompt)
|
21 |
+
def __call__(self, msg:str):
|
22 |
+
raise NotImplementedError
|
23 |
+
def getconversation(self) -> conversation:
|
24 |
+
raise NotImplementedError
|
25 |
+
def conversationend(self) -> bool:
|
26 |
+
raise NotImplementedError
|
27 |
+
def setconversation(self,conversation:conversation):
|
28 |
+
raise NotImplementedError
|
29 |
+
def setSysPrompt(self,sysprompt:str):
|
30 |
+
def _eq(this, a,b):
|
31 |
+
return a==b
|
32 |
+
self.sysprompt=compiler.compile(sysprompt)({
|
33 |
+
"model":self.name
|
34 |
+
},helpers={"eq":_eq})
|
35 |
+
print(self.name+" SystemPrompt:\n"+self.sysprompt)
|
36 |
+
def setModel(self,model:Model):
|
37 |
+
self.model=model
|
38 |
+
|
39 |
+
class SwapChatModel(ChatModel):
|
40 |
+
def __init__(self,model:Model,sysprompt:str):
|
41 |
+
super().__init__(model,sysprompt)
|
42 |
+
self.conversation=[]
|
43 |
+
def __call__(self, msg:str):
|
44 |
+
if "End of conversation." in [i["content"] for i in self.conversation]:
|
45 |
+
return
|
46 |
+
self.conversation.append(chatmsg(msg,"assistant"))
|
47 |
+
prompt="".join([
|
48 |
+
self.model.start(),
|
49 |
+
self.model.conv([chatmsg(self.sysprompt,"system")]),
|
50 |
+
self.model.conv(self.conversation),self.model.starttok("user")
|
51 |
+
])
|
52 |
+
ret=self.model(prompt, stop=[".","\n \n","?\n",".\n","tile|>","\n"],max_tokens=100)
|
53 |
+
comp=ret["choices"][0]["text"]
|
54 |
+
if("<|end" in comp):
|
55 |
+
self.conversation.append(chatmsg(comp.removesuffix("<|end"),"user"))
|
56 |
+
self.conversation.append(chatmsg("End of conversation.","user"))
|
57 |
+
else:
|
58 |
+
self.conversation.append(chatmsg(comp,"user"))
|
59 |
+
def getconversation(self) -> conversation:
|
60 |
+
return self.conversation
|
61 |
+
def conversationend(self) -> bool:
|
62 |
+
return "End of conversation." in [i["content"] for i in self.conversation]
|
63 |
+
def setconversation(self,conversation:conversation):
|
64 |
+
self.conversation=conversation
|
65 |
+
SwapChatModel.name="SwapChat"
|
66 |
+
|
67 |
+
|
68 |
+
class InquiryChatModel(SwapChatModel):
|
69 |
+
def __init__(self,model:Model,sysprompt:str):
|
70 |
+
super().__init__(model,sysprompt)
|
71 |
+
def inquire(self,msg):
|
72 |
+
prompt="".join([
|
73 |
+
self.model.start(),
|
74 |
+
self.model.conv([chatmsg(self.sysprompt,"system")]),
|
75 |
+
self.model.conv(self.conversation),
|
76 |
+
self.model.conv([chatmsg(msg,"assistant")]),
|
77 |
+
self.model.starttok("system"),
|
78 |
+
"Is this conversation complete(true/false)?\n"
|
79 |
+
])
|
80 |
+
ret=self.model(prompt, stop=[".","\n \n","?\n",".\n","tile|>","\n"],max_tokens=10)
|
81 |
+
print("system prompt:",ret["choices"][0]["text"])
|
82 |
+
if "true" in ret["choices"][0]["text"].lower():
|
83 |
+
self.conversation.append(chatmsg(msg,"user"))
|
84 |
+
self.conversation.append(chatmsg("End of conversation.","user"))
|
85 |
+
def __call__(self, msg:str):
|
86 |
+
self.inquire(msg)
|
87 |
+
super().__call__(msg)
|
88 |
+
InquiryChatModel.name="InquiryChat"
|
89 |
+
models=[SwapChatModel,InquiryChatModel]
|
interactive_test.py
CHANGED
@@ -2,26 +2,39 @@ from typing import Any, Dict, List
|
|
2 |
import gradio as gr
|
3 |
from llama_cpp import Llama
|
4 |
|
|
|
5 |
from models import Phi35,models
|
6 |
|
7 |
-
|
|
|
8 |
The User will make an inquiry to the assistant.
|
9 |
Fullfill the users inquiry.
|
10 |
-
|
|
|
|
|
11 |
The User will never have more than one inquiry in one conversation.
|
12 |
The User will never complete his own inquiry.
|
13 |
The User will never be a assistant.
|
14 |
The User keep his message short in one sentence.
|
|
|
15 |
All conversations will end with "<|endtile|>".
|
|
|
16 |
After each User message is one assistant response.
|
17 |
There can never be more than one assistant response in succession.
|
18 |
-
|
19 |
Example:
|
20 |
User: What is the capital?
|
21 |
Assistant: Could you please specify which capital you are referring to?
|
22 |
User: The capital of France
|
23 |
Assistant: The capital of France is Paris
|
24 |
User: <|endtile|>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
""".strip()
|
26 |
|
27 |
conversations:List[Dict[str, Any]]=[
|
@@ -79,9 +92,11 @@ conversations:List[Dict[str, Any]]=[
|
|
79 |
def chatmsg(message, role):
|
80 |
return {"role": role, "content": message}
|
81 |
|
82 |
-
currmodel=Phi35()
|
83 |
|
84 |
|
|
|
|
|
|
|
85 |
with gr.Blocks() as demo:
|
86 |
with gr.Accordion("Info"):
|
87 |
gr.Markdown(f"""
|
@@ -117,8 +132,15 @@ with gr.Blocks() as demo:
|
|
117 |
return "", next(conversation for conversation in conversations if conversation["name"] == choice)["content"]
|
118 |
convchoicebox.change(update_choicebox, [convchoicebox,custom_conv], [msg,chatbot])
|
119 |
|
|
|
120 |
|
121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
122 |
|
123 |
#Choose Models
|
124 |
modelchoicebox = gr.Radio(choices=[model.modelname for model in models], value=currmodel.modelname, label="Model")
|
@@ -126,25 +148,25 @@ with gr.Blocks() as demo:
|
|
126 |
global currmodel
|
127 |
currmodel.close()
|
128 |
currmodel=next(model for model in models if model.modelname == choice)()
|
129 |
-
|
|
|
|
|
130 |
modelchoicebox.change(update_modelchoicebox, [modelchoicebox], [msg,chatbot])
|
131 |
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
|
|
138 |
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
submit.click(respond, [msg, chatbot,sysprompt], [msg, chatbot])
|
149 |
-
msg.submit(respond, [msg, chatbot,sysprompt], [msg, chatbot])
|
150 |
-
demo.launch()
|
|
|
2 |
import gradio as gr
|
3 |
from llama_cpp import Llama
|
4 |
|
5 |
+
import chatmodel
|
6 |
from models import Phi35,models
|
7 |
|
8 |
+
sysprompt=r"""
|
9 |
+
{{! This comment will not show up in the output}}
|
10 |
The User will make an inquiry to the assistant.
|
11 |
Fullfill the users inquiry.
|
12 |
+
{{#if (eq model "SwapChat")}}
|
13 |
+
The User will write a message with his closing thoughts and the keyword "<|endtile|>" if his inquiry is fulfilled.
|
14 |
+
{{/if}}
|
15 |
The User will never have more than one inquiry in one conversation.
|
16 |
The User will never complete his own inquiry.
|
17 |
The User will never be a assistant.
|
18 |
The User keep his message short in one sentence.
|
19 |
+
{{#if (eq model "SwapChat")}}
|
20 |
All conversations will end with "<|endtile|>".
|
21 |
+
{{/if}}
|
22 |
After each User message is one assistant response.
|
23 |
There can never be more than one assistant response in succession.
|
24 |
+
{{#if (eq model "SwapChat")}}
|
25 |
Example:
|
26 |
User: What is the capital?
|
27 |
Assistant: Could you please specify which capital you are referring to?
|
28 |
User: The capital of France
|
29 |
Assistant: The capital of France is Paris
|
30 |
User: <|endtile|>
|
31 |
+
{{else}}
|
32 |
+
Example:
|
33 |
+
User: What is the capital?
|
34 |
+
Assistant: Could you please specify which capital you are referring to?
|
35 |
+
User: The capital of France
|
36 |
+
Assistant: The capital of France is Paris
|
37 |
+
{{/if}}
|
38 |
""".strip()
|
39 |
|
40 |
conversations:List[Dict[str, Any]]=[
|
|
|
92 |
def chatmsg(message, role):
|
93 |
return {"role": role, "content": message}
|
94 |
|
|
|
95 |
|
96 |
|
97 |
+
currmodel=Phi35()
|
98 |
+
chat:chatmodel.ChatModel=chatmodel.models[0](currmodel,sysprompt)
|
99 |
+
|
100 |
with gr.Blocks() as demo:
|
101 |
with gr.Accordion("Info"):
|
102 |
gr.Markdown(f"""
|
|
|
132 |
return "", next(conversation for conversation in conversations if conversation["name"] == choice)["content"]
|
133 |
convchoicebox.change(update_choicebox, [convchoicebox,custom_conv], [msg,chatbot])
|
134 |
|
135 |
+
msysprompt=gr.Textbox(value=sysprompt, label="System Prompt")
|
136 |
|
137 |
+
def update_sysprompt(csysprompt:str):
|
138 |
+
global sysprompt
|
139 |
+
sysprompt=csysprompt
|
140 |
+
chat.setSysPrompt(sysprompt)
|
141 |
+
chat.setconversation([])
|
142 |
+
return "", chat.getconversation()
|
143 |
+
msysprompt.submit(update_sysprompt, [msysprompt], [msg,chatbot])
|
144 |
|
145 |
#Choose Models
|
146 |
modelchoicebox = gr.Radio(choices=[model.modelname for model in models], value=currmodel.modelname, label="Model")
|
|
|
148 |
global currmodel
|
149 |
currmodel.close()
|
150 |
currmodel=next(model for model in models if model.modelname == choice)()
|
151 |
+
chat.setModel(currmodel)
|
152 |
+
chat.setconversation([])
|
153 |
+
return "", chat.getconversation()
|
154 |
modelchoicebox.change(update_modelchoicebox, [modelchoicebox], [msg,chatbot])
|
155 |
|
156 |
+
chatchoicebox = gr.Radio(choices=[model.name for model in chatmodel.models], value=chat.name, label="Chat")
|
157 |
+
def update_chatchoicebox(choice):
|
158 |
+
global chat, currmodel, sysprompt
|
159 |
+
chat=next(model for model in chatmodel.models if model.name == choice)(currmodel,sysprompt)
|
160 |
+
chat.setconversation([])
|
161 |
+
return "", chat.getconversation()
|
162 |
+
chatchoicebox.change(update_chatchoicebox, [chatchoicebox], [msg,chatbot])
|
163 |
|
164 |
+
#generate response
|
165 |
+
def respond(message:str,chatbot:List[Dict[str, str]]):
|
166 |
+
global chat
|
167 |
+
chat.setconversation(chatbot)
|
168 |
+
chat(message)
|
169 |
+
return "", chat.getconversation()
|
170 |
+
submit.click(respond, [msg,chatbot], [msg, chatbot])
|
171 |
+
msg.submit(respond, [msg,chatbot], [msg, chatbot])
|
172 |
+
demo.launch()
|
|
|
|
|
|
models.py
CHANGED
@@ -1,17 +1,19 @@
|
|
1 |
from typing import Dict, List
|
2 |
|
3 |
from llama_cpp import Llama
|
4 |
-
|
5 |
|
6 |
class Model:
|
7 |
def __init__(self):
|
8 |
pass
|
9 |
def __call__(self, msg:str, stop:List[str], max_tokens:int):
|
10 |
raise NotImplementedError
|
11 |
-
def conv(self, msgs:List[Dict[str, str]]):
|
12 |
raise NotImplementedError
|
13 |
-
def starttok(self, user:str):
|
14 |
raise NotImplementedError
|
|
|
|
|
15 |
def close(self):
|
16 |
pass
|
17 |
|
@@ -20,10 +22,13 @@ class Phi35RPMax(Model):
|
|
20 |
self.llm = Llama.from_pretrained(
|
21 |
repo_id="ArliAI/Phi-3.5-mini-3.8B-ArliAI-RPMax-v1.1-GGUF",
|
22 |
filename="ArliAI-RPMax-3.8B-v1.1-fp16.gguf",
|
|
|
|
|
23 |
)
|
24 |
|
25 |
def __call__(self, msg:str, stop:List[str], max_tokens:int):
|
26 |
-
|
|
|
27 |
|
28 |
def conv(self,msgs:List[Dict[str, str]]):
|
29 |
return "\n".join([f"<|{msg['role']}|>\n{msg['content']}<|end|>" for msg in msgs])
|
@@ -36,11 +41,12 @@ class Phi35(Model):
|
|
36 |
def __init__(self):
|
37 |
self.llm = Llama.from_pretrained(
|
38 |
repo_id="bartowski/Phi-3.5-mini-instruct-GGUF",
|
39 |
-
filename="Phi-3.5-mini-instruct-
|
|
|
40 |
)
|
41 |
def __call__(self, msg:str, stop:List[str], max_tokens:int):
|
42 |
return self.llm(msg, stop=stop, max_tokens=max_tokens)
|
43 |
-
|
44 |
def conv(self,msgs:List[Dict[str, str]]):
|
45 |
return "\n".join([f"<|{msg['role']}|>\n{msg['content']}<|end|>" for msg in msgs])
|
46 |
|
@@ -81,14 +87,18 @@ class Llama31uncensored(Model):
|
|
81 |
self.llm = Llama.from_pretrained(
|
82 |
repo_id="Orenguteng/Llama-3.1-8B-Lexi-Uncensored-V2-GGUF",
|
83 |
filename="Llama-3.1-8B-Lexi-Uncensored_V2_F16.gguf",
|
|
|
84 |
)
|
85 |
def __call__(self, msg:str, stop:List[str], max_tokens:int):
|
86 |
return self.llm(msg, stop=stop, max_tokens=max_tokens)
|
87 |
|
|
|
|
|
|
|
88 |
def conv(self,msgs:List[Dict[str, str]]):
|
89 |
-
return "\n".join([f"<|
|
90 |
def starttok(self,user:str):
|
91 |
-
return f"<|
|
92 |
def close(self):
|
93 |
self.llm.close()
|
94 |
Llama31uncensored.modelname="Llama31-uncensored-fp16"
|
@@ -98,6 +108,7 @@ class Llama31(Model):
|
|
98 |
self.llm = Llama.from_pretrained(
|
99 |
repo_id="lmstudio-community/Meta-Llama-3.1-8B-Instruct-GGUF",
|
100 |
filename="Meta-Llama-3.1-8B-Instruct-IQ4_XS.gguf",
|
|
|
101 |
)
|
102 |
def __call__(self, msg:str, stop:List[str], max_tokens:int):
|
103 |
return self.llm(msg, stop=stop, max_tokens=max_tokens)
|
|
|
1 |
from typing import Dict, List
|
2 |
|
3 |
from llama_cpp import Llama
|
4 |
+
llama_args={"n_gpu_layers":100,"main_gpu":0,"verbose":True}
|
5 |
|
6 |
class Model:
|
7 |
def __init__(self):
|
8 |
pass
|
9 |
def __call__(self, msg:str, stop:List[str], max_tokens:int):
|
10 |
raise NotImplementedError
|
11 |
+
def conv(self, msgs:List[Dict[str, str]])->str:
|
12 |
raise NotImplementedError
|
13 |
+
def starttok(self, user:str)->str:
|
14 |
raise NotImplementedError
|
15 |
+
def start(self)->str:
|
16 |
+
return ""
|
17 |
def close(self):
|
18 |
pass
|
19 |
|
|
|
22 |
self.llm = Llama.from_pretrained(
|
23 |
repo_id="ArliAI/Phi-3.5-mini-3.8B-ArliAI-RPMax-v1.1-GGUF",
|
24 |
filename="ArliAI-RPMax-3.8B-v1.1-fp16.gguf",
|
25 |
+
**llama_args,
|
26 |
+
|
27 |
)
|
28 |
|
29 |
def __call__(self, msg:str, stop:List[str], max_tokens:int):
|
30 |
+
ret=self.llm(msg, stop=stop, max_tokens=max_tokens)
|
31 |
+
return ret
|
32 |
|
33 |
def conv(self,msgs:List[Dict[str, str]]):
|
34 |
return "\n".join([f"<|{msg['role']}|>\n{msg['content']}<|end|>" for msg in msgs])
|
|
|
41 |
def __init__(self):
|
42 |
self.llm = Llama.from_pretrained(
|
43 |
repo_id="bartowski/Phi-3.5-mini-instruct-GGUF",
|
44 |
+
filename="Phi-3.5-mini-instruct-f32.gguf",
|
45 |
+
**llama_args,
|
46 |
)
|
47 |
def __call__(self, msg:str, stop:List[str], max_tokens:int):
|
48 |
return self.llm(msg, stop=stop, max_tokens=max_tokens)
|
49 |
+
|
50 |
def conv(self,msgs:List[Dict[str, str]]):
|
51 |
return "\n".join([f"<|{msg['role']}|>\n{msg['content']}<|end|>" for msg in msgs])
|
52 |
|
|
|
87 |
self.llm = Llama.from_pretrained(
|
88 |
repo_id="Orenguteng/Llama-3.1-8B-Lexi-Uncensored-V2-GGUF",
|
89 |
filename="Llama-3.1-8B-Lexi-Uncensored_V2_F16.gguf",
|
90 |
+
**llama_args,
|
91 |
)
|
92 |
def __call__(self, msg:str, stop:List[str], max_tokens:int):
|
93 |
return self.llm(msg, stop=stop, max_tokens=max_tokens)
|
94 |
|
95 |
+
def start(self):
|
96 |
+
return "<|begin_of_text|>"
|
97 |
+
|
98 |
def conv(self,msgs:List[Dict[str, str]]):
|
99 |
+
return "\n".join([f"<|start_header_id|>{msg['role']}<|end_header_id|>\n\n{msg['content']}<|eot_id|>" for msg in msgs])
|
100 |
def starttok(self,user:str):
|
101 |
+
return f"<|start_header_id|>{user}<|end_header_id|>\n\n"
|
102 |
def close(self):
|
103 |
self.llm.close()
|
104 |
Llama31uncensored.modelname="Llama31-uncensored-fp16"
|
|
|
108 |
self.llm = Llama.from_pretrained(
|
109 |
repo_id="lmstudio-community/Meta-Llama-3.1-8B-Instruct-GGUF",
|
110 |
filename="Meta-Llama-3.1-8B-Instruct-IQ4_XS.gguf",
|
111 |
+
**llama_args,
|
112 |
)
|
113 |
def __call__(self, msg:str, stop:List[str], max_tokens:int):
|
114 |
return self.llm(msg, stop=stop, max_tokens=max_tokens)
|
requirements.txt
CHANGED
@@ -1 +1,60 @@
|
|
1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
aiofiles==23.2.1
|
2 |
+
annotated-types==0.7.0
|
3 |
+
anyio==4.6.2.post1
|
4 |
+
certifi==2024.8.30
|
5 |
+
charset-normalizer==3.4.0
|
6 |
+
click==8.1.7
|
7 |
+
colorama==0.4.6
|
8 |
+
contourpy==1.3.0
|
9 |
+
cycler==0.12.1
|
10 |
+
diskcache==5.6.3
|
11 |
+
fastapi==0.112.4
|
12 |
+
ffmpy==0.4.0
|
13 |
+
filelock==3.16.1
|
14 |
+
fonttools==4.54.1
|
15 |
+
fsspec==2024.9.0
|
16 |
+
gradio==4.43.0
|
17 |
+
gradio_client==1.3.0
|
18 |
+
h11==0.14.0
|
19 |
+
httpcore==1.0.6
|
20 |
+
httpx==0.27.2
|
21 |
+
huggingface-hub==0.26.0
|
22 |
+
idna==3.10
|
23 |
+
importlib_resources==6.4.5
|
24 |
+
Jinja2==3.1.4
|
25 |
+
kiwisolver==1.4.7
|
26 |
+
llama_cpp_python==0.2.90
|
27 |
+
markdown-it-py==3.0.0
|
28 |
+
MarkupSafe==2.1.5
|
29 |
+
matplotlib==3.9.2
|
30 |
+
mdurl==0.1.2
|
31 |
+
numpy==2.1.2
|
32 |
+
orjson==3.10.7
|
33 |
+
packaging==24.1
|
34 |
+
pandas==2.2.3
|
35 |
+
pillow==10.4.0
|
36 |
+
pydantic==2.9.2
|
37 |
+
pydantic_core==2.23.4
|
38 |
+
pydub==0.25.1
|
39 |
+
Pygments==2.18.0
|
40 |
+
pyparsing==3.2.0
|
41 |
+
python-dateutil==2.9.0.post0
|
42 |
+
python-multipart==0.0.12
|
43 |
+
pytz==2024.2
|
44 |
+
PyYAML==6.0.2
|
45 |
+
requests==2.32.3
|
46 |
+
rich==13.9.2
|
47 |
+
ruff==0.7.0
|
48 |
+
semantic-version==2.10.0
|
49 |
+
shellingham==1.5.4
|
50 |
+
six==1.16.0
|
51 |
+
sniffio==1.3.1
|
52 |
+
starlette==0.38.6
|
53 |
+
tomlkit==0.12.0
|
54 |
+
tqdm==4.66.5
|
55 |
+
typer==0.12.5
|
56 |
+
typing_extensions==4.12.2
|
57 |
+
tzdata==2024.2
|
58 |
+
urllib3==2.2.3
|
59 |
+
uvicorn==0.32.0
|
60 |
+
websockets==12.0
|