@saltcorn/copilot 0.5.3 → 0.6.1

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/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.3",
3
+ "version": "0.6.1",
4
4
  "description": "AI assistant for building Saltcorn applications",
5
5
  "main": "index.js",
6
6
  "dependencies": {
@@ -0,0 +1,214 @@
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: "image_prompt",
32
+ label: "Prompt image files",
33
+ sublabel:
34
+ "Optional. An expression, based on the context, for file path or array of file paths for prompting",
35
+ class: "validate-expression",
36
+ type: "String",
37
+ },
38
+ {
39
+ name: "answer_field",
40
+ label: "Answer variable",
41
+ sublabel: "Optional. Set the generated HTML to this context variable",
42
+ class: "validate-identifier",
43
+ type: "String",
44
+ },
45
+ // ...override_fields,
46
+ {
47
+ name: "model",
48
+ label: "Model",
49
+ sublabel: "Override default model name",
50
+ type: "String",
51
+ },
52
+ ];
53
+ } else if (table) {
54
+ const textFields = table.fields
55
+ .filter((f) => f.type?.sql_name === "text")
56
+ .map((f) => f.name);
57
+
58
+ return [
59
+ {
60
+ name: "prompt_field",
61
+ label: "Prompt field",
62
+ sublabel: "Field with the text of the prompt",
63
+ type: "String",
64
+ required: true,
65
+ attributes: { options: [...textFields, "Formula"] },
66
+ },
67
+ {
68
+ name: "prompt_formula",
69
+ label: "Prompt formula",
70
+ type: "String",
71
+ showIf: { prompt_field: "Formula" },
72
+ },
73
+ {
74
+ name: "answer_field",
75
+ label: "Answer field",
76
+ sublabel: "Output field will be set to the generated answer",
77
+ type: "String",
78
+ required: true,
79
+ attributes: { options: textFields },
80
+ },
81
+ // ...override_fields,
82
+ ];
83
+ }
84
+ },
85
+ run: async ({
86
+ row,
87
+ table,
88
+ user,
89
+ mode,
90
+ configuration: {
91
+ page_name,
92
+ prompt_field,
93
+ prompt_formula,
94
+ prompt_template,
95
+ answer_field,
96
+ image_prompt,
97
+ chat_history_field,
98
+ model,
99
+ },
100
+ }) => {
101
+ let prompt;
102
+ if (mode === "workflow") prompt = interpolate(prompt_template, row, user);
103
+ else if (prompt_field === "Formula" || mode === "workflow")
104
+ prompt = eval_expression(
105
+ prompt_formula,
106
+ row,
107
+ user,
108
+ "copilot_generate_page prompt formula"
109
+ );
110
+ else prompt = row[prompt_field];
111
+ const opts = {};
112
+
113
+ if (model) opts.model = model;
114
+ const tools = [];
115
+ const systemPrompt = await GeneratePage.system_prompt();
116
+ tools.push({
117
+ type: "function",
118
+ function: {
119
+ name: GeneratePage.function_name,
120
+ description: GeneratePage.description,
121
+ parameters: await GeneratePage.json_schema(),
122
+ },
123
+ });
124
+ const { llm_generate } = getState().functions;
125
+ let chat;
126
+ if (image_prompt) {
127
+ const from_ctx = eval_expression(
128
+ image_prompt,
129
+ row,
130
+ user,
131
+ "copilot_generate_page image prompt"
132
+ );
133
+
134
+ chat = [];
135
+ for (const image of Array.isArray(from_ctx) ? from_ctx : [from_ctx]) {
136
+ const file = await File.findOne({ name: image });
137
+ const imageurl = await file.get_contents("base64");
138
+
139
+ chat.push({
140
+ role: "user",
141
+ content: [
142
+ {
143
+ type: "image",
144
+ image: `data:${file.mimetype};base64,${imageurl}`,
145
+ },
146
+ ],
147
+ });
148
+ }
149
+ }
150
+ const initial_ans = await llm_generate.run(prompt, {
151
+ tools,
152
+ chat,
153
+ systemPrompt,
154
+ });
155
+ const initial_info = initial_ans.tool_calls[0].input;
156
+ const full = await GeneratePage.follow_on_generate(initial_info);
157
+
158
+ const page_html = await getState().functions.llm_generate.run(
159
+ `${prompt}.
160
+
161
+ The page title is: ${initial_info.title}.
162
+ Further page description: ${initial_info.description}.
163
+
164
+ Generate the HTML for the web page using the Bootstrap 5 CSS framework.
165
+ If you need to include the standard bootstrap CSS and javascript files, they are available as:
166
+
167
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
168
+
169
+ and
170
+
171
+ <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>
172
+
173
+ Just generate HTML code, do not wrap in markdown code tags`,
174
+ {
175
+ debugResult: true,
176
+ chat,
177
+ response_format: full.response_schema
178
+ ? {
179
+ type: "json_schema",
180
+ json_schema: {
181
+ name: "generate_page",
182
+ schema: full.response_schema,
183
+ },
184
+ }
185
+ : undefined,
186
+ }
187
+ );
188
+
189
+ const use_page_name = page_name ? interpolate(page_name, row, user) : "";
190
+ if (use_page_name) {
191
+ //save to a file
192
+ const file = await File.from_contents(
193
+ `${use_page_name}.html`,
194
+ "text/html",
195
+ page_html,
196
+ user.id,
197
+ 100
198
+ );
199
+
200
+ //create page
201
+ await Page.create({
202
+ name: use_page_name,
203
+ title: initial_info.title,
204
+ description: initial_info.description,
205
+ min_role: 100,
206
+ layout: { html_file: file.path_to_serve },
207
+ });
208
+ getState().refresh_pages();
209
+ }
210
+ const upd = answer_field ? { [answer_field]: page_html } : {};
211
+ if (mode === "workflow") return upd;
212
+ else if (answer_field) await table.updateRow(upd, row[table.pk_name]);
213
+ },
214
+ };