Spaces:
Runtime error
Runtime error
Minimal working example SPIA2024
Browse files- .gitignore +3 -0
- SPIA2024/config.json +14 -0
- SPIA2024/initial_message.txt +5 -0
- SPIA2024/system_message.txt +12 -0
- app.py +105 -96
.gitignore
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__
|
| 2 |
+
.venv
|
| 3 |
+
wandb
|
SPIA2024/config.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"wandb_args" : {
|
| 3 |
+
"project": "spiaclass2024",
|
| 4 |
+
"tags": [
|
| 5 |
+
"dev",
|
| 6 |
+
"demo",
|
| 7 |
+
"class 01"
|
| 8 |
+
]
|
| 9 |
+
},
|
| 10 |
+
"model_args" : {
|
| 11 |
+
"model": "gpt-4-1106-preview",
|
| 12 |
+
"temperature": 0.0
|
| 13 |
+
}
|
| 14 |
+
}
|
SPIA2024/initial_message.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Hello! My name is SurveyGPT, a conversational AI designed to help improve interview and survey research.
|
| 2 |
+
|
| 3 |
+
Today I will be asking you some questions about your background and expectations.
|
| 4 |
+
|
| 5 |
+
To begin, could you tell me what you hope to get out of this course?
|
SPIA2024/system_message.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
You are an AI designed to help professors better understand the needs of their classes by fielding chat-based interviews with students at the beginning of the semester.
|
| 2 |
+
Your job is to conduct an interview with students where you ask them about their expectations of the course and previous relevant experience in order to produce interview transcripts that the professor can analyze and create an educational experience that is tailored to students needs, abilities and expectations.
|
| 3 |
+
|
| 4 |
+
The class you are assisting with this semester is an Introduction to Machine Learning for Public Policy, taught by Professor Brandon Stewart at the School of Public and International Affairs (SPIA) at Princeton University. This is a one-semester applied introduction to machine learning for policy makers and analysts, with an emphasis on building strong foundations and core intutions for applying machine learning in social science and policy settings.
|
| 5 |
+
|
| 6 |
+
Your instructions for conducting the interview are as follows:
|
| 7 |
+
- Begin by asking the student what they hope to get out of the course.
|
| 8 |
+
- If there are ambiguities in their answer, ask probing questions to elicit further information.
|
| 9 |
+
- Once the conversation makes clear what the student hopes to get out of the course, ask about their background in quantitative and technical work or projects.
|
| 10 |
+
- Finally, ask any other questions that you think are helpful.
|
| 11 |
+
- Maintain a polite and professional demeanor.
|
| 12 |
+
- If asked questions that you do not have the information about, such as about accommodations or assignments, advise the student to direct their question to the Professor instead.
|
app.py
CHANGED
|
@@ -1,14 +1,16 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
|
| 4 |
Author: Dr Musashi Hinck
|
| 5 |
|
| 6 |
|
| 7 |
-
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
-
|
|
|
|
|
|
|
| 12 |
|
| 13 |
"""
|
| 14 |
from __future__ import annotations
|
|
@@ -20,6 +22,7 @@ import wandb
|
|
| 20 |
import gradio as gr
|
| 21 |
import openai
|
| 22 |
|
|
|
|
| 23 |
from base64 import urlsafe_b64decode
|
| 24 |
|
| 25 |
logger = logging.getLogger(__name__)
|
|
@@ -28,90 +31,68 @@ from utils import PromptTemplate, convert_gradio_to_openai, seed_openai_key
|
|
| 28 |
|
| 29 |
|
| 30 |
# %% Initialization
|
|
|
|
| 31 |
if os.environ.get(f"OPENAI_API_KEY", "DEFAULT") == "DEFAULT":
|
| 32 |
seed_openai_key()
|
| 33 |
client = openai.OpenAI()
|
| 34 |
|
| 35 |
|
| 36 |
# %% (functions)
|
| 37 |
-
def
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
config = decode_config(request.query_params["dta"])
|
| 47 |
-
survey_question = config["question"]
|
| 48 |
-
survey_template = config["template"]
|
| 49 |
-
initial_message = config["initial_message"]
|
| 50 |
-
model_args = {"model": config["model"], "temperature": config["temperature"]}
|
| 51 |
-
userid = config["userid"]
|
| 52 |
-
return survey_question, survey_template, initial_message, model_args, userid
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
# Post-loading
|
| 56 |
-
def update_template(question: str, template: PromptTemplate | str) -> str:
|
| 57 |
-
"""
|
| 58 |
-
Updates templates. Currently only accepts a "question" variable, but can add future templating in the future.
|
| 59 |
-
"""
|
| 60 |
-
if isinstance(template, str):
|
| 61 |
-
template = PromptTemplate(template)
|
| 62 |
-
if "question" in template.variables:
|
| 63 |
-
return template.format(question=question)
|
| 64 |
-
else:
|
| 65 |
-
return str(template)
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
def reset_interview() -> tuple[list[list[str | None]], gr.Button, gr.Button]:
|
| 69 |
-
wandb.finish()
|
| 70 |
-
gr.Info("Interview reset.")
|
| 71 |
-
return (
|
| 72 |
-
[],
|
| 73 |
-
gr.Button("Start Interview", visible=True),
|
| 74 |
-
gr.Button("Reply", visible=False),
|
| 75 |
-
gr.Button("Save Survey", visible=False, variant="secondary"),
|
| 76 |
-
gr.Button("Save and Exit", visible=False, variant="stop"),
|
| 77 |
)
|
|
|
|
|
|
|
| 78 |
|
| 79 |
|
| 80 |
def initialize_interview(
|
| 81 |
-
system_message: str,
|
| 82 |
-
) -> tuple[
|
| 83 |
"Read system prompt and start interview"
|
| 84 |
-
if len(
|
| 85 |
-
|
| 86 |
[], system_message, client, model_args, stream=False
|
| 87 |
)
|
| 88 |
-
|
| 89 |
-
|
|
|
|
| 90 |
return (
|
| 91 |
-
chat_history,
|
| 92 |
gr.Textbox(
|
| 93 |
-
placeholder="Type response here.",
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
gr.Button(
|
|
|
|
|
|
|
|
|
|
| 98 |
)
|
| 99 |
|
| 100 |
|
| 101 |
def initialize_tracker(
|
| 102 |
model_args: dict[str, str | float],
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
) -> None:
|
| 107 |
"Initializes wandb run for interview"
|
| 108 |
run_config = model_args | {
|
| 109 |
-
"
|
| 110 |
-
"template": str(template),
|
| 111 |
"userid": userid,
|
| 112 |
}
|
| 113 |
wandb.init(
|
| 114 |
-
project=
|
|
|
|
|
|
|
|
|
|
| 115 |
)
|
| 116 |
|
| 117 |
|
|
@@ -166,7 +147,7 @@ def call_openai(
|
|
| 166 |
def user_message(
|
| 167 |
message: str, chat_history: list[list[str | None]]
|
| 168 |
) -> tuple[str, list[list[str | None]]]:
|
| 169 |
-
"
|
| 170 |
return "", chat_history + [[message, None]]
|
| 171 |
|
| 172 |
|
|
@@ -183,6 +164,7 @@ def bot_message(
|
|
| 183 |
+ messages
|
| 184 |
+ [{"role": "user", "content": user_msg}]
|
| 185 |
)
|
|
|
|
| 186 |
response = client.chat.completions.create(
|
| 187 |
messages=messages, stream=True, **model_args
|
| 188 |
)
|
|
@@ -195,58 +177,65 @@ def bot_message(
|
|
| 195 |
yield chat_history
|
| 196 |
|
| 197 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
# LAYOUT
|
| 199 |
with gr.Blocks() as demo:
|
| 200 |
-
gr.Markdown("#
|
| 201 |
|
| 202 |
-
#
|
| 203 |
-
|
| 204 |
-
surveyTemplate = gr.Textbox(visible=False)
|
| 205 |
initialMessage = gr.Textbox(visible=False)
|
| 206 |
systemMessage = gr.Textbox(visible=False)
|
| 207 |
modelArgs = gr.State(value={"model": "", "temperature": ""})
|
| 208 |
-
|
| 209 |
|
| 210 |
-
##
|
| 211 |
-
|
| 212 |
-
show_label=False
|
| 213 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
with gr.Row():
|
| 215 |
chatInput = gr.Textbox(
|
| 216 |
placeholder="Click 'Start Interview' to begin.",
|
|
|
|
| 217 |
interactive=False,
|
| 218 |
show_label=False,
|
| 219 |
scale=10,
|
| 220 |
)
|
| 221 |
chatSubmit = gr.Button(
|
| 222 |
"",
|
| 223 |
-
variant="
|
| 224 |
interactive=False,
|
| 225 |
icon="./arrow_icon.svg",
|
|
|
|
| 226 |
)
|
| 227 |
-
startInterview = gr.Button("Start Interview", variant="primary")
|
| 228 |
resetButton = gr.Button("Save and Exit", visible=False, variant="stop")
|
| 229 |
|
| 230 |
## INTERACTIONS
|
| 231 |
# Start Interview button
|
| 232 |
-
|
| 233 |
load_config,
|
| 234 |
-
inputs=
|
| 235 |
-
outputs=[
|
| 236 |
-
surveyQuestion,
|
| 237 |
-
surveyTemplate,
|
| 238 |
-
initialMessage,
|
| 239 |
-
modelArgs,
|
| 240 |
-
userid,
|
| 241 |
-
],
|
| 242 |
-
).then(
|
| 243 |
-
update_template,
|
| 244 |
-
inputs=[surveyQuestion, surveyTemplate],
|
| 245 |
-
outputs=[systemMessage],
|
| 246 |
-
).then(
|
| 247 |
-
update_template,
|
| 248 |
-
inputs=[surveyQuestion, initialMessage],
|
| 249 |
-
outputs=initialMessage,
|
| 250 |
).then(
|
| 251 |
initialize_interview,
|
| 252 |
inputs=[systemMessage, initialMessage, modelArgs],
|
|
@@ -255,13 +244,26 @@ with gr.Blocks() as demo:
|
|
| 255 |
chatInput,
|
| 256 |
chatSubmit,
|
| 257 |
startInterview,
|
|
|
|
| 258 |
resetButton,
|
| 259 |
],
|
| 260 |
).then(
|
| 261 |
-
initialize_tracker, inputs=[modelArgs,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
)
|
| 263 |
|
| 264 |
-
#
|
|
|
|
| 265 |
chatInput.submit(
|
| 266 |
user_message,
|
| 267 |
inputs=[chatInput, chatDisplay],
|
|
@@ -274,8 +276,7 @@ with gr.Blocks() as demo:
|
|
| 274 |
).then(
|
| 275 |
save_interview, inputs=[chatDisplay]
|
| 276 |
)
|
| 277 |
-
|
| 278 |
-
# "Submit" button
|
| 279 |
chatSubmit.click(
|
| 280 |
user_message,
|
| 281 |
inputs=[chatInput, chatDisplay],
|
|
@@ -289,9 +290,17 @@ with gr.Blocks() as demo:
|
|
| 289 |
save_interview, inputs=[chatDisplay]
|
| 290 |
)
|
| 291 |
|
|
|
|
| 292 |
resetButton.click(save_interview, [chatDisplay]).then(
|
| 293 |
reset_interview,
|
| 294 |
-
outputs=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
show_progress=False,
|
| 296 |
)
|
| 297 |
|
|
|
|
| 1 |
"""
|
| 2 |
+
General-Purpose LM Interview Interface
|
| 3 |
|
| 4 |
Author: Dr Musashi Hinck
|
| 5 |
|
| 6 |
|
| 7 |
+
Version Log:
|
| 8 |
|
| 9 |
+
- 2024.01.29: prototype without separate launching interface for demoing in SPIA class.
|
| 10 |
+
- Remove URL decoding
|
| 11 |
+
- Read sysprompt and initial_message from file
|
| 12 |
+
- Begins with user entering name/alias
|
| 13 |
+
- Azure OpenAI?
|
| 14 |
|
| 15 |
"""
|
| 16 |
from __future__ import annotations
|
|
|
|
| 22 |
import gradio as gr
|
| 23 |
import openai
|
| 24 |
|
| 25 |
+
from pathlib import Path
|
| 26 |
from base64 import urlsafe_b64decode
|
| 27 |
|
| 28 |
logger = logging.getLogger(__name__)
|
|
|
|
| 31 |
|
| 32 |
|
| 33 |
# %% Initialization
|
| 34 |
+
CONFIG_DIR: Path = Path("./SPIA2024")
|
| 35 |
if os.environ.get(f"OPENAI_API_KEY", "DEFAULT") == "DEFAULT":
|
| 36 |
seed_openai_key()
|
| 37 |
client = openai.OpenAI()
|
| 38 |
|
| 39 |
|
| 40 |
# %% (functions)
|
| 41 |
+
def load_config(
|
| 42 |
+
path: Path,
|
| 43 |
+
) -> tuple[str, str, dict[str, str | float], dict[str, str | list[str]]]:
|
| 44 |
+
"Read configs, return inital_message, system_message, model_args, wandb_args"
|
| 45 |
+
initial_message: str = (path / "initial_message.txt").read_text().strip()
|
| 46 |
+
system_message: str = (path / "system_message.txt").read_text().strip()
|
| 47 |
+
cfg: dict[str, str] = json.loads((path / "config.json").read_bytes())
|
| 48 |
+
model_args: dict[str, str | float] = cfg.get(
|
| 49 |
+
"model_args", {"model": "gpt4", "temperature": 0.0}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
)
|
| 51 |
+
wandb_args: dict = cfg.get("wandb_args")
|
| 52 |
+
return initial_message, system_message, model_args, wandb_args
|
| 53 |
|
| 54 |
|
| 55 |
def initialize_interview(
|
| 56 |
+
system_message: str, initial_message: str, model_args: dict[str, str | float]
|
| 57 |
+
) -> tuple[gr.Chatbot, gr.Textbox, gr.Button, gr.Button]:
|
| 58 |
"Read system prompt and start interview"
|
| 59 |
+
if len(initial_message) == 0: # If empty inital message, ask the LM to write it.
|
| 60 |
+
initial_message = call_openai(
|
| 61 |
[], system_message, client, model_args, stream=False
|
| 62 |
)
|
| 63 |
+
chat_history = [
|
| 64 |
+
[None, initial_message]
|
| 65 |
+
] # First item is for user, in this case bot starts interaction.
|
| 66 |
return (
|
| 67 |
+
gr.Chatbot(visible=True, value=chat_history),
|
| 68 |
gr.Textbox(
|
| 69 |
+
placeholder="Type response here. Hit 'enter' to submit.",
|
| 70 |
+
visible=True,
|
| 71 |
+
interactive=True,
|
| 72 |
+
), # chatInput
|
| 73 |
+
gr.Button(visible=True, interactive=True), # chatSubmit
|
| 74 |
+
gr.Button(visible=False), # startInterview
|
| 75 |
+
gr.Textbox(visible=False), # userBox
|
| 76 |
+
gr.Button(visible=True), # resetButton
|
| 77 |
)
|
| 78 |
|
| 79 |
|
| 80 |
def initialize_tracker(
|
| 81 |
model_args: dict[str, str | float],
|
| 82 |
+
system_message: PromptTemplate,
|
| 83 |
+
userid: str,
|
| 84 |
+
wandb_args: dict[str, str | list[str]],
|
| 85 |
) -> None:
|
| 86 |
"Initializes wandb run for interview"
|
| 87 |
run_config = model_args | {
|
| 88 |
+
"system_message": str(system_message),
|
|
|
|
| 89 |
"userid": userid,
|
| 90 |
}
|
| 91 |
wandb.init(
|
| 92 |
+
project=wandb_args["project"],
|
| 93 |
+
name=userid,
|
| 94 |
+
config=run_config,
|
| 95 |
+
tags=wandb_args["tags"],
|
| 96 |
)
|
| 97 |
|
| 98 |
|
|
|
|
| 147 |
def user_message(
|
| 148 |
message: str, chat_history: list[list[str | None]]
|
| 149 |
) -> tuple[str, list[list[str | None]]]:
|
| 150 |
+
"Display user message immediately"
|
| 151 |
return "", chat_history + [[message, None]]
|
| 152 |
|
| 153 |
|
|
|
|
| 164 |
+ messages
|
| 165 |
+ [{"role": "user", "content": user_msg}]
|
| 166 |
)
|
| 167 |
+
# API call
|
| 168 |
response = client.chat.completions.create(
|
| 169 |
messages=messages, stream=True, **model_args
|
| 170 |
)
|
|
|
|
| 177 |
yield chat_history
|
| 178 |
|
| 179 |
|
| 180 |
+
def reset_interview() -> (
|
| 181 |
+
tuple[
|
| 182 |
+
list[list[str | None]], gr.Chatbot, gr.Textbox, gr.Button, gr.Button, gr.Button
|
| 183 |
+
]
|
| 184 |
+
):
|
| 185 |
+
wandb.finish()
|
| 186 |
+
gr.Info("Interview reset.")
|
| 187 |
+
return (
|
| 188 |
+
gr.Chatbot(visible=False, value=[]), # chatDisplay
|
| 189 |
+
gr.Textbox(visible=False), # chatInput
|
| 190 |
+
gr.Button(visible=False), # chatSubmit
|
| 191 |
+
gr.Textbox(value=None, visible=True), # userBox
|
| 192 |
+
gr.Button(visible=True), # startInterview
|
| 193 |
+
gr.Button(visible=False), # resetButton
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
|
| 197 |
# LAYOUT
|
| 198 |
with gr.Blocks() as demo:
|
| 199 |
+
gr.Markdown("# StewartLab LM Interviewer")
|
| 200 |
|
| 201 |
+
# Config values
|
| 202 |
+
configDir = gr.State(value=CONFIG_DIR)
|
|
|
|
| 203 |
initialMessage = gr.Textbox(visible=False)
|
| 204 |
systemMessage = gr.Textbox(visible=False)
|
| 205 |
modelArgs = gr.State(value={"model": "", "temperature": ""})
|
| 206 |
+
wandbArgs = gr.State(value={"project": "", "tags": []})
|
| 207 |
|
| 208 |
+
## Start interview by entering name or alias
|
| 209 |
+
userBox = gr.Textbox(
|
| 210 |
+
placeholder="Enter name or alias and hit 'enter' to begin.", show_label=False
|
| 211 |
)
|
| 212 |
+
startInterview = gr.Button("Start Interview", variant="primary", visible=True)
|
| 213 |
+
|
| 214 |
+
## RESPONDENT
|
| 215 |
+
chatDisplay = gr.Chatbot(show_label=False, visible=False)
|
| 216 |
with gr.Row():
|
| 217 |
chatInput = gr.Textbox(
|
| 218 |
placeholder="Click 'Start Interview' to begin.",
|
| 219 |
+
visible=False,
|
| 220 |
interactive=False,
|
| 221 |
show_label=False,
|
| 222 |
scale=10,
|
| 223 |
)
|
| 224 |
chatSubmit = gr.Button(
|
| 225 |
"",
|
| 226 |
+
variant="primary",
|
| 227 |
interactive=False,
|
| 228 |
icon="./arrow_icon.svg",
|
| 229 |
+
visible=False,
|
| 230 |
)
|
|
|
|
| 231 |
resetButton = gr.Button("Save and Exit", visible=False, variant="stop")
|
| 232 |
|
| 233 |
## INTERACTIONS
|
| 234 |
# Start Interview button
|
| 235 |
+
userBox.submit(
|
| 236 |
load_config,
|
| 237 |
+
inputs=configDir,
|
| 238 |
+
outputs=[initialMessage, systemMessage, modelArgs, wandbArgs],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
).then(
|
| 240 |
initialize_interview,
|
| 241 |
inputs=[systemMessage, initialMessage, modelArgs],
|
|
|
|
| 244 |
chatInput,
|
| 245 |
chatSubmit,
|
| 246 |
startInterview,
|
| 247 |
+
userBox,
|
| 248 |
resetButton,
|
| 249 |
],
|
| 250 |
).then(
|
| 251 |
+
initialize_tracker, inputs=[modelArgs, systemMessage, userBox, wandbArgs]
|
| 252 |
+
)
|
| 253 |
+
startInterview.click(
|
| 254 |
+
load_config,
|
| 255 |
+
inputs=None,
|
| 256 |
+
outputs=[initialMessage, systemMessage, modelArgs, wandbArgs],
|
| 257 |
+
).then(
|
| 258 |
+
initialize_interview,
|
| 259 |
+
inputs=[systemMessage, initialMessage, modelArgs],
|
| 260 |
+
outputs=[chatDisplay, chatInput, chatSubmit, startInterview, resetButton],
|
| 261 |
+
).then(
|
| 262 |
+
initialize_tracker, inputs=[modelArgs, systemMessage, userBox, wandbArgs]
|
| 263 |
)
|
| 264 |
|
| 265 |
+
# Chat interaction
|
| 266 |
+
# "Enter"
|
| 267 |
chatInput.submit(
|
| 268 |
user_message,
|
| 269 |
inputs=[chatInput, chatDisplay],
|
|
|
|
| 276 |
).then(
|
| 277 |
save_interview, inputs=[chatDisplay]
|
| 278 |
)
|
| 279 |
+
# Button
|
|
|
|
| 280 |
chatSubmit.click(
|
| 281 |
user_message,
|
| 282 |
inputs=[chatInput, chatDisplay],
|
|
|
|
| 290 |
save_interview, inputs=[chatDisplay]
|
| 291 |
)
|
| 292 |
|
| 293 |
+
# Reset button
|
| 294 |
resetButton.click(save_interview, [chatDisplay]).then(
|
| 295 |
reset_interview,
|
| 296 |
+
outputs=[
|
| 297 |
+
chatDisplay,
|
| 298 |
+
chatInput,
|
| 299 |
+
chatSubmit,
|
| 300 |
+
userBox,
|
| 301 |
+
startInterview,
|
| 302 |
+
resetButton,
|
| 303 |
+
],
|
| 304 |
show_progress=False,
|
| 305 |
)
|
| 306 |
|