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 +307 -0
- package/actions.js +380 -0
- package/index.js +7 -0
- package/package.json +19 -0
- package/saltcorn-ai-fields-0.1.0.tgz +0 -0
- package/sessions.sqlite +0 -0
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
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
|
package/sessions.sqlite
ADDED
|
Binary file
|