@saltcorn/copilot 0.4.1 → 0.4.3
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/package.json +1 -1
- package/user-copilot.js +154 -70
package/package.json
CHANGED
package/user-copilot.js
CHANGED
|
@@ -68,10 +68,12 @@ const configuration_workflow = (req) =>
|
|
|
68
68
|
viewtemplate: "Show",
|
|
69
69
|
});
|
|
70
70
|
show_view_opts[t.name] = views.map((v) => v.name);
|
|
71
|
-
const lviews = await View.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
const lviews = await View.find_table_views_where(
|
|
72
|
+
t.id,
|
|
73
|
+
({ state_fields, viewrow }) =>
|
|
74
|
+
viewrow.viewtemplate !== "Edit" &&
|
|
75
|
+
state_fields.every((sf) => !sf.required)
|
|
76
|
+
);
|
|
75
77
|
list_view_opts[t.name] = lviews.map((v) => v.name);
|
|
76
78
|
}
|
|
77
79
|
return new Form({
|
|
@@ -105,6 +107,13 @@ const configuration_workflow = (req) =>
|
|
|
105
107
|
calcOptions: ["table_name", list_view_opts],
|
|
106
108
|
},
|
|
107
109
|
},
|
|
110
|
+
{
|
|
111
|
+
name: "exclude_fields",
|
|
112
|
+
label: "Exclude fields",
|
|
113
|
+
sublabel:
|
|
114
|
+
"Exclude fields from the chat context. Comma-separated list.",
|
|
115
|
+
type: "String",
|
|
116
|
+
},
|
|
108
117
|
],
|
|
109
118
|
}),
|
|
110
119
|
],
|
|
@@ -203,6 +212,20 @@ const run = async (table_id, viewname, config, state, { res, req }) => {
|
|
|
203
212
|
"Copilot"
|
|
204
213
|
)
|
|
205
214
|
);
|
|
215
|
+
} else if (tool_call.function.name.startsWith("Query")) {
|
|
216
|
+
const query = JSON.parse(tool_call.function.arguments);
|
|
217
|
+
const queryText = query.sql_id_query
|
|
218
|
+
? query.sql_id_query
|
|
219
|
+
: JSON.stringify(query, null, 2);
|
|
220
|
+
interactMarkups.push(
|
|
221
|
+
wrapSegment(
|
|
222
|
+
wrapCard(
|
|
223
|
+
"Query " + tool_call.function.name.replace("Query", ""),
|
|
224
|
+
pre(queryText)
|
|
225
|
+
),
|
|
226
|
+
"Copilot"
|
|
227
|
+
)
|
|
228
|
+
);
|
|
206
229
|
}
|
|
207
230
|
}
|
|
208
231
|
} else
|
|
@@ -425,68 +448,19 @@ const getCompletionArguments = async (config) => {
|
|
|
425
448
|
|
|
426
449
|
const table = Table.findOne({ name: tableCfg.table_name });
|
|
427
450
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
.
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
{ type: "string", description: "Match this value exactly" },
|
|
437
|
-
{
|
|
438
|
-
type: "object",
|
|
439
|
-
properties: {
|
|
440
|
-
ilike: {
|
|
441
|
-
type: "string",
|
|
442
|
-
description: "Case insensitive substring match.",
|
|
443
|
-
},
|
|
444
|
-
in: {
|
|
445
|
-
type: "array",
|
|
446
|
-
description: "Match any one of these values",
|
|
447
|
-
items: {
|
|
448
|
-
type: "string",
|
|
449
|
-
},
|
|
450
|
-
},
|
|
451
|
-
},
|
|
452
|
-
},
|
|
453
|
-
];
|
|
454
|
-
}
|
|
455
|
-
if (field.type.name === "Integer" || field.type.name === "Float") {
|
|
456
|
-
const basetype = field.type.name === "Integer" ? "integer" : "number";
|
|
457
|
-
properties[field.name].anyOf = [
|
|
458
|
-
{ type: basetype, description: "Match this value exactly" },
|
|
459
|
-
{
|
|
460
|
-
type: "object",
|
|
461
|
-
properties: {
|
|
462
|
-
gt: {
|
|
463
|
-
type: basetype,
|
|
464
|
-
description: "Greater than this value",
|
|
465
|
-
},
|
|
466
|
-
lt: {
|
|
467
|
-
type: basetype,
|
|
468
|
-
description: "Less than this value",
|
|
469
|
-
},
|
|
470
|
-
in: {
|
|
471
|
-
type: "array",
|
|
472
|
-
description: "Match any one of these values",
|
|
473
|
-
items: {
|
|
474
|
-
type: basetype,
|
|
475
|
-
},
|
|
476
|
-
},
|
|
477
|
-
},
|
|
478
|
-
},
|
|
479
|
-
];
|
|
480
|
-
} else Object.assign(fieldProperties(field), properties[field.name]);
|
|
481
|
-
});
|
|
482
|
-
|
|
451
|
+
properties.sql_id_query = {
|
|
452
|
+
type: "string",
|
|
453
|
+
description: `An SQL query for this table's primary keys (${table.pk_name}). This must select only the primary keys (even if the user wants a count), for example SELECT ${table.name[0]}."${table.pk_name}" from "${table.name}" ${table.name[0]} JOIN ... where... Use this to join other tables in the database.`,
|
|
454
|
+
};
|
|
455
|
+
properties.is_count = {
|
|
456
|
+
type: "boolean",
|
|
457
|
+
description: `Is the only desired output a count? Make this true if the user wants a count of rows`,
|
|
458
|
+
};
|
|
483
459
|
tools.push({
|
|
484
460
|
type: "function",
|
|
485
461
|
function: {
|
|
486
462
|
name: "Query" + table.name,
|
|
487
|
-
description: `Query the ${table.name} table
|
|
488
|
-
table.description || ""
|
|
489
|
-
}`,
|
|
463
|
+
description: `Query the ${table.name} table and show the results to the user in a grid format`,
|
|
490
464
|
parameters: {
|
|
491
465
|
type: "object",
|
|
492
466
|
//required: ["action_javascript_code", "action_name"],
|
|
@@ -524,9 +498,45 @@ const getCompletionArguments = async (config) => {
|
|
|
524
498
|
},
|
|
525
499
|
});
|
|
526
500
|
}
|
|
501
|
+
const tables = (await Table.find({})).filter(
|
|
502
|
+
(t) => !t.external && !t.provider_name
|
|
503
|
+
);
|
|
504
|
+
const schemaPrefix = db.getTenantSchemaPrefix();
|
|
527
505
|
const systemPrompt =
|
|
528
506
|
"You are helping users retrieve information and perform actions on a relational database" +
|
|
529
|
-
config.sys_prompt
|
|
507
|
+
config.sys_prompt +
|
|
508
|
+
`
|
|
509
|
+
If you are generating SQL, Your database the following tables in PostgreSQL:
|
|
510
|
+
|
|
511
|
+
` +
|
|
512
|
+
tables
|
|
513
|
+
.map(
|
|
514
|
+
(t) => `CREATE TABLE "${t.name}" (${
|
|
515
|
+
t.description
|
|
516
|
+
? `
|
|
517
|
+
/* ${t.description} */`
|
|
518
|
+
: ""
|
|
519
|
+
}
|
|
520
|
+
${t.fields
|
|
521
|
+
.map(
|
|
522
|
+
(f) =>
|
|
523
|
+
` "${f.name}" ${
|
|
524
|
+
f.primary_key && f.type?.name === "Integer"
|
|
525
|
+
? "SERIAL PRIMARY KEY"
|
|
526
|
+
: f.sql_type.replace(schemaPrefix, "")
|
|
527
|
+
}`
|
|
528
|
+
)
|
|
529
|
+
.join(",\n")}
|
|
530
|
+
)`
|
|
531
|
+
)
|
|
532
|
+
.join(";\n\n") +
|
|
533
|
+
`
|
|
534
|
+
|
|
535
|
+
If the user asks to you show rows from a table, just run the query tool for that table. This will display the result to the user.
|
|
536
|
+
You should not render the results again, that will cause them to be displayed twice. Only if the user asks for a summary
|
|
537
|
+
or a calculation based on the query results should you show that requested summary or calculation.
|
|
538
|
+
`;
|
|
539
|
+
//console.log("sysprompt", systemPrompt);
|
|
530
540
|
|
|
531
541
|
if (tools.length === 0) tools = undefined;
|
|
532
542
|
return { tools, systemPrompt };
|
|
@@ -562,7 +572,24 @@ const interact = async (table_id, viewname, config, body, { req, res }) => {
|
|
|
562
572
|
};
|
|
563
573
|
|
|
564
574
|
const renderQueryInteraction = async (table, result, config, req) => {
|
|
565
|
-
if (result
|
|
575
|
+
if (typeof result === "number")
|
|
576
|
+
return wrapSegment(
|
|
577
|
+
wrapCard(
|
|
578
|
+
"Query " + table.name,
|
|
579
|
+
//div("Query: ", code(JSON.stringify(query))),
|
|
580
|
+
`${result}`
|
|
581
|
+
),
|
|
582
|
+
"Copilot"
|
|
583
|
+
);
|
|
584
|
+
if (result.length === 0)
|
|
585
|
+
return wrapSegment(
|
|
586
|
+
wrapCard(
|
|
587
|
+
"Query " + table.name,
|
|
588
|
+
//div("Query: ", code(JSON.stringify(query))),
|
|
589
|
+
"No rows found"
|
|
590
|
+
),
|
|
591
|
+
"Copilot"
|
|
592
|
+
);
|
|
566
593
|
|
|
567
594
|
const tableCfg = config.tables.find((t) => t.table_name === table.name);
|
|
568
595
|
let viewRes = "";
|
|
@@ -647,7 +674,10 @@ const process_interaction = async (
|
|
|
647
674
|
result,
|
|
648
675
|
});
|
|
649
676
|
|
|
650
|
-
if (
|
|
677
|
+
if (
|
|
678
|
+
(typeof result === "object" && Object.keys(result || {}).length) ||
|
|
679
|
+
typeof result === "string"
|
|
680
|
+
) {
|
|
651
681
|
responses.push(
|
|
652
682
|
wrapSegment(
|
|
653
683
|
wrapCard(
|
|
@@ -665,7 +695,10 @@ const process_interaction = async (
|
|
|
665
695
|
role: "tool",
|
|
666
696
|
tool_call_id: tool_call.id,
|
|
667
697
|
name: tool_call.function.name,
|
|
668
|
-
content:
|
|
698
|
+
content:
|
|
699
|
+
result && typeof result !== "string"
|
|
700
|
+
? JSON.stringify(result)
|
|
701
|
+
: result || "Action run",
|
|
669
702
|
},
|
|
670
703
|
],
|
|
671
704
|
});
|
|
@@ -673,11 +706,62 @@ const process_interaction = async (
|
|
|
673
706
|
const table = Table.findOne({
|
|
674
707
|
name: tool_call.function.name.replace("Query", ""),
|
|
675
708
|
});
|
|
709
|
+
const tableCfg = config.tables.find((t) => t.table_name === table.name);
|
|
676
710
|
const query = JSON.parse(tool_call.function.arguments);
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
711
|
+
|
|
712
|
+
const is_sqlite = db.isSQLite;
|
|
713
|
+
|
|
714
|
+
const client = is_sqlite ? db : await db.getClient();
|
|
715
|
+
await client.query(`BEGIN;`);
|
|
716
|
+
if (!is_sqlite) {
|
|
717
|
+
await client.query(
|
|
718
|
+
`SET LOCAL search_path TO "${db.getTenantSchema()}";`
|
|
719
|
+
);
|
|
720
|
+
await client.query(
|
|
721
|
+
`SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY;`
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const { rows } = await client.query(query.sql_id_query);
|
|
726
|
+
await client.query(`ROLLBACK;`);
|
|
727
|
+
|
|
728
|
+
if (!is_sqlite) client.release(true);
|
|
729
|
+
let result;
|
|
730
|
+
const id_query = {
|
|
731
|
+
[table.pk_name]: { in: rows.map((r) => r[table.pk_name]) },
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
if (query.is_count) {
|
|
735
|
+
const role = req.user?.role_id || 100;
|
|
736
|
+
if (role <= table.min_role_read) {
|
|
737
|
+
result = await table.countRows(id_query);
|
|
738
|
+
} else result = "Not authorized";
|
|
739
|
+
} else {
|
|
740
|
+
result = await table.getRows(id_query, {
|
|
741
|
+
forUser: req.user,
|
|
742
|
+
forPublic: !req.user,
|
|
743
|
+
});
|
|
744
|
+
if (tableCfg.exclude_fields) {
|
|
745
|
+
const fields = tableCfg.exclude_fields
|
|
746
|
+
.split(",")
|
|
747
|
+
.map((s) => s.trim());
|
|
748
|
+
fields.forEach((f) => {
|
|
749
|
+
result.forEach((r) => {
|
|
750
|
+
delete r[f];
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
responses.push(
|
|
756
|
+
wrapSegment(
|
|
757
|
+
wrapCard(
|
|
758
|
+
"Query " + tool_call.function.name.replace("Query", ""),
|
|
759
|
+
pre(query.sql_id_query)
|
|
760
|
+
),
|
|
761
|
+
"Copilot"
|
|
762
|
+
)
|
|
763
|
+
);
|
|
764
|
+
|
|
681
765
|
await addToContext(run, {
|
|
682
766
|
interactions: [
|
|
683
767
|
{
|