saltcorn-ai-fields 0.1.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/README.md ADDED
@@ -0,0 +1,307 @@
1
+ # saltcorn-ai-fields
2
+
3
+ AI-powered field actions for [Saltcorn](https://github.com/saltcorn/saltcorn) — fill, summarize, classify, translate, and extract using any LLM configured in the copilot plugin.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ - [Requirements](#requirements)
10
+ - [Installation](#installation)
11
+ - [Actions](#actions)
12
+ - [ai\_fill\_field](#ai_fill_field)
13
+ - [ai\_summarize](#ai_summarize)
14
+ - [ai\_classify](#ai_classify)
15
+ - [ai\_translate](#ai_translate)
16
+ - [ai\_extract](#ai_extract)
17
+ - [How to use actions in Saltcorn](#how-to-use-actions-in-saltcorn)
18
+ - [As a button in an Edit view](#as-a-button-in-an-edit-view)
19
+ - [As a row action in a List view](#as-a-row-action-in-a-list-view)
20
+ - [As a trigger (on insert / on update)](#as-a-trigger-on-insert--on-update)
21
+ - [Testing locally](#testing-locally)
22
+ - [License](#license)
23
+
24
+ ---
25
+
26
+ ## Requirements
27
+
28
+ - **Saltcorn** v1.0.0 or later
29
+ - **[saltcorn/copilot](https://github.com/saltcorn/copilot)** plugin — must be installed and configured with an OpenAI or compatible API key. This plugin registers the `llm_generate` function that `saltcorn-ai-fields` uses for all LLM calls.
30
+
31
+ ---
32
+
33
+ ## Installation
34
+
35
+ ### From the plugin store
36
+
37
+ 1. Go to **Settings → Plugins → Plugin Store**
38
+ 2. Search for `saltcorn-ai-fields`
39
+ 3. Click **Install**
40
+
41
+ ### From npm
42
+
43
+ ```bash
44
+ npm install saltcorn-ai-fields
45
+ ```
46
+
47
+ Then in Saltcorn: **Settings → Plugins → Install from npm** → enter `saltcorn-ai-fields`.
48
+
49
+ ---
50
+
51
+ ## Actions
52
+
53
+ ### `ai_fill_field`
54
+
55
+ Fill any field using a custom prompt. Reference any other field's value with `{{field_name}}`.
56
+
57
+ | Config field | Required | Description |
58
+ |---|---|---|
59
+ | Target field | ✅ | The field whose value will be set by the AI |
60
+ | Prompt | ✅ | Describe what to generate. Use `{{field_name}}` to inject row values |
61
+ | Response instruction | — | Extra instruction appended to prompt, e.g. `Be concise. Under 50 words.` |
62
+
63
+ **Example prompts:**
64
+
65
+ ```
66
+ Write a product description for {{name}} priced at ${{price}}.
67
+ ```
68
+ ```
69
+ Generate 5 SEO keywords for an article titled "{{title}}" in the {{category}} category.
70
+ ```
71
+ ```
72
+ Write a professional bio for {{first_name}} {{last_name}}, who works as {{job_title}} at {{company}}.
73
+ ```
74
+
75
+ ---
76
+
77
+ ### `ai_summarize`
78
+
79
+ Summarize a long text field and write the result to another field.
80
+
81
+ | Config field | Required | Description |
82
+ |---|---|---|
83
+ | Source field | ✅ | Text field to summarize |
84
+ | Target field | ✅ | Field where the summary will be written |
85
+ | Max words | — | Approximate word limit (default: 50) |
86
+ | Style | — | `Concise`, `Bullet points`, `One sentence`, `Formal`, `Casual` |
87
+
88
+ **Example use cases:**
89
+ - Summarize a long customer review into a one-sentence highlight
90
+ - Condense meeting notes into bullet points
91
+ - Create a short description from a long article body
92
+
93
+ ---
94
+
95
+ ### `ai_classify`
96
+
97
+ Classify a text field into one of your defined categories.
98
+
99
+ | Config field | Required | Description |
100
+ |---|---|---|
101
+ | Source field | ✅ | Text field to classify |
102
+ | Target field | ✅ | Field where the category will be written |
103
+ | Categories | ✅ | Comma-separated list, e.g. `Bug,Feature,Question,Other` |
104
+ | Context hint | — | Describe what the text is about to improve accuracy |
105
+
106
+ **Example use cases:**
107
+ - Classify support tickets: `Bug,Feature Request,Billing,General`
108
+ - Tag blog posts by topic: `Technology,Health,Finance,Travel,Food`
109
+ - Prioritise leads: `Hot,Warm,Cold`
110
+
111
+ > The AI response is validated against your category list. If the model returns an exact match (case-insensitive), the correctly-cased category is stored.
112
+
113
+ ---
114
+
115
+ ### `ai_translate`
116
+
117
+ Translate a text field to another language.
118
+
119
+ | Config field | Required | Description |
120
+ |---|---|---|
121
+ | Source field | ✅ | Text field to translate |
122
+ | Target field | ✅ | Field where the translation will be written |
123
+ | Target language | ✅ | One of 18 supported languages |
124
+ | Formality | — | `Default`, `Formal`, or `Informal` |
125
+
126
+ **Supported languages:**
127
+ Spanish, French, German, Italian, Portuguese, Dutch, Russian, Japanese, Chinese (Simplified), Chinese (Traditional), Korean, Arabic, Hindi, Turkish, Polish, Swedish, Norwegian, Danish.
128
+
129
+ ---
130
+
131
+ ### `ai_extract`
132
+
133
+ Extract specific information from a text field — dates, prices, email addresses, names, phone numbers, etc.
134
+
135
+ | Config field | Required | Description |
136
+ |---|---|---|
137
+ | Source field | ✅ | Text field to extract from |
138
+ | Target field | ✅ | Field where the extracted value will be written |
139
+ | What to extract | ✅ | Plain-English description of what to extract |
140
+ | Fallback value | — | Value to write if the information is not found |
141
+
142
+ **Example extraction prompts:**
143
+
144
+ ```
145
+ the total price including tax
146
+ ```
147
+ ```
148
+ the customer's email address
149
+ ```
150
+ ```
151
+ the delivery date in YYYY-MM-DD format
152
+ ```
153
+ ```
154
+ the company name mentioned in the first paragraph
155
+ ```
156
+
157
+ > If the information is not found and no fallback is set, the target field is left unchanged and a notification is shown.
158
+
159
+ ---
160
+
161
+ ## How to use actions in Saltcorn
162
+
163
+ ### As a button in an Edit view
164
+
165
+ This is the most common use case. The button calls the AI when clicked, fills in the field on the form, and the user can review the result before saving.
166
+
167
+ 1. Open a table and go to **Views**
168
+ 2. Create or open an **Edit** view
169
+ 3. In the view builder, drag a **Button** element into the layout
170
+ 4. Click the button element to configure it:
171
+ - **Label:** e.g. `Generate with AI`
172
+ - **Action:** select one of the `ai_*` actions
173
+ - Configure the action fields (source field, target field, prompt, etc.)
174
+ 5. Save the view
175
+ 6. Open the Edit view on a row, fill in the source fields, and click the button
176
+
177
+ The AI-generated value appears in the target field immediately. You can edit it before saving.
178
+
179
+ ---
180
+
181
+ ### As a row action in a List view
182
+
183
+ Row actions run on an already-saved row and update the database directly.
184
+
185
+ 1. Open a **List** view in the builder
186
+ 2. Add an **Action** column
187
+ 3. Set the action to one of the `ai_*` actions and configure it
188
+ 4. Save the view
189
+ 5. In the list, click the action button on any row — the target field is updated immediately in the database
190
+
191
+ ---
192
+
193
+ ### As a trigger (on insert / on update)
194
+
195
+ Triggers let you run AI actions automatically whenever a row is created or updated — no button required.
196
+
197
+ 1. Go to **Settings → Triggers → Add trigger**
198
+ 2. Set **When:** `Insert` or `Update`
199
+ 3. Set **Table:** your table
200
+ 4. Set **Action:** one of the `ai_*` actions
201
+ 5. Configure the action fields
202
+ 6. Save
203
+
204
+ Now every time a row is inserted (or updated), the AI action runs automatically. Useful for:
205
+ - Auto-summarising a description when a product is added
206
+ - Auto-classifying a support ticket on creation
207
+ - Auto-translating content into a second language field on save
208
+
209
+ ---
210
+
211
+ ## Testing locally
212
+
213
+ Use these steps to test the plugin against a local Saltcorn development server before publishing.
214
+
215
+ ### Prerequisites
216
+
217
+ - Node.js 18+
218
+ - Saltcorn installed globally: `npm install -g @saltcorn/cli`
219
+ - The `saltcorn/copilot` plugin installed and configured with an OpenAI API key
220
+
221
+ ### Step 1 — Clone and install dependencies
222
+
223
+ ```bash
224
+ git clone https://github.com/satwikjambula/saltcorn-ai-fields
225
+ cd saltcorn-ai-fields
226
+ npm install
227
+ ```
228
+
229
+ ### Step 2 — Install the plugin into your local Saltcorn instance
230
+
231
+ ```bash
232
+ saltcorn install-plugin -d /absolute/path/to/saltcorn-ai-fields
233
+ ```
234
+
235
+ > Replace `/absolute/path/to/saltcorn-ai-fields` with the actual path, e.g. `/Users/you/Documents/saltcorn-ai-fields`.
236
+
237
+ This registers the plugin in Saltcorn's SQLite database using `source: local`. Saltcorn loads it directly from the directory — no publish step needed.
238
+
239
+ ### Step 3 — Start the Saltcorn server
240
+
241
+ ```bash
242
+ saltcorn serve
243
+ ```
244
+
245
+ Open [http://localhost:3000](http://localhost:3000) and log in as admin.
246
+
247
+ ### Step 4 — Verify plugin is loaded
248
+
249
+ Go to **Settings → Plugins** — you should see `saltcorn-ai-fields` listed.
250
+
251
+ ### Step 5 — Create a test table
252
+
253
+ Go to **Tables → Add table** → name it `test_ai`.
254
+
255
+ Add these fields:
256
+
257
+ | Field name | Type | Notes |
258
+ |---|---|---|
259
+ | `body` | String | Long text input field |
260
+ | `summary` | String | Will be filled by AI |
261
+ | `category` | String | Will be filled by AI |
262
+ | `translated` | String | Will be filled by AI |
263
+
264
+ ### Step 6 — Test `ai_summarize`
265
+
266
+ 1. Go to **Views → Add view** → pick `test_ai` table → template: **Edit** → name it `Edit test_ai`
267
+ 2. In the builder, drag a **Button** into the layout
268
+ 3. Label: `Summarize`, Action: `ai_summarize`
269
+ - Source field: `body`
270
+ - Target field: `summary`
271
+ - Max words: `30`
272
+ - Style: `One sentence`
273
+ 4. Save the view
274
+ 5. Go to **Views → Edit test_ai** → enter a long paragraph in `body` → click **Summarize**
275
+ 6. The `summary` field should populate with a one-sentence summary
276
+
277
+ ### Step 7 — Test `ai_classify`
278
+
279
+ 1. In the same Edit view, add another button: Label `Classify`, Action: `ai_classify`
280
+ - Source field: `body`
281
+ - Target field: `category`
282
+ - Categories: `Technology,Health,Finance,General`
283
+ 2. Save, open the view, enter text, click **Classify**
284
+ 3. The `category` field should contain one of the four categories
285
+
286
+ ### Step 8 — Test as a trigger
287
+
288
+ 1. Go to **Settings → Triggers → Add trigger**
289
+ 2. When: `Insert`, Table: `test_ai`, Action: `ai_summarize`
290
+ - Source field: `body`, Target field: `summary`, Max words: `20`
291
+ 3. Save
292
+ 4. Insert a new row via the Edit view — the `summary` field should be populated automatically on save
293
+
294
+ ### Troubleshooting
295
+
296
+ | Symptom | Fix |
297
+ |---|---|
298
+ | `No LLM available` error | Install and configure `saltcorn/copilot` with an API key |
299
+ | Button appears but does nothing | Check the browser console for errors; ensure the action is fully configured |
300
+ | Target field not updating in form | Confirm the target field name matches exactly (case-sensitive) |
301
+ | Trigger not firing | Check **Settings → Event log** for errors |
302
+
303
+ ---
304
+
305
+ ## License
306
+
307
+ MIT
package/actions.js ADDED
@@ -0,0 +1,380 @@
1
+ // ─── LLM helper ───────────────────────────────────────────────────────────────
2
+
3
+ // Saltcorn copies the plugin to a temp dir without node_modules, so we cannot
4
+ // use require('@saltcorn/data/...') at module load time. Instead we read the
5
+ // already-loaded modules straight from Node's module cache — they are always
6
+ // present because Saltcorn loaded them before our plugin.
7
+ function _saltcornState() {
8
+ const key = Object.keys(require.cache).find((k) =>
9
+ /\/@saltcorn\/data\/db\/state(\.[cm]?js)?$/.test(k)
10
+ );
11
+ return key ? require.cache[key].exports : null;
12
+ }
13
+
14
+ function getLLM() {
15
+ return _saltcornState()?.getState?.()?.functions?.llm_generate ?? null;
16
+ }
17
+
18
+ async function callLLM(prompt) {
19
+ const llm = getLLM();
20
+ if (!llm)
21
+ throw new Error(
22
+ "No LLM available. Install the saltcorn/copilot plugin and configure an API key."
23
+ );
24
+ const result = await llm.run(prompt, {});
25
+ return typeof result === "string" ? result.trim() : String(result ?? "").trim();
26
+ }
27
+
28
+ // interpolate {{field_name}} placeholders with row values
29
+ function interpolate(template, row) {
30
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) =>
31
+ row[key] !== undefined && row[key] !== null ? String(row[key]) : ""
32
+ );
33
+ }
34
+
35
+ // persist to DB if row already saved, always return set_fields for form context
36
+ async function applyResult(table, row, user, target_field, value) {
37
+ if (row && row.id) {
38
+ await table.updateRow({ [target_field]: value }, row.id, user);
39
+ }
40
+ return { set_fields: { [target_field]: value } };
41
+ }
42
+
43
+ function fieldOptions(fields) {
44
+ return fields.filter((f) => !f.primary_key).map((f) => f.name);
45
+ }
46
+
47
+ // ─── actions ──────────────────────────────────────────────────────────────────
48
+
49
+ const ai_fill_field = {
50
+ name: "ai_fill_field",
51
+ description:
52
+ "Fill a field using a custom AI prompt. Reference any row field with {{field_name}}.",
53
+ configFields: async ({ table }) => {
54
+ const fields = table ? table.getFields() : [];
55
+ return [
56
+ {
57
+ name: "target_field",
58
+ label: "Target field",
59
+ sublabel: "Field whose value will be set by the AI response",
60
+ type: "String",
61
+ required: true,
62
+ attributes: { options: fieldOptions(fields) },
63
+ },
64
+ {
65
+ name: "prompt_template",
66
+ label: "Prompt",
67
+ sublabel:
68
+ "Describe what to generate. Use {{field_name}} to inject row values. " +
69
+ "Example: Write a product description for {{name}} priced at {{price}}.",
70
+ input_type: "textarea",
71
+ required: true,
72
+ },
73
+ {
74
+ name: "instruction",
75
+ label: "Response instruction",
76
+ sublabel:
77
+ "Optional extra instruction appended to the prompt, e.g. 'Be concise. Under 50 words.'",
78
+ type: "String",
79
+ required: false,
80
+ },
81
+ ];
82
+ },
83
+ run: async ({ row, table, user, configuration: { target_field, prompt_template, instruction } }) => {
84
+ if (!target_field || !prompt_template)
85
+ return { error: "target_field and prompt_template are required" };
86
+
87
+ const prompt =
88
+ interpolate(prompt_template, row || {}) +
89
+ (instruction ? `\n\n${instruction}` : "") +
90
+ "\n\nRespond with only the generated value, nothing else.";
91
+
92
+ const value = await callLLM(prompt);
93
+ return applyResult(table, row, user, target_field, value);
94
+ },
95
+ };
96
+
97
+ // ─────────────────────────────────────────────────────────────────────────────
98
+
99
+ const ai_summarize = {
100
+ name: "ai_summarize",
101
+ description: "Summarize a text field and write the result to another field.",
102
+ configFields: async ({ table }) => {
103
+ const fields = table ? table.getFields() : [];
104
+ const textFields = fields.filter(
105
+ (f) => !f.primary_key && (f.type?.name === "String" || f.type?.name === "Text")
106
+ );
107
+ return [
108
+ {
109
+ name: "source_field",
110
+ label: "Source field",
111
+ sublabel: "Text field to summarize",
112
+ type: "String",
113
+ required: true,
114
+ attributes: { options: textFields.map((f) => f.name) },
115
+ },
116
+ {
117
+ name: "target_field",
118
+ label: "Target field",
119
+ sublabel: "Field where the summary will be written",
120
+ type: "String",
121
+ required: true,
122
+ attributes: { options: fieldOptions(fields) },
123
+ },
124
+ {
125
+ name: "max_words",
126
+ label: "Max words",
127
+ sublabel: "Approximate maximum length of the summary",
128
+ type: "Integer",
129
+ required: false,
130
+ attributes: { default: 50 },
131
+ },
132
+ {
133
+ name: "style",
134
+ label: "Summary style",
135
+ type: "String",
136
+ required: false,
137
+ attributes: {
138
+ options: ["Concise", "Bullet points", "One sentence", "Formal", "Casual"],
139
+ },
140
+ },
141
+ ];
142
+ },
143
+ run: async ({ row, table, user, configuration: { source_field, target_field, max_words, style } }) => {
144
+ if (!source_field || !target_field)
145
+ return { error: "source_field and target_field are required" };
146
+
147
+ const sourceText = row?.[source_field];
148
+ if (!sourceText) return { notify: "Source field is empty — nothing to summarize" };
149
+
150
+ const wordLimit = max_words || 50;
151
+ const styleHint = style ? ` Style: ${style}.` : "";
152
+ const prompt =
153
+ `Summarize the following text in ${wordLimit} words or fewer.${styleHint} ` +
154
+ `Respond with only the summary, nothing else.\n\n${sourceText}`;
155
+
156
+ const value = await callLLM(prompt);
157
+ return applyResult(table, row, user, target_field, value);
158
+ },
159
+ };
160
+
161
+ // ─────────────────────────────────────────────────────────────────────────────
162
+
163
+ const ai_classify = {
164
+ name: "ai_classify",
165
+ description:
166
+ "Classify a text field into one of your defined categories and write the result to another field.",
167
+ configFields: async ({ table }) => {
168
+ const fields = table ? table.getFields() : [];
169
+ const textFields = fields.filter(
170
+ (f) => !f.primary_key && (f.type?.name === "String" || f.type?.name === "Text")
171
+ );
172
+ return [
173
+ {
174
+ name: "source_field",
175
+ label: "Source field",
176
+ sublabel: "Text field to classify",
177
+ type: "String",
178
+ required: true,
179
+ attributes: { options: textFields.map((f) => f.name) },
180
+ },
181
+ {
182
+ name: "target_field",
183
+ label: "Target field",
184
+ sublabel: "Field where the category will be written",
185
+ type: "String",
186
+ required: true,
187
+ attributes: { options: fieldOptions(fields) },
188
+ },
189
+ {
190
+ name: "categories",
191
+ label: "Categories",
192
+ sublabel: "Comma-separated list of valid categories, e.g. Bug,Feature,Question,Other",
193
+ type: "String",
194
+ required: true,
195
+ },
196
+ {
197
+ name: "context_prompt",
198
+ label: "Context hint",
199
+ sublabel:
200
+ "Optional: describe what the text is about to improve accuracy, e.g. 'This is a customer support ticket.'",
201
+ type: "String",
202
+ required: false,
203
+ },
204
+ ];
205
+ },
206
+ run: async ({ row, table, user, configuration: { source_field, target_field, categories, context_prompt } }) => {
207
+ if (!source_field || !target_field || !categories)
208
+ return { error: "source_field, target_field, and categories are required" };
209
+
210
+ const sourceText = row?.[source_field];
211
+ if (!sourceText) return { notify: "Source field is empty — nothing to classify" };
212
+
213
+ const context = context_prompt ? `${context_prompt}\n\n` : "";
214
+ const prompt =
215
+ `${context}Classify the following text into exactly one of these categories: ${categories}.\n\n` +
216
+ `Text: ${sourceText}\n\n` +
217
+ `Respond with only the category name, exactly as written above. No explanation.`;
218
+
219
+ const value = await callLLM(prompt);
220
+
221
+ // validate response is one of the configured categories
222
+ const validCats = categories.split(",").map((c) => c.trim());
223
+ const matched = validCats.find(
224
+ (c) => c.toLowerCase() === value.toLowerCase()
225
+ );
226
+ const finalValue = matched || value;
227
+
228
+ return applyResult(table, row, user, target_field, finalValue);
229
+ },
230
+ };
231
+
232
+ // ─────────────────────────────────────────────────────────────────────────────
233
+
234
+ const ai_translate = {
235
+ name: "ai_translate",
236
+ description: "Translate a text field to another language and write the result to a field.",
237
+ configFields: async ({ table }) => {
238
+ const fields = table ? table.getFields() : [];
239
+ const textFields = fields.filter(
240
+ (f) => !f.primary_key && (f.type?.name === "String" || f.type?.name === "Text")
241
+ );
242
+ return [
243
+ {
244
+ name: "source_field",
245
+ label: "Source field",
246
+ sublabel: "Text field to translate",
247
+ type: "String",
248
+ required: true,
249
+ attributes: { options: textFields.map((f) => f.name) },
250
+ },
251
+ {
252
+ name: "target_field",
253
+ label: "Target field",
254
+ sublabel: "Field where the translation will be written",
255
+ type: "String",
256
+ required: true,
257
+ attributes: { options: fieldOptions(fields) },
258
+ },
259
+ {
260
+ name: "target_language",
261
+ label: "Target language",
262
+ sublabel: "Language to translate into",
263
+ type: "String",
264
+ required: true,
265
+ attributes: {
266
+ options: [
267
+ "Spanish", "French", "German", "Italian", "Portuguese",
268
+ "Dutch", "Russian", "Japanese", "Chinese (Simplified)",
269
+ "Chinese (Traditional)", "Korean", "Arabic", "Hindi",
270
+ "Turkish", "Polish", "Swedish", "Norwegian", "Danish",
271
+ ],
272
+ },
273
+ },
274
+ {
275
+ name: "formality",
276
+ label: "Formality",
277
+ type: "String",
278
+ required: false,
279
+ attributes: { options: ["Default", "Formal", "Informal"] },
280
+ },
281
+ ];
282
+ },
283
+ run: async ({ row, table, user, configuration: { source_field, target_field, target_language, formality } }) => {
284
+ if (!source_field || !target_field || !target_language)
285
+ return { error: "source_field, target_field, and target_language are required" };
286
+
287
+ const sourceText = row?.[source_field];
288
+ if (!sourceText) return { notify: "Source field is empty — nothing to translate" };
289
+
290
+ const formalityHint =
291
+ formality && formality !== "Default" ? ` Use a ${formality.toLowerCase()} tone.` : "";
292
+ const prompt =
293
+ `Translate the following text to ${target_language}.${formalityHint} ` +
294
+ `Respond with only the translation, nothing else.\n\n${sourceText}`;
295
+
296
+ const value = await callLLM(prompt);
297
+ return applyResult(table, row, user, target_field, value);
298
+ },
299
+ };
300
+
301
+ // ─────────────────────────────────────────────────────────────────────────────
302
+
303
+ const ai_extract = {
304
+ name: "ai_extract",
305
+ description:
306
+ "Extract specific information from a text field (dates, names, numbers, etc.) and write it to another field.",
307
+ configFields: async ({ table }) => {
308
+ const fields = table ? table.getFields() : [];
309
+ const textFields = fields.filter(
310
+ (f) => !f.primary_key && (f.type?.name === "String" || f.type?.name === "Text")
311
+ );
312
+ return [
313
+ {
314
+ name: "source_field",
315
+ label: "Source field",
316
+ sublabel: "Text field to extract information from",
317
+ type: "String",
318
+ required: true,
319
+ attributes: { options: textFields.map((f) => f.name) },
320
+ },
321
+ {
322
+ name: "target_field",
323
+ label: "Target field",
324
+ sublabel: "Field where the extracted value will be written",
325
+ type: "String",
326
+ required: true,
327
+ attributes: { options: fieldOptions(fields) },
328
+ },
329
+ {
330
+ name: "extract_prompt",
331
+ label: "What to extract",
332
+ sublabel:
333
+ "Describe what to extract, e.g. 'the total price', 'the customer email address', 'the delivery date in YYYY-MM-DD format'",
334
+ type: "String",
335
+ required: true,
336
+ },
337
+ {
338
+ name: "fallback_value",
339
+ label: "Fallback value",
340
+ sublabel: "Value to write if nothing is found (leave blank to leave field unchanged)",
341
+ type: "String",
342
+ required: false,
343
+ },
344
+ ];
345
+ },
346
+ run: async ({ row, table, user, configuration: { source_field, target_field, extract_prompt, fallback_value } }) => {
347
+ if (!source_field || !target_field || !extract_prompt)
348
+ return { error: "source_field, target_field, and extract_prompt are required" };
349
+
350
+ const sourceText = row?.[source_field];
351
+ if (!sourceText) return { notify: "Source field is empty — nothing to extract from" };
352
+
353
+ const prompt =
354
+ `From the following text, extract ${extract_prompt}.\n\n` +
355
+ `Text: ${sourceText}\n\n` +
356
+ `Respond with only the extracted value, nothing else. ` +
357
+ `If the information is not present, respond with exactly: NOT_FOUND`;
358
+
359
+ const raw = await callLLM(prompt);
360
+
361
+ if (raw === "NOT_FOUND") {
362
+ if (!fallback_value) return { notify: "Could not extract the requested information" };
363
+ return applyResult(table, row, user, target_field, fallback_value);
364
+ }
365
+
366
+ return applyResult(table, row, user, target_field, raw);
367
+ },
368
+ };
369
+
370
+ // ─────────────────────────────────────────────────────────────────────────────
371
+
372
+ // Saltcorn reads plugin.actions directly as an object (keyed by action name).
373
+ // The function form only applies when the plugin has a configuration_workflow.
374
+ module.exports = {
375
+ ai_fill_field,
376
+ ai_summarize,
377
+ ai_classify,
378
+ ai_translate,
379
+ ai_extract,
380
+ };
package/index.js ADDED
@@ -0,0 +1,7 @@
1
+ const actions = require("./actions");
2
+
3
+ module.exports = {
4
+ sc_plugin_api_version: 1,
5
+ plugin_name: "saltcorn-ai-fields",
6
+ actions,
7
+ };
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "saltcorn-ai-fields",
3
+ "version": "0.1.1",
4
+ "description": "AI-powered field actions for Saltcorn — fill, summarize, classify, translate, and extract using LLMs",
5
+ "main": "index.js",
6
+ "keywords": [
7
+ "saltcorn",
8
+ "plugin",
9
+ "ai",
10
+ "llm",
11
+ "openai",
12
+ "fields",
13
+ "actions"
14
+ ],
15
+ "license": "MIT",
16
+ "dependencies": {
17
+ "@saltcorn/data": ">=0.5.0"
18
+ }
19
+ }
Binary file
Binary file