@saltcorn/agents 0.7.5 → 0.7.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/action.js CHANGED
@@ -90,24 +90,28 @@ module.exports = {
90
90
  run_id,
91
91
  req,
92
92
  is_sub_agent,
93
+ agent_view_config,
94
+ dyn_updates,
93
95
  ...rest
94
96
  }) => {
95
97
  const userinput = interpolate(configuration.prompt, row, user);
96
98
 
97
- const run = run_id
98
- ? await WorkflowRun.findOne({ id: run_id })
99
- : await WorkflowRun.create({
100
- status: "Running",
101
- started_by: user?.id,
102
- trigger_id: trigger_id || undefined,
103
- context: {
104
- implemented_fcall_ids: [],
105
- interactions: [{ role: "user", content: userinput }],
106
- funcalls: {},
107
- },
108
- });
109
- if (run_id)
110
- run.context.interactions.push({ role: "user", content: userinput });
99
+ const run =
100
+ rest.run ||
101
+ (run_id
102
+ ? await WorkflowRun.findOne({ id: run_id })
103
+ : await WorkflowRun.create({
104
+ status: "Running",
105
+ started_by: user?.id,
106
+ trigger_id: trigger_id || undefined,
107
+ context: {
108
+ implemented_fcall_ids: [],
109
+ interactions: [],
110
+ funcalls: {},
111
+ },
112
+ }));
113
+
114
+ run.context.interactions.push({ role: "user", content: userinput });
111
115
  return await process_interaction(
112
116
  run,
113
117
  configuration,
@@ -115,9 +119,9 @@ module.exports = {
115
119
  undefined,
116
120
  [],
117
121
  row,
118
- { stream: false },
119
- false,
120
- is_sub_agent
122
+ agent_view_config || { stream: false },
123
+ dyn_updates,
124
+ is_sub_agent,
121
125
  );
122
126
  },
123
127
  };
package/agent-view.js CHANGED
@@ -18,6 +18,7 @@ const {
18
18
  input,
19
19
  h4,
20
20
  h3,
21
+ h2,
21
22
  style,
22
23
  h5,
23
24
  button,
@@ -192,13 +193,15 @@ const realTimeCollabScript = (viewname, rndid, layout) => {
192
193
  ['${view.getRealTimeEventName(
193
194
  "STREAM_CHUNK",
194
195
  )}' + \`?page_load_tag=\${_sc_pageloadtag}\`]: async (data) => {
196
+ $(".agent-waiting-indicator").remove();
195
197
  window['stream scratch ${viewname} ${rndid}'].push(data.content)
196
198
  const rendered = md.render(window['stream scratch ${viewname} ${rndid}'].join(""));
197
- $('form.agent-view div.next_response_scratch').html(
199
+ $('div.next_response_scratch').html(
198
200
  (${JSON.stringify(layout || "")} || "").startsWith("Modern chat")
199
201
  ? '<div class="chat-message chat-assistant"><div class="chat-avatar"><i class="fas fa-robot"></i></div><div class="chat-bubble">' + rendered + '</div></div>'
200
202
  : rendered
201
203
  );
204
+ scrollAgentToBottom();
202
205
  }
203
206
  }
204
207
  };
@@ -434,7 +437,7 @@ const run = async (
434
437
  const rndid = Math.floor(Math.random() * 16777215).toString(16);
435
438
  const input_form = form(
436
439
  {
437
- onsubmit: `event.preventDefault();spin_send_button();view_post('${viewname}', 'interact', new FormData(this), ${dyn_updates ? "null" : "processCopilotResponse"});return false;`,
440
+ onsubmit: `event.preventDefault();const _fd=new FormData(this);spin_send_button();view_post('${viewname}', 'interact', _fd, ${dyn_updates ? "null" : "processCopilotResponse"});return false;`,
438
441
  class: ["form-namespace copilot mt-2 agent-view"],
439
442
  method: "post",
440
443
  },
@@ -492,8 +495,7 @@ const run = async (
492
495
  explainer && small({ class: "explainer" }, i(explainer)),
493
496
  ),
494
497
  stream &&
495
- realTimeCollabScript(viewname, rndid, layout) +
496
- div({ class: "next_response_scratch" }),
498
+ realTimeCollabScript(viewname, rndid, layout),
497
499
  );
498
500
 
499
501
  const isModernSidebar = layout && layout.startsWith("Modern chat");
@@ -612,6 +614,7 @@ const run = async (
612
614
  req.__("Sessions"),
613
615
  ),
614
616
  div({ id: "copilotinteractions" }, runInteractions),
617
+ stream ? div({ class: "next_response_scratch" }) : "",
615
618
  input_form,
616
619
  style(
617
620
  `div.interaction-segment:not(:first-child) {border-top: 1px solid #e7e7e7; }
@@ -685,6 +688,14 @@ const run = async (
685
688
  margin-bottom: 0px;
686
689
  display: block;
687
690
  text-overflow: ellipsis;}
691
+ /* Typing / Waiting Indicator */
692
+ .agent-waiting-indicator { display:flex; align-items:center; padding:0.75rem 1rem; }
693
+ .typing-dots { display:flex; gap:4px; align-items:center; }
694
+ .typing-dots span { width:8px; height:8px; border-radius:50%; background:#6c757d; animation:typingBounce 1.4s infinite ease-in-out both; }
695
+ .typing-dots span:nth-child(1) { animation-delay:-0.32s; }
696
+ .typing-dots span:nth-child(2) { animation-delay:-0.16s; }
697
+ .typing-dots span:nth-child(3) { animation-delay:0s; }
698
+ @keyframes typingBounce { 0%,80%,100%{transform:scale(.6);opacity:.4} 40%{transform:scale(1);opacity:1} }
688
699
  /* Modern Chat Layout */
689
700
  .modern-chat-layout {
690
701
  display: flex;
@@ -811,6 +822,21 @@ const run = async (
811
822
  .modern-chat-layout .chat-user .chat-bubble table th {
812
823
  background: rgba(255,255,255,0.1);
813
824
  }
825
+ /* Skill attribution badge */
826
+ .modern-chat-layout .chat-bubble .badge.bg-info {
827
+ display: inline-block;
828
+ margin-bottom: 6px;
829
+ font-size: 0.7rem;
830
+ font-weight: 600;
831
+ letter-spacing: 0.3px;
832
+ text-transform: uppercase;
833
+ opacity: 0.85;
834
+ }
835
+ .modern-chat-layout .chat-bubble .card.bg-secondary-subtle {
836
+ border: none;
837
+ background-color: rgba(0,0,0,0.03) !important;
838
+ margin-bottom: 0.5rem;
839
+ }
814
840
  /* Input area for modern chat */
815
841
  .modern-chat-layout .copilot-entry {
816
842
  border-top: 1px solid var(--tblr-border-color, var(--bs-border-color, #dee2e6));
@@ -881,6 +907,16 @@ const run = async (
881
907
  script(domReady(`$( "#inputuserinput" ).autogrow({paddingBottom: 20});`)),
882
908
  script(
883
909
  `
910
+ function scrollAgentToBottom() {
911
+ const container = document.getElementById('copilotinteractions');
912
+ if (container) {
913
+ if (container.scrollHeight > container.clientHeight) {
914
+ container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' });
915
+ }
916
+ const inputForm = document.querySelector('form.agent-view');
917
+ if (inputForm) inputForm.scrollIntoView({ behavior: 'smooth', block: 'end' });
918
+ }
919
+ }
884
920
  function close_session_list() {
885
921
  $("div.prev-runs-list").hide().parents(".col-3").removeClass("col-3").addClass("was-col-3").parent().children(".col-9").removeClass("col-9").addClass("col-12")
886
922
  $("div.open-prev-runs").show()
@@ -916,14 +952,21 @@ const run = async (
916
952
  if(user_input && (!${JSON.stringify(dyn_updates)}))
917
953
  $("#copilotinteractions").append(wrapSegment('<p>'+user_input+'</p>'+fileBadge, "You", true))
918
954
  $("textarea[name=userinput]").val("")
919
- $('form.agent-view div.next_response_scratch').html("")
955
+ $('div.next_response_scratch').html("")
920
956
  window['stream scratch ${viewname} ${rndid}'] = []
921
- if(res.response)
922
- $("#copilotinteractions").append(res.response)
957
+ if(res.response) {
958
+ $(".agent-waiting-indicator").remove();
959
+ $("#copilotinteractions").append(res.response);
960
+ scrollAgentToBottom();
961
+ }
923
962
  }
924
963
  window.processCopilotResponse = processCopilotResponse;
925
964
  window.final_agent_response = () => {
926
965
  $("#sendbuttonicon").attr("class","far fa-paper-plane");
966
+ $(".agent-waiting-indicator").remove();
967
+ $("textarea[name=userinput]").prop("disabled", false).attr("placeholder", ${JSON.stringify(placeholder || "How can I help you?")}).focus();
968
+ $(".copilot-entry .submit-button").css("pointer-events", "");
969
+ scrollAgentToBottom();
927
970
  }
928
971
  window._agentDT = new DataTransfer();
929
972
  function setAgentFiles(files) {
@@ -1046,6 +1089,14 @@ const run = async (
1046
1089
  }
1047
1090
  function spin_send_button() {
1048
1091
  $("#sendbuttonicon").attr("class","fas fa-spinner fa-spin");
1092
+ $("textarea[name=userinput]").prop("disabled", true).attr("placeholder", "Waiting for response...");
1093
+ $(".copilot-entry .submit-button").css("pointer-events", "none");
1094
+ const isModernLayout = ${JSON.stringify((layout || "").startsWith("Modern chat"))};
1095
+ const indicator = isModernLayout
1096
+ ? '<div class="agent-waiting-indicator chat-message chat-assistant"><div class="chat-avatar"><i class="fas fa-robot"></i></div><div class="chat-bubble"><div class="typing-dots"><span></span><span></span><span></span></div></div></div>'
1097
+ : '<div class="agent-waiting-indicator"><div class="typing-dots"><span></span><span></span><span></span></div></div>';
1098
+ $('div.next_response_scratch').before(indicator);
1099
+ scrollAgentToBottom();
1049
1100
  };`,
1050
1101
  stream &&
1051
1102
  domReady(
@@ -1238,12 +1289,80 @@ const debug_info = async (table_id, viewname, config, body, { req, res }) => {
1238
1289
  );
1239
1290
  sysPrompt = complArgs.systemPrompt;
1240
1291
  }
1292
+ const apiJson = JSON.stringify(run.context.api_interactions, null, 2);
1241
1293
  const debug_html = div(
1242
- div(h4("System prompt"), pre(text(escapeHtml(sysPrompt)))),
1294
+ { class: "accordion", id: "debugAccordion" },
1243
1295
  div(
1244
- h4("API interactions"),
1245
- pre(
1246
- text(escapeHtml(JSON.stringify(run.context.api_interactions, null, 2))),
1296
+ { class: "accordion-item" },
1297
+ h2(
1298
+ { class: "accordion-header", id: "debugHeadPrompt" },
1299
+ button(
1300
+ {
1301
+ class: "accordion-button collapsed",
1302
+ type: "button",
1303
+ "data-bs-toggle": "collapse",
1304
+ "data-bs-target": "#debugCollapsePrompt",
1305
+ "aria-expanded": "false",
1306
+ "aria-controls": "debugCollapsePrompt",
1307
+ },
1308
+ "System prompt",
1309
+ ),
1310
+ ),
1311
+ div(
1312
+ {
1313
+ id: "debugCollapsePrompt",
1314
+ class: "accordion-collapse collapse",
1315
+ "aria-labelledby": "debugHeadPrompt",
1316
+ "data-bs-parent": "#debugAccordion",
1317
+ },
1318
+ div(
1319
+ { class: "accordion-body" },
1320
+ pre({ style: "white-space:pre-wrap" }, text(escapeHtml(sysPrompt))),
1321
+ ),
1322
+ ),
1323
+ ),
1324
+ div(
1325
+ { class: "accordion-item" },
1326
+ h2(
1327
+ { class: "accordion-header", id: "debugHeadAPI" },
1328
+ button(
1329
+ {
1330
+ class: "accordion-button",
1331
+ type: "button",
1332
+ "data-bs-toggle": "collapse",
1333
+ "data-bs-target": "#debugCollapseAPI",
1334
+ "aria-expanded": "true",
1335
+ "aria-controls": "debugCollapseAPI",
1336
+ },
1337
+ "API interactions",
1338
+ ),
1339
+ ),
1340
+ div(
1341
+ {
1342
+ id: "debugCollapseAPI",
1343
+ class: "accordion-collapse collapse show",
1344
+ "aria-labelledby": "debugHeadAPI",
1345
+ "data-bs-parent": "#debugAccordion",
1346
+ },
1347
+ div(
1348
+ { class: "accordion-body" },
1349
+ button(
1350
+ {
1351
+ class: "btn btn-sm btn-outline-secondary mb-2",
1352
+ onclick: `
1353
+ var t=document.getElementById('debugApiPre').textContent;
1354
+ navigator.clipboard.writeText(t).then(function(){
1355
+ var b=event.target;b.textContent='Copied!';
1356
+ setTimeout(function(){b.textContent='Copy to clipboard'},1500)
1357
+ })`,
1358
+ },
1359
+ "Copy to clipboard",
1360
+ ),
1361
+ pre(
1362
+ { id: "debugApiPre", style: "white-space:pre-wrap" },
1363
+ text(escapeHtml(apiJson)),
1364
+ ),
1365
+ ),
1247
1366
  ),
1248
1367
  ),
1249
1368
  );
package/common.js CHANGED
@@ -489,6 +489,8 @@ const process_interaction = async (
489
489
  chat,
490
490
  req,
491
491
  run,
492
+ agent_view_config: agentsViewCfg,
493
+ dyn_updates,
492
494
  async generate(prompt, opts = {}) {
493
495
  generateUsed = true;
494
496
  return await sysState.functions.llm_generate.run(prompt, {
@@ -513,7 +515,7 @@ const process_interaction = async (
513
515
  });
514
516
  if (generateUsed)
515
517
  await addToContext(run, {
516
- interactions: chat,
518
+ interactions: run.context.interactions,
517
519
  });
518
520
  if (postprocres.stop) stop = true;
519
521
  if (postprocres.add_system_prompt)
@@ -544,35 +546,35 @@ const process_interaction = async (
544
546
  layout,
545
547
  ),
546
548
  );
547
- //replace tool response with this
548
- // run.context.interactions.forEach((ic) => {});
549
+
549
550
  const result = add_resp;
550
551
  await sysState.functions.llm_add_message.run(
551
552
  "assistant",
553
+
552
554
  !result || typeof result === "string"
553
- ? {
554
- type: "text",
555
- value: result || "Action run",
556
- }
557
- : {
558
- type: "json",
559
- value: JSON.parse(JSON.stringify(result)),
560
- },
555
+ ? result || "Action run"
556
+ : JSON.stringify(result),
557
+
561
558
  {
562
559
  chat: run.context.interactions,
563
560
  },
564
561
  );
565
- if (!postprocres.stop)
566
- await sysState.functions.llm_add_message.run(
567
- "user",
568
- {
569
- type: "text",
570
- value: "Continue",
571
- },
572
- {
573
- chat: run.context.interactions,
574
- },
575
- );
562
+ await addToContext(run, {
563
+ interactions: run.context.interactions,
564
+ });
565
+ }
566
+ if (!postprocres.stop) {
567
+ await sysState.functions.llm_add_message.run(
568
+ "user",
569
+ postprocres.follow_up_prompt || "Continue with the query",
570
+ {
571
+ chat: run.context.interactions,
572
+ },
573
+ );
574
+ await addToContext(run, {
575
+ interactions: run.context.interactions,
576
+ });
577
+ myHasResult = true;
576
578
  }
577
579
  if (postprocres.add_user_action && viewname) {
578
580
  const user_actions = Array.isArray()
@@ -635,7 +637,7 @@ const process_interaction = async (
635
637
  return {
636
638
  json: {
637
639
  success: "ok",
638
- ...(is_sub_agent ? { raw_responses } : {}),
640
+ ...(is_sub_agent && !stream ? { raw_responses } : {}),
639
641
  response: [...prevResponses, ...responses].join(""),
640
642
  run_id: run?.id,
641
643
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/agents",
3
- "version": "0.7.5",
3
+ "version": "0.7.7",
4
4
  "description": "AI agents for Saltcorn",
5
5
  "main": "index.js",
6
6
  "dependencies": {
@@ -18,6 +18,24 @@ const { validID } = require("@saltcorn/markup/layout_utils");
18
18
  const vm = require("vm");
19
19
  const { replaceUserContinue } = require("../common");
20
20
 
21
+ function extractCode(str) {
22
+ // Try ```javascript fence
23
+ if (str.includes("```javascript")) {
24
+ return str.split("```javascript")[1].split("```")[0];
25
+ }
26
+ // Try ```js fence
27
+ if (str.includes("```js\n") || str.includes("```js\r")) {
28
+ return str.split(/```js\s/)[1].split("```")[0];
29
+ }
30
+ // Try generic ``` fence (code is between first and second ```)
31
+ const fenceMatch = str.match(/```\s*\n([\s\S]*?)```/);
32
+ if (fenceMatch) {
33
+ return fenceMatch[1];
34
+ }
35
+ // No fences found - return raw string
36
+ return str;
37
+ }
38
+
21
39
  //const { fieldProperties } = require("./helpers");
22
40
 
23
41
  class GenerateAndRunJsCodeSkill {
@@ -105,6 +123,14 @@ class GenerateAndRunJsCodeSkill {
105
123
  sublabel: "Allow calls to functions from codepages and modules",
106
124
  type: "Bool",
107
125
  },
126
+ {
127
+ name: "follow_up_prompt",
128
+ label: "Follow-up prompt",
129
+ sublabel:
130
+ "If set, the agent will continue processing after code execution with this prompt. Leave empty to stop after code result.",
131
+ type: "String",
132
+ fieldview: "textarea",
133
+ },
108
134
  ...(Table.subClass
109
135
  ? [
110
136
  {
@@ -171,26 +197,34 @@ return { x, y }
171
197
 
172
198
  ${extra || ""}
173
199
 
200
+ CRITICAL: Your response must contain ONLY a single JavaScript code block wrapped in \`\`\`javascript ... \`\`\` fences. Do not include any text, explanation, or commentary before or after the code block.
201
+
174
202
  Now generate the JavaScript code required by the user.`,
175
203
  );
176
204
  getState().log(
177
205
  6,
178
206
  "Generated code:\n--BEGIN CODE--\n" + str + "\n--END CODE--\n",
179
207
  );
180
- const js_code = str.includes("```javascript")
181
- ? str.split("```javascript")[1].split("```")[0]
182
- : str;
208
+ const js_code = extractCode(str);
183
209
  return js_code;
184
210
  };
185
211
  const js_code = await gen_the_code();
186
212
  emit_update("Running code");
213
+ const ensureResult = (res) => {
214
+ if (res && typeof res === "object") return JSON.stringify(res);
215
+ if (res !== undefined && res !== null && res !== "") return res;
216
+ return "Code executed successfully but returned no output.";
217
+ };
187
218
  try {
188
219
  const res = await this.runCode(js_code, { user: req.user });
189
- //console.log("code response", res);
190
220
  getState().log(6, "Code answer: " + JSON.stringify(res));
221
+ const effectiveRes = ensureResult(res);
191
222
  return {
192
- stop: typeof res === "string",
193
- add_response: res,
223
+ stop: typeof res === "string" && !this.follow_up_prompt,
224
+ add_response: effectiveRes,
225
+ ...(this.follow_up_prompt
226
+ ? { follow_up_prompt: this.follow_up_prompt }
227
+ : {}),
194
228
  };
195
229
  } catch (err) {
196
230
  console.error(err);
@@ -208,13 +242,27 @@ ${err.message}
208
242
 
209
243
  Correct this error.
210
244
  `);
211
- const res = await this.runCode(retry_js_code, { user: req.user });
212
- //console.log("code response", res);
213
- getState().log(6, "Code retry answer: " + JSON.stringify(res));
214
- return {
215
- stop: typeof res === "string",
216
- add_response: res,
217
- };
245
+ try {
246
+ const res = await this.runCode(retry_js_code, {
247
+ user: req.user,
248
+ });
249
+ getState().log(6, "Code retry answer: " + JSON.stringify(res));
250
+ const effectiveRes = ensureResult(res);
251
+ return {
252
+ stop: typeof res === "string" && !this.follow_up_prompt,
253
+ add_response: effectiveRes,
254
+ ...(this.follow_up_prompt
255
+ ? { follow_up_prompt: this.follow_up_prompt }
256
+ : {}),
257
+ };
258
+ } catch (retryErr) {
259
+ console.error(retryErr);
260
+ return {
261
+ add_response:
262
+ "Error: code generation failed after retry: " +
263
+ retryErr.message,
264
+ };
265
+ }
218
266
  }
219
267
  },
220
268
  function: {
@@ -52,6 +52,12 @@ class SubagentToSkill {
52
52
  sublabel: `Optional. The prompt initialising the subagent. Example: "Continue answering my query using the tool now at you disposal"`,
53
53
  type: "String",
54
54
  },
55
+ {
56
+ name: "handoff_prompt",
57
+ label: "Handoff prompt",
58
+ sublabel: `Optional. A prompt to process the results of the subagent. Example: "Analyze this response in relation to my query"`,
59
+ type: "String",
60
+ },
55
61
  ];
56
62
  }
57
63
 
@@ -78,8 +84,10 @@ class SubagentToSkill {
78
84
  emit_update,
79
85
  run,
80
86
  chat,
87
+ agent_view_config,
88
+ dyn_updates,
81
89
  }) => {
82
- getState().log(6, "Running subagent", this.agent_name);
90
+ getState().log(6, "Running subagent", this.agent_name);
83
91
  const subres = await agent_action.run({
84
92
  row: {},
85
93
  configuration: {
@@ -89,15 +97,20 @@ class SubagentToSkill {
89
97
  "Your instructions and tools have changed. Continue answering my query using the instructions and tools at you disposal, if any",
90
98
  },
91
99
  user: req.user,
92
- run_id: run.id,
100
+ run,
93
101
  is_sub_agent: true,
102
+ agent_view_config,
103
+ dyn_updates,
94
104
  req,
95
105
  });
96
- getState().log(6, "Subagent response", subres.json.raw_responses);
97
-
98
- if (subres.json.raw_responses)
99
- return { add_responses: subres.json.raw_responses };
106
+ getState().log(6, "Subagent response", JSON.stringify(subres, null, 2));
107
+ //if (subres.json.raw_responses)
108
+ // return { add_responses: subres.json.raw_responses };
100
109
  return {
110
+ ...(this.handoff_prompt
111
+ ? { follow_up_prompt: this.handoff_prompt }
112
+ : { stop: true }),
113
+
101
114
  //stop: true,
102
115
  //add_response: result,
103
116
  };
@@ -38,7 +38,7 @@ beforeAll(async () => {
38
38
  });
39
39
 
40
40
  await getState().refresh_triggers(false);
41
- //await getState().setConfig("log_level", 6);
41
+ await getState().setConfig("log_level", 6);
42
42
  });
43
43
 
44
44
  jest.setTimeout(40000);
@@ -46,6 +46,13 @@ jest.setTimeout(40000);
46
46
  const user = { id: 1, role_id: 1 };
47
47
  const action = require("../action");
48
48
 
49
+ const getLastInteraction = async ({ run_id }) => {
50
+ const run = await WorkflowRuns.findOne({ id: run_id });
51
+ return JSON.stringify(
52
+ run.context.interactions[run.context.interactions.length - 1],
53
+ );
54
+ };
55
+
49
56
  for (const nameconfig of require("./configs")) {
50
57
  const { name, ...config } = nameconfig;
51
58
  describe("agent action with " + name, () => {
@@ -114,7 +121,10 @@ for (const nameconfig of require("./configs")) {
114
121
  user,
115
122
  req: { user },
116
123
  });
117
- expect(result.json.response).toContain("987");
124
+
125
+ const lastInteraction = await getLastInteraction(result.json);
126
+
127
+ expect(result.json.response || lastInteraction).toContain("987");
118
128
  });
119
129
  it("run multiple subagents concurrenty", async () => {
120
130
  const configuration = { ...require("./agentcfg").agent1 };
@@ -134,7 +144,8 @@ for (const nameconfig of require("./configs")) {
134
144
  user,
135
145
  req: { user },
136
146
  });
137
- expect(result.json.response).toContain("987");
147
+ const lastInteraction = await getLastInteraction(result.json);
148
+ expect(lastInteraction).toContain("987");
138
149
  });
139
150
  });
140
151