add auto_save and input limit

#1
by floydchow7 - opened
Files changed (3) hide show
  1. app.py +94 -23
  2. constants.py +53 -0
  3. save_data.py +45 -11
app.py CHANGED
@@ -3,6 +3,8 @@ from utils import *
3
  from save_data import add_new_data, get_sheet_service
4
  from instructions import *
5
  from user_groups import user_data
 
 
6
 
7
  class SessionManager:
8
  def __init__(self):
@@ -58,19 +60,26 @@ def handle_create_sequential(task, human_input, session_manager, api_key, identi
58
  cooperate_style = "sequential"
59
  session_index = session_manager.add_session(task=task, cooperate_style=cooperate_style)
60
  session_manager.update(session_index, human_input, 'human_initial_answer')
61
- output = merge_texts_sequential(task, human_input, api_key)
62
- session_manager.update(session_index, output, 'ai_modificated_output')
63
  session_manager.update(session_index, identification_code, 'user_identification_code')
 
 
 
 
 
64
  return output, session_index
65
 
66
 
67
  def handle_create_parallel(task, human_input, session_manager, api_key, identification_code):
68
  cooperate_style = "parallel"
69
  session_index = session_manager.add_session(task=task, cooperate_style=cooperate_style)
70
- ai_initial_answer = generate_ai_initial_answer(task, api_key)
 
 
 
 
 
71
  session_manager.update(session_index, human_input, 'human_initial_answer')
72
  session_manager.update(session_index, ai_initial_answer, 'ai_initial_answer')
73
- final_answer = merge_texts_parallel(task, human_input, ai_initial_answer, api_key)
74
  session_manager.update(session_index, final_answer, 'merged_final_answer')
75
  session_manager.update(session_index, identification_code, 'user_identification_code')
76
  return ai_initial_answer, final_answer, session_index
@@ -87,7 +96,10 @@ def handle_create_reverse_sequential(task, session_manager, api_key, identificat
87
  def handle_modify_reverse_sequential(session_index, modification_suggestions, session_manager, api_key):
88
  session = session_manager.get_session(session_index)
89
  session_manager.update(session_index, modification_suggestions, 'human_modifications')
90
- final_answer = modify_with_suggestion(session['task'], modification_suggestions, api_key)
 
 
 
91
  session_manager.update(session_index, final_answer, 'final_answer')
92
  return final_answer, session_index
93
 
@@ -109,12 +121,24 @@ def save_data(session_index, session_manager, service, SHEET_ID):
109
  return "Data has been saved to Google Sheets."
110
 
111
  def login(identification_code):
112
- user_info = user_data.get(identification_code)
113
- if user_info:
114
- return update_content(user_info["group"])
 
 
 
 
115
  else:
116
  return update_content(None)
117
 
 
 
 
 
 
 
 
 
118
 
119
  if __name__ == "__main__":
120
  api_key = get_api_key(local=False)
@@ -130,17 +154,22 @@ if __name__ == "__main__":
130
  login_status = gr.Textbox(label="Next Tasks", interactive=False)
131
  group = gr.State()
132
 
133
- task = gr.Textbox(label="Task Description",
134
- value = default_task_discription(),
135
- visible=False,
136
- lines=10)
137
 
 
 
 
138
  # initialization of different group contents
139
  group_a_content = gr.Group(visible=False)
140
  group_b_content = gr.Group(visible=False)
141
  group_c_content = gr.Group(visible=False)
142
 
143
 
 
 
144
  def update_content(group):
145
  if group == "A":
146
  return gr.update(visible=True), gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), group_a_instructions()
@@ -157,7 +186,7 @@ if __name__ == "__main__":
157
 
158
  with group_a_content:
159
  with gr.Row():
160
- human_input = gr.Textbox(label="Human Input")
161
  with gr.Row():
162
  submit_btn = gr.Button("Create")
163
  with gr.Row():
@@ -166,10 +195,15 @@ if __name__ == "__main__":
166
 
167
  submit_btn.click(
168
  fn=lambda task, human_input, id: handle_create_sequential(task, human_input, session_manager, api_key, id),
169
- inputs=[task, human_input, identification_code],
170
  outputs=[ai_output, session_index]
171
  )
172
 
 
 
 
 
 
173
  # evaluate same for every group
174
  evaluate_btn = gr.Button("Evaluate")
175
  evaluation_result = gr.Textbox(label="Evaluation Result")
@@ -180,18 +214,27 @@ if __name__ == "__main__":
180
  outputs=[evaluation_result]
181
  )
182
 
183
- save_btn = gr.Button("Save Data")
 
 
 
 
 
184
  save_result = gr.Label()
185
 
 
186
  save_btn.click(
187
  fn=lambda session_index: save_data(session_index, session_manager, service, SHEET_ID1),
188
  inputs=[session_index],
189
  outputs=[save_result]
190
  )
 
 
 
191
 
192
  with group_b_content:
193
  with gr.Row():
194
- human_input = gr.Textbox(label="Human Input")
195
  with gr.Row():
196
  create_btn = gr.Button("Create")
197
  with gr.Row():
@@ -201,11 +244,14 @@ if __name__ == "__main__":
201
 
202
  create_btn.click(
203
  fn=lambda task, human_input, id: handle_create_parallel(task, human_input, session_manager, api_key, id),
204
- inputs=[task, human_input, identification_code],
205
  outputs=[ai_initial_output, final_output, session_index]
206
  )
207
 
208
-
 
 
 
209
 
210
 
211
  evaluate_btn = gr.Button("Evaluate")
@@ -217,7 +263,12 @@ if __name__ == "__main__":
217
  outputs=[evaluation_result]
218
  )
219
 
220
- save_btn = gr.Button("Save Data")
 
 
 
 
 
221
  save_result = gr.Label()
222
 
223
  save_btn.click(
@@ -226,12 +277,15 @@ if __name__ == "__main__":
226
  outputs=[save_result]
227
  )
228
 
 
 
 
229
  with group_c_content:
230
  with gr.Row():
231
  create_initial_btn = gr.Button("Create")
232
  with gr.Row():
233
  initial_answer = gr.Textbox(label="AI Initial Answer")
234
- modification_suggestions = gr.Textbox(label="Modification Suggestions")
235
  with gr.Row():
236
  create_final_btn = gr.Button("Modify")
237
  with gr.Row():
@@ -241,16 +295,26 @@ if __name__ == "__main__":
241
 
242
  create_initial_btn.click(
243
  fn=lambda task, id: handle_create_reverse_sequential(task, session_manager, api_key, id),
244
- inputs=[task, identification_code],
245
  outputs=[initial_answer, session_index]
246
  )
247
 
 
 
 
 
 
248
  create_final_btn.click(
249
  fn=lambda session_index, modification_suggestions: handle_modify_reverse_sequential(session_index, modification_suggestions, session_manager, api_key),
250
  inputs=[session_index, modification_suggestions],
251
  outputs=[final_answer, session_index]
252
  )
253
 
 
 
 
 
 
254
  evaluate_btn = gr.Button("Evaluate")
255
  evaluation_result = gr.Textbox(label="Evaluation Result")
256
 
@@ -260,7 +324,12 @@ if __name__ == "__main__":
260
  outputs=[evaluation_result]
261
  )
262
 
263
- save_btn = gr.Button("Save Data")
 
 
 
 
 
264
  save_result = gr.Label()
265
 
266
  save_btn.click(
@@ -268,6 +337,8 @@ if __name__ == "__main__":
268
  inputs=[session_index],
269
  outputs=[save_result]
270
  )
271
-
 
 
272
 
273
  app.launch(share=True)
 
3
  from save_data import add_new_data, get_sheet_service
4
  from instructions import *
5
  from user_groups import user_data
6
+ from constants import SDG_DETAILS, GPT_PROMPT_parallel, GPT_PROMPT_sequential, GPT_PROMPT_reverse_sequential
7
+
8
 
9
  class SessionManager:
10
  def __init__(self):
 
60
  cooperate_style = "sequential"
61
  session_index = session_manager.add_session(task=task, cooperate_style=cooperate_style)
62
  session_manager.update(session_index, human_input, 'human_initial_answer')
 
 
63
  session_manager.update(session_index, identification_code, 'user_identification_code')
64
+ if word_limit_validation(human_input):
65
+ output = word_limit_validation(human_input)
66
+ else:
67
+ output = merge_texts_sequential(task, human_input, api_key)
68
+ session_manager.update(session_index, output, 'ai_modificated_output')
69
  return output, session_index
70
 
71
 
72
  def handle_create_parallel(task, human_input, session_manager, api_key, identification_code):
73
  cooperate_style = "parallel"
74
  session_index = session_manager.add_session(task=task, cooperate_style=cooperate_style)
75
+ if word_limit_validation(human_input):
76
+ ai_initial_answer = word_limit_validation(human_input)
77
+ final_answer = word_limit_validation(human_input)
78
+ else:
79
+ ai_initial_answer = generate_ai_initial_answer(task, api_key)
80
+ final_answer = merge_texts_parallel(task, human_input, ai_initial_answer, api_key)
81
  session_manager.update(session_index, human_input, 'human_initial_answer')
82
  session_manager.update(session_index, ai_initial_answer, 'ai_initial_answer')
 
83
  session_manager.update(session_index, final_answer, 'merged_final_answer')
84
  session_manager.update(session_index, identification_code, 'user_identification_code')
85
  return ai_initial_answer, final_answer, session_index
 
96
  def handle_modify_reverse_sequential(session_index, modification_suggestions, session_manager, api_key):
97
  session = session_manager.get_session(session_index)
98
  session_manager.update(session_index, modification_suggestions, 'human_modifications')
99
+ if word_limit_validation(modification_suggestions):
100
+ final_answer = word_limit_validation(modification_suggestions)
101
+ else:
102
+ final_answer = modify_with_suggestion(session['task'], modification_suggestions, api_key)
103
  session_manager.update(session_index, final_answer, 'final_answer')
104
  return final_answer, session_index
105
 
 
121
  return "Data has been saved to Google Sheets."
122
 
123
  def login(identification_code):
124
+ groups = ["A", "B", "C"]
125
+ if not identification_code:
126
+ return update_content(None)
127
+
128
+ user_group_id = int(identification_code)//1000
129
+ if user_group_id in range(3):
130
+ return update_content(groups[user_group_id])
131
  else:
132
  return update_content(None)
133
 
134
+ def word_limit_validation(human_input):
135
+ words = human_input.split()
136
+ if len(words) < 50:
137
+ return f"Error: Your input '{human_input}'is less than 50 words. Please enter at least 50 words."
138
+ return None
139
+
140
+ def on_textbox_change(session_index, session_manager, service, SHEET_ID):
141
+ return save_data(session_index, session_manager, service, SHEET_ID)
142
 
143
  if __name__ == "__main__":
144
  api_key = get_api_key(local=False)
 
154
  login_status = gr.Textbox(label="Next Tasks", interactive=False)
155
  group = gr.State()
156
 
157
+ with gr.Column(visible=False) as task:
158
+ description = gr.Textbox(label="Task Description",
159
+ value = default_task_discription(),
160
+ lines=10)
161
 
162
+ with gr.Accordion(label = "17 Sustainable Development Goals",
163
+ open=False):
164
+ gr.Markdown(SDG_DETAILS)
165
  # initialization of different group contents
166
  group_a_content = gr.Group(visible=False)
167
  group_b_content = gr.Group(visible=False)
168
  group_c_content = gr.Group(visible=False)
169
 
170
 
171
+
172
+
173
  def update_content(group):
174
  if group == "A":
175
  return gr.update(visible=True), gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), group_a_instructions()
 
186
 
187
  with group_a_content:
188
  with gr.Row():
189
+ human_input = gr.Textbox(label="Human Input (At least 50 words)")
190
  with gr.Row():
191
  submit_btn = gr.Button("Create")
192
  with gr.Row():
 
195
 
196
  submit_btn.click(
197
  fn=lambda task, human_input, id: handle_create_sequential(task, human_input, session_manager, api_key, id),
198
+ inputs=[description, human_input, identification_code],
199
  outputs=[ai_output, session_index]
200
  )
201
 
202
+ ai_output.change(
203
+ fn = lambda session_index: on_textbox_change(session_index, session_manager, service, SHEET_ID1),
204
+ inputs = [session_index]
205
+ )
206
+
207
  # evaluate same for every group
208
  evaluate_btn = gr.Button("Evaluate")
209
  evaluation_result = gr.Textbox(label="Evaluation Result")
 
214
  outputs=[evaluation_result]
215
  )
216
 
217
+ evaluation_result.change(
218
+ fn = lambda session_index: on_textbox_change(session_index, session_manager, service, SHEET_ID1),
219
+ inputs = [session_index]
220
+ )
221
+
222
+ save_btn = gr.Button("Save Data", elem_id="save_btn")
223
  save_result = gr.Label()
224
 
225
+
226
  save_btn.click(
227
  fn=lambda session_index: save_data(session_index, session_manager, service, SHEET_ID1),
228
  inputs=[session_index],
229
  outputs=[save_result]
230
  )
231
+
232
+ with gr.Accordion(label="Appendix: AI instructions", open=False):
233
+ gr.Markdown(GPT_PROMPT_sequential)
234
 
235
  with group_b_content:
236
  with gr.Row():
237
+ human_input = gr.Textbox(label="Human Input(At least 50 words)")
238
  with gr.Row():
239
  create_btn = gr.Button("Create")
240
  with gr.Row():
 
244
 
245
  create_btn.click(
246
  fn=lambda task, human_input, id: handle_create_parallel(task, human_input, session_manager, api_key, id),
247
+ inputs=[description, human_input, identification_code],
248
  outputs=[ai_initial_output, final_output, session_index]
249
  )
250
 
251
+ ai_initial_output.change(
252
+ fn = lambda session_index: on_textbox_change(session_index, session_manager, service, SHEET_ID2),
253
+ inputs = [session_index]
254
+ )
255
 
256
 
257
  evaluate_btn = gr.Button("Evaluate")
 
263
  outputs=[evaluation_result]
264
  )
265
 
266
+ evaluation_result.change(
267
+ fn = lambda session_index: on_textbox_change(session_index, session_manager, service, SHEET_ID2),
268
+ inputs = [session_index]
269
+ )
270
+
271
+ save_btn = gr.Button("Save Data",elem_id="save_btn")
272
  save_result = gr.Label()
273
 
274
  save_btn.click(
 
277
  outputs=[save_result]
278
  )
279
 
280
+ with gr.Accordion(label="Appendix: AI instructions", open=False):
281
+ gr.Markdown(GPT_PROMPT_parallel)
282
+
283
  with group_c_content:
284
  with gr.Row():
285
  create_initial_btn = gr.Button("Create")
286
  with gr.Row():
287
  initial_answer = gr.Textbox(label="AI Initial Answer")
288
+ modification_suggestions = gr.Textbox(label="Modification Suggestions(At least 50 words)")
289
  with gr.Row():
290
  create_final_btn = gr.Button("Modify")
291
  with gr.Row():
 
295
 
296
  create_initial_btn.click(
297
  fn=lambda task, id: handle_create_reverse_sequential(task, session_manager, api_key, id),
298
+ inputs=[description, identification_code],
299
  outputs=[initial_answer, session_index]
300
  )
301
 
302
+ initial_answer.change(
303
+ fn = lambda session_index: on_textbox_change(session_index, session_manager, service, SHEET_ID3),
304
+ inputs = [session_index]
305
+ )
306
+
307
  create_final_btn.click(
308
  fn=lambda session_index, modification_suggestions: handle_modify_reverse_sequential(session_index, modification_suggestions, session_manager, api_key),
309
  inputs=[session_index, modification_suggestions],
310
  outputs=[final_answer, session_index]
311
  )
312
 
313
+ final_answer.change(
314
+ fn = lambda session_index: on_textbox_change(session_index, session_manager, service, SHEET_ID3),
315
+ inputs = [session_index]
316
+ )
317
+
318
  evaluate_btn = gr.Button("Evaluate")
319
  evaluation_result = gr.Textbox(label="Evaluation Result")
320
 
 
324
  outputs=[evaluation_result]
325
  )
326
 
327
+ evaluation_result.change(
328
+ fn = lambda session_index: on_textbox_change(session_index, session_manager, service, SHEET_ID3),
329
+ inputs = [session_index]
330
+ )
331
+
332
+ save_btn = gr.Button("Save Data", elem_id="save_btn")
333
  save_result = gr.Label()
334
 
335
  save_btn.click(
 
337
  inputs=[session_index],
338
  outputs=[save_result]
339
  )
340
+
341
+ with gr.Accordion(label="Appendix: AI instructions", open=False):
342
+ gr.Markdown(GPT_PROMPT_reverse_sequential)
343
 
344
  app.launch(share=True)
constants.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ SDG_DETAILS = """
2
+ 1. No Poverty: End poverty in all its forms everywhere.
3
+ 2. Zero Hunger: End hunger, achieve food security and improved nutrition, and promote sustainable agriculture.
4
+ 3. Good Health and Well-being: Ensure healthy lives and promote well-being for all at all ages.
5
+ 4. Quality Education: Ensure inclusive and equitable quality education and promote lifelong learning opportunities for all.
6
+ 5. Gender Equality: Achieve gender equality and empower all women and girls.
7
+ 6. Clean Water and Sanitation: Ensure availability and sustainable management of water and sanitation for all.
8
+ 7. Affordable and Clean Energy: Ensure access to affordable, reliable, sustainable and modern energy for all.
9
+ 8. Decent Work and Economic Growth: Promote sustained, inclusive and sustainable economic growth, full and productive employment and decent work for all.
10
+ 9. Industry, Innovation and Infrastructure: Build resilient infrastructure, promote inclusive and sustainable industrialization and foster innovation.
11
+ 10. Reduced Inequality: Reduce inequality within and among countries.
12
+ 11. Sustainable Cities and Communities: Make cities and human settlements inclusive, safe, resilient and sustainable.
13
+ 12. Responsible Consumption and Production: Ensure sustainable consumption and production patterns.
14
+ 13. Climate Action: Take urgent action to combat climate change and its impacts.
15
+ 14. Life Below Water: Conserve and sustainably use the oceans, seas and marine resources for sustainable development.
16
+ 15. Life on Land: Protect, restore and promote sustainable use of terrestrial ecosystems, sustainably manage forests, combat desertification, and halt and reverse land degradation and halt biodiversity loss.
17
+ 16. Peace and Justice Strong Institutions: Promote peaceful and inclusive societies for sustainable development, provide access to justice for all and build effective, accountable and inclusive institutions at all levels.
18
+ 17. Partnerships to achieve the Goal: Strengthen the means of implementation and revitalize the Global Partnership for Sustainable Development.
19
+ """
20
+
21
+ GPT_PROMPT_parallel="""
22
+ For AI generted/modified parts, our base model is GPT-3.5-turbo.
23
+
24
+ For direct generation, we use instructions like following:
25
+ "Given the task as: [task_description], provide an answer: "
26
+
27
+ For the merging, we use instructions like following:
28
+ "Given the task as: [task_description], there are two answers provided:
29
+ The first answer: [human_text]
30
+ The second answer: [ai_text]
31
+ Merge the two answers into one in a coherent way: "
32
+ """
33
+
34
+ GPT_PROMPT_sequential="""
35
+ For AI generted/modified parts, our base model is GPT-3.5-turbo.
36
+
37
+ For the modification, we use instructions like following:
38
+ "Given the task as :[task_description, the human answer is: [human_text]
39
+ Provide an answer of your own but make sure it is coherent and should be based on the human answer: "
40
+ """
41
+
42
+ GPT_PROMPT_reverse_sequential="""
43
+ For AI generted/modified parts, our base model is GPT-3.5-turbo.
44
+
45
+ For direct generation, we use instructions like following:
46
+ "Given the task as: [task_description], provide an answer: "
47
+
48
+ For the merging, we use instructions like following:
49
+ "Given the task as: [task_description], there are two answers provided:
50
+ The first answer: [human_text]
51
+ The second answer: [ai_text]
52
+ Merge the two answers into one in a coherent way: "
53
+ """
save_data.py CHANGED
@@ -10,7 +10,7 @@ def load_envs(local=False):
10
  """Load the environment variables."""
11
  if local:
12
  from dotenv import load_dotenv
13
- load_dotenv()
14
  service_account_info = json.loads(os.environ['GOOGLE_APPLICATION_CREDENTIALS_JSON'])
15
  SHEET_ID1 = os.environ['SHEET_ID1'] # human-ai-sequential
16
  SHEET_ID2 = os.environ['SHEET_ID2'] # human-ai-parallel
@@ -40,6 +40,14 @@ def col_letter(col_num):
40
  letter = chr(65 + remainder) + letter
41
  return letter
42
 
 
 
 
 
 
 
 
 
43
  def add_new_data(new_row, service, SPREADSHEET_ID, num_of_columns = 5):
44
  """Add new data to the spreadsheet.
45
  new_row: list of data to be added. """
@@ -50,23 +58,49 @@ def add_new_data(new_row, service, SPREADSHEET_ID, num_of_columns = 5):
50
  spreadsheetId=SPREADSHEET_ID,
51
  range=range_to_read
52
  ).execute()
 
 
 
 
 
 
 
 
 
 
 
 
53
  values = result.get('values', [])
54
  number_of_rows = len(values)
55
  new_row = [new_row]
56
- range_to_write = f'Sheet1!A{number_of_rows + 1}'
 
57
 
58
  request_body = {
59
  'values': new_row
60
  }
61
- response = service.spreadsheets().values().append(
62
- spreadsheetId=SPREADSHEET_ID,
63
- range=range_to_write,
64
- valueInputOption='RAW',
65
- insertDataOption='INSERT_ROWS',
66
- body=request_body
67
- ).execute()
68
-
69
- print(f"Added new row at position {number_of_rows + 1}")
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
 
72
 
 
10
  """Load the environment variables."""
11
  if local:
12
  from dotenv import load_dotenv
13
+ load_dotenv(override=True)
14
  service_account_info = json.loads(os.environ['GOOGLE_APPLICATION_CREDENTIALS_JSON'])
15
  SHEET_ID1 = os.environ['SHEET_ID1'] # human-ai-sequential
16
  SHEET_ID2 = os.environ['SHEET_ID2'] # human-ai-parallel
 
40
  letter = chr(65 + remainder) + letter
41
  return letter
42
 
43
+ def col_letter(col_num):
44
+ """Convert a column number to its corresponding Excel-style letter."""
45
+ letter = ''
46
+ while col_num > 0:
47
+ col_num, remainder = divmod(col_num - 1, 26)
48
+ letter = chr(65 + remainder) + letter
49
+ return letter
50
+
51
  def add_new_data(new_row, service, SPREADSHEET_ID, num_of_columns = 5):
52
  """Add new data to the spreadsheet.
53
  new_row: list of data to be added. """
 
58
  spreadsheetId=SPREADSHEET_ID,
59
  range=range_to_read
60
  ).execute()
61
+
62
+
63
+ # search for first columns match
64
+ match_found = False
65
+ update_idx = None
66
+ for idx, row in enumerate(result.get('values', [])):
67
+ if row[0] == new_row[0]:
68
+ match_found = True
69
+ range_to_write = f'Sheet1!A{idx + 1}:{col_letter(num_of_columns)}{idx + 1}'
70
+ update_idx = idx + 1
71
+ break
72
+
73
  values = result.get('values', [])
74
  number_of_rows = len(values)
75
  new_row = [new_row]
76
+ if not match_found:
77
+ range_to_write = f'Sheet1!A{number_of_rows + 1}'
78
 
79
  request_body = {
80
  'values': new_row
81
  }
82
+ if match_found:
83
+ service.spreadsheets().values().clear(
84
+ spreadsheetId=SPREADSHEET_ID,
85
+ range=range_to_write,
86
+ body={},
87
+ ).execute()
88
+ response = service.spreadsheets().values().update(
89
+ spreadsheetId=SPREADSHEET_ID,
90
+ range=range_to_write,
91
+ valueInputOption='RAW',
92
+ body=request_body
93
+ ).execute()
94
+ print(f"Updated row at position {update_idx}")
95
+ else:
96
+ response = service.spreadsheets().values().append(
97
+ spreadsheetId=SPREADSHEET_ID,
98
+ range=range_to_write,
99
+ valueInputOption='RAW',
100
+ insertDataOption='INSERT_ROWS',
101
+ body=request_body
102
+ ).execute()
103
+ print(f"Added new row at position {number_of_rows + 1}")
104
 
105
 
106