@saltcorn/copilot 0.5.2 → 0.6.0

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.
@@ -361,90 +361,4 @@ function escapeHtml(unsafe) {
361
361
  .replace(/'/g, "'");
362
362
  }
363
363
 
364
- parseHTML(`<!DOCTYPE html>
365
- <html lang="en">
366
- <head>
367
- <meta charset="UTF-8">
368
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
369
- <title>Popcorn Beer - Unleash the Flavor Explosion!</title>
370
- <link href="https://stackpath.bootstrapcdn.com/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
371
- </head>
372
- <body>
373
-
374
- <header class="bg-warning text-dark py-5">
375
- <div class="container text-center">
376
- <h1 class="display-4">Popcorn Beer</h1>
377
- <p class="lead">Unleash the Flavor Explosion!</p>
378
- </div>
379
- </header>
380
-
381
- <section class="py-5">
382
- <div class="container">
383
- <div class="row align-items-center">
384
- <div class="col-md-6">
385
- <img src="your-image-url.jpg" alt="Popcorn Beer" class="img-fluid rounded">
386
- </div>
387
- <div class="col-md-6">
388
- <h2>Discover the Unique Taste</h2>
389
- <p>Introducing our popcorn-flavored beer, a delightful fusion of classic beer with a twist of popcorn essence. Experience the taste sensation that will keep you coming back for more.</p>
390
- <a href="#purchase" class="btn btn-warning btn-lg mt-3">Buy Now</a>
391
- </div>
392
- </div>
393
- </div>
394
- </section>
395
-
396
- <section class="bg-light py-5">
397
- <div class="container">
398
- <h2 class="text-center mb-4">Why Choose Popcorn Beer?</h2>
399
- <div class="row">
400
- <div class="col-lg-4">
401
- <div class="card mb-4">
402
- <div class="card-body text-center">
403
- <h5 class="card-title">Unique Flavor</h5>
404
- <p class="card-text">A perfect blend of beer and popcorn that tantalizes your taste buds.</p>
405
- </div>
406
- </div>
407
- </div>
408
- <div class="col-lg-4">
409
- <div class="card mb-4">
410
- <div class="card-body text-center">
411
- <h5 class="card-title">Premium Ingredients</h5>
412
- <p class="card-text">Crafted with the finest ingredients for an unparalleled taste.</p>
413
- </div>
414
- </div>
415
- </div>
416
- <div class="col-lg-4">
417
- <div class="card mb-4">
418
- <div class="card-body text-center">
419
- <h5 class="card-title">Perfect for Any Occasion</h5>
420
- <p class="card-text">Whether it's a movie night or a party, Popcorn Beer is your go-to drink.</p>
421
- </div>
422
- </div>
423
- </div>
424
- </div>
425
- </div>
426
- </section>
427
-
428
- <section id="purchase" class="py-5">
429
- <div class="container text-center">
430
- <h2>Get Your Popcorn Beer Today!</h2>
431
- <p class="mb-4">Available for purchase online and in select stores near you.</p>
432
- <a href="shop.html" class="btn btn-warning btn-lg">Shop Now</a>
433
- </div>
434
- </section>
435
-
436
- <footer class="bg-dark text-white py-4">
437
- <div class="container text-center">
438
- <p>&copy; 2023 Popcorn Beer. All rights reserved.</p>
439
- <ul class="list-inline">
440
- <li class="list-inline-item"><a href="#" class="text-white">Privacy Policy</a></li>
441
- <li class="list-inline-item"><a href="#" class="text-white">Terms of Service</a></li>
442
- <li class="list-inline-item"><a href="#" class="text-white">Contact Us</a></li>
443
- </ul>
444
- </div>
445
- </footer>
446
-
447
- <script src="https://stackpath.bootstrapcdn.com/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
448
- </body>`);
449
-
450
364
  module.exports = GeneratePage;
package/chat-copilot.js CHANGED
@@ -337,23 +337,24 @@ const execute = async (table_id, viewname, config, body, { req }) => {
337
337
 
338
338
  const fcall = run.context.funcalls[fcall_id];
339
339
  const actionClass = classesWithSkills().find(
340
- (ac) => ac.function_name === fcall.name
340
+ (ac) => ac.function_name === (fcall.name || fcall.toolName)
341
341
  );
342
342
  let result;
343
+ const args = fcall.arguments ? JSON.parse(fcall.arguments) : fcall.input;
344
+
343
345
  if (actionClass.follow_on_generate) {
344
346
  const toolCallIndex = run.context.interactions.findIndex(
345
- (i) => i.tool_call_id === fcall_id
347
+ (i) =>
348
+ i.tool_call_id === fcall_id ||
349
+ (Array.isArray(i.content) &&
350
+ i.content.some((c) => c.toolCallId === fcall_id))
346
351
  );
347
352
  const follow_on_gen = run.context.interactions.find(
348
353
  (i, ix) => i.role === "assistant" && ix > toolCallIndex
349
354
  );
350
- result = await actionClass.execute(
351
- JSON.parse(fcall.arguments),
352
- req,
353
- follow_on_gen.content
354
- );
355
+ result = await actionClass.execute(args, req, follow_on_gen.content);
355
356
  } else {
356
- result = await actionClass.execute(JSON.parse(fcall.arguments), req);
357
+ result = await actionClass.execute(args, req);
357
358
  }
358
359
  await addToContext(run, { implemented_fcall_ids: [fcall_id] });
359
360
  return { json: { success: "ok", fcall_id, ...(result || {}) } };
@@ -390,27 +391,50 @@ const interact = async (table_id, viewname, config, body, { req }) => {
390
391
  userinput,
391
392
  complArgs
392
393
  );
393
- await addToContext(run, {
394
- interactions:
395
- typeof answer === "object" && answer.tool_calls
396
- ? [
397
- { role: "assistant", tool_calls: answer.tool_calls },
398
- ...answer.tool_calls.map((tc) => ({
399
- role: "tool",
400
- tool_call_id: tc.id,
401
- name: tc.function.name,
402
- content: "Action suggested to user.",
403
- })),
404
- ]
405
- : [{ role: "assistant", content: answer }],
406
- });
407
- console.log("answer", answer);
408
-
394
+ if (answer.ai_sdk)
395
+ for (const tool_call of answer.tool_calls)
396
+ await addToContext(run, {
397
+ interactions: [
398
+ ...answer.messages,
399
+ {
400
+ role: "tool",
401
+ content: [
402
+ {
403
+ type: "tool-result",
404
+ toolCallId: tool_call.toolCallId,
405
+ toolName: tool_call.toolName,
406
+ output: {
407
+ type: "text",
408
+ value: "Action suggested to user.",
409
+ },
410
+ },
411
+ ],
412
+ },
413
+ ],
414
+ });
415
+ else
416
+ await addToContext(run, {
417
+ interactions:
418
+ typeof answer === "object" && answer.tool_calls
419
+ ? [
420
+ { role: "assistant", tool_calls: answer.tool_calls },
421
+ ...answer.tool_calls.map((tc) => ({
422
+ role: "tool",
423
+ tool_call_id: tc.id || tc.toolCallId,
424
+ name: tc.function?.name || tc.toolName,
425
+ content: "Action suggested to user.",
426
+ })),
427
+ ]
428
+ : [{ role: "assistant", content: answer }],
429
+ });
409
430
  if (typeof answer === "object" && answer.tool_calls) {
410
431
  const actions = [];
411
432
  for (const tool_call of answer.tool_calls) {
412
433
  await addToContext(run, {
413
- funcalls: { [tool_call.id]: tool_call.function },
434
+ funcalls: {
435
+ [tool_call.id || tool_call.toolCallId]:
436
+ tool_call.function || tool_call,
437
+ },
414
438
  });
415
439
 
416
440
  const followOnGen = await getFollowOnGeneration(tool_call);
@@ -433,7 +457,6 @@ const interact = async (table_id, viewname, config, body, { req }) => {
433
457
  }
434
458
  );
435
459
 
436
- console.log("follow on answer", follow_on_answer);
437
460
 
438
461
  await addToContext(run, {
439
462
  interactions: [
@@ -465,11 +488,13 @@ const interact = async (table_id, viewname, config, body, { req }) => {
465
488
  };
466
489
 
467
490
  const getFollowOnGeneration = async (tool_call) => {
468
- const fname = tool_call.function.name;
491
+ const fname = tool_call.function?.name || tool_call.toolName;
469
492
  const actionClass = classesWithSkills().find(
470
493
  (ac) => ac.function_name === fname
471
494
  );
472
- const args = JSON.parse(tool_call.function.arguments);
495
+ const args = tool_call.function?.arguments
496
+ ? JSON.parse(tool_call.function?.arguments)
497
+ : tool_call.input;
473
498
 
474
499
  if (actionClass.follow_on_generate) {
475
500
  return await actionClass.follow_on_generate(args);
@@ -483,11 +508,13 @@ const renderToolcall = async (
483
508
  run,
484
509
  follow_on_answer
485
510
  ) => {
486
- const fname = tool_call.function.name;
511
+ const fname = tool_call.function?.name || tool_call.toolName;
487
512
  const actionClass = classesWithSkills().find(
488
513
  (ac) => ac.function_name === fname
489
514
  );
490
- const args = JSON.parse(tool_call.function.arguments);
515
+ const args = tool_call.function?.arguments
516
+ ? JSON.parse(tool_call.function.arguments)
517
+ : tool_call.input;
491
518
 
492
519
  const inner_markup = await actionClass.render_html(args, follow_on_answer);
493
520
  return wrapAction(
@@ -529,7 +556,9 @@ const wrapAction = (
529
556
  type: "button",
530
557
  id: "exec-" + tool_call.id,
531
558
  class: "btn btn-primary d-block mt-3 float-end",
532
- onclick: `press_store_button(this, true);view_post('${viewname}', 'execute', {fcall_id: '${tool_call.id}', run_id: ${run.id}}, processExecuteResponse)`,
559
+ onclick: `press_store_button(this, true);view_post('${viewname}', 'execute', {fcall_id: '${
560
+ tool_call.id || tool_call.toolCallId
561
+ }', run_id: ${run.id}}, processExecuteResponse)`,
533
562
  },
534
563
  "Apply"
535
564
  ),
package/index.js CHANGED
@@ -2,7 +2,6 @@ const Workflow = require("@saltcorn/data/models/workflow");
2
2
  const Form = require("@saltcorn/data/models/form");
3
3
  const { features } = require("@saltcorn/data/db/state");
4
4
 
5
-
6
5
  module.exports = {
7
6
  sc_plugin_api_version: 1,
8
7
  dependencies: ["@saltcorn/large-language-model"],
@@ -12,4 +11,5 @@ module.exports = {
12
11
  functions: features.workflows
13
12
  ? { copilot_generate_workflow: require("./workflow-gen") }
14
13
  : {},
14
+ actions: { copilot_generate_page: require("./page-gen-action") },
15
15
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/copilot",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "AI assistant for building Saltcorn applications",
5
5
  "main": "index.js",
6
6
  "dependencies": {
@@ -0,0 +1,179 @@
1
+ const { eval_expression } = require("@saltcorn/data/models/expression");
2
+ const { interpolate } = require("@saltcorn/data/utils");
3
+ const { getState } = require("@saltcorn/data/db/state");
4
+ const File = require("@saltcorn/data/models/file");
5
+ const Page = require("@saltcorn/data/models/page");
6
+
7
+ const GeneratePage = require("./actions/generate-page");
8
+
9
+ module.exports = {
10
+ description: "Generate page with AI copilot",
11
+ configFields: ({ table, mode }) => {
12
+ if (mode === "workflow") {
13
+ return [
14
+ {
15
+ name: "page_name",
16
+ label: "Page name",
17
+ sublabel:
18
+ "Leave blank to not save a Saltcorn page. Use interpolations {{ }} to access variables in the context",
19
+ type: "String",
20
+ },
21
+ {
22
+ name: "prompt_template",
23
+ label: "Prompt",
24
+ sublabel:
25
+ "Prompt text. Use interpolations {{ }} to access variables in the context",
26
+ type: "String",
27
+ fieldview: "textarea",
28
+ required: true,
29
+ },
30
+ {
31
+ name: "answer_field",
32
+ label: "Answer variable",
33
+ sublabel: "Optional. Set the generated HTML to this context variable",
34
+ type: "String",
35
+ required: true,
36
+ },
37
+ // ...override_fields,
38
+ {
39
+ name: "model",
40
+ label: "Model",
41
+ sublabel: "Override default model name",
42
+ type: "String",
43
+ },
44
+ ];
45
+ } else if (table) {
46
+ const textFields = table.fields
47
+ .filter((f) => f.type?.sql_name === "text")
48
+ .map((f) => f.name);
49
+
50
+ return [
51
+ {
52
+ name: "prompt_field",
53
+ label: "Prompt field",
54
+ sublabel: "Field with the text of the prompt",
55
+ type: "String",
56
+ required: true,
57
+ attributes: { options: [...textFields, "Formula"] },
58
+ },
59
+ {
60
+ name: "prompt_formula",
61
+ label: "Prompt formula",
62
+ type: "String",
63
+ showIf: { prompt_field: "Formula" },
64
+ },
65
+ {
66
+ name: "answer_field",
67
+ label: "Answer field",
68
+ sublabel: "Output field will be set to the generated answer",
69
+ type: "String",
70
+ required: true,
71
+ attributes: { options: textFields },
72
+ },
73
+ // ...override_fields,
74
+ ];
75
+ }
76
+ },
77
+ run: async ({
78
+ row,
79
+ table,
80
+ user,
81
+ mode,
82
+ configuration: {
83
+ page_name,
84
+ prompt_field,
85
+ prompt_formula,
86
+ prompt_template,
87
+ answer_field,
88
+ chat_history_field,
89
+ model,
90
+ },
91
+ }) => {
92
+ let prompt;
93
+ if (mode === "workflow") prompt = interpolate(prompt_template, row, user);
94
+ else if (prompt_field === "Formula" || mode === "workflow")
95
+ prompt = eval_expression(
96
+ prompt_formula,
97
+ row,
98
+ user,
99
+ "llm_generate prompt formula"
100
+ );
101
+ else prompt = row[prompt_field];
102
+ const opts = {};
103
+
104
+ if (model) opts.model = model;
105
+ const tools = [];
106
+ const systemPrompt = await GeneratePage.system_prompt();
107
+ tools.push({
108
+ type: "function",
109
+ function: {
110
+ name: GeneratePage.function_name,
111
+ description: GeneratePage.description,
112
+ parameters: await GeneratePage.json_schema(),
113
+ },
114
+ });
115
+ const { llm_generate } = getState().functions;
116
+
117
+ const initial_ans = await llm_generate.run(prompt, {
118
+ tools,
119
+ systemPrompt,
120
+ });
121
+ const initial_info = initial_ans.tool_calls[0].input;
122
+ const full = await GeneratePage.follow_on_generate(initial_info);
123
+
124
+ const page_html = await getState().functions.llm_generate.run(
125
+ `${prompt}.
126
+
127
+ The page title is: ${initial_info.title}.
128
+ Further page description: ${initial_info.description}.
129
+
130
+ Generate the HTML for the web page using the Bootstrap 5 CSS framework.
131
+ If you need to include the standard bootstrap CSS and javascript files, they are available as:
132
+
133
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
134
+
135
+ and
136
+
137
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
138
+
139
+ Just generate HTML code, do not wrap in markdown code tags`,
140
+ {
141
+ debugResult: true,
142
+ response_format: full.response_schema
143
+ ? {
144
+ type: "json_schema",
145
+ json_schema: {
146
+ name: "generate_page",
147
+ schema: full.response_schema,
148
+ },
149
+ }
150
+ : undefined,
151
+ }
152
+ );
153
+
154
+ const use_page_name = page_name ? interpolate(page_name, row, user) : "";
155
+ if (use_page_name) {
156
+ //save to a file
157
+ const file = await File.from_contents(
158
+ `${use_page_name}.html`,
159
+ "text/html",
160
+ page_html,
161
+ user.id,
162
+ 100
163
+ );
164
+
165
+ //create page
166
+ await Page.create({
167
+ name: use_page_name,
168
+ title: initial_info.title,
169
+ description: initial_info.description,
170
+ min_role: 100,
171
+ layout: { html_file: file.path_to_serve },
172
+ });
173
+ getState().refresh_pages()
174
+ }
175
+ const upd = answer_field ? { [answer_field]: page_html } : {};
176
+ if (mode === "workflow") return upd;
177
+ else if (answer_field) await table.updateRow(upd, row[table.pk_name]);
178
+ },
179
+ };