@saltcorn/copilot 0.4.0 → 0.4.2

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/user-copilot.js +114 -22
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/copilot",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "AI assistant for building Saltcorn applications",
5
5
  "main": "index.js",
6
6
  "dependencies": {
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.find({
72
- table_id: t.id,
73
- viewtemplate: "List",
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({
@@ -193,11 +195,26 @@ const run = async (table_id, viewname, config, state, { res, req }) => {
193
195
  );
194
196
  if (action) {
195
197
  const row = JSON.parse(tool_call.function.arguments);
198
+ if (Object.keys(row || {}).length)
199
+ interactMarkups.push(
200
+ wrapSegment(
201
+ wrapCard(
202
+ action.trigger_name,
203
+ pre(JSON.stringify(row, null, 2))
204
+ ),
205
+ "Copilot"
206
+ )
207
+ );
208
+ } else if (tool_call.function.name.startsWith("Query")) {
209
+ const query = JSON.parse(tool_call.function.arguments);
210
+ const queryText = query.sql_id_query
211
+ ? query.sql_id_query
212
+ : JSON.stringify(query, null, 2);
196
213
  interactMarkups.push(
197
214
  wrapSegment(
198
215
  wrapCard(
199
- action.trigger_name,
200
- pre(JSON.stringify(row, null, 2))
216
+ "Query " + tool_call.function.name.replace("Query", ""),
217
+ pre(queryText)
201
218
  ),
202
219
  "Copilot"
203
220
  )
@@ -233,11 +250,7 @@ const run = async (table_id, viewname, config, state, { res, req }) => {
233
250
  wrapSegment(
234
251
  wrapCard(
235
252
  interact.name,
236
- pre(
237
- interact.name.startsWith("Query")
238
- ? JSON.stringify(JSON.parse(interact.content), null, 2)
239
- : JSON.stringify(interact.content, null, 2)
240
- )
253
+ pre(JSON.stringify(JSON.parse(interact.content), null, 2))
241
254
  ),
242
255
  "Copilot"
243
256
  )
@@ -482,7 +495,10 @@ const getCompletionArguments = async (config) => {
482
495
  ];
483
496
  } else Object.assign(fieldProperties(field), properties[field.name]);
484
497
  });
485
-
498
+ properties.sql_id_query = {
499
+ type: "string",
500
+ description: `An SQL query for this table's primary keys (${table.pk_name}). This must select only the primary keys, 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. If you use sql_id_query, use it alone, do not combine with other field queries. Any other fields you want to filter on must be accessed in the WHERE clause`,
501
+ };
486
502
  tools.push({
487
503
  type: "function",
488
504
  function: {
@@ -527,9 +543,39 @@ const getCompletionArguments = async (config) => {
527
543
  },
528
544
  });
529
545
  }
546
+ const tables = (await Table.find({})).filter(
547
+ (t) => !t.external && !t.provider_name
548
+ );
549
+ const schemaPrefix = db.getTenantSchemaPrefix();
530
550
  const systemPrompt =
531
551
  "You are helping users retrieve information and perform actions on a relational database" +
532
- config.sys_prompt;
552
+ config.sys_prompt +
553
+ `
554
+ If you are generating SQL, Your database the following tables in PostgreSQL:
555
+
556
+ ` +
557
+ tables
558
+ .map(
559
+ (t) => `CREATE TABLE "${t.name}" (${
560
+ t.description
561
+ ? `
562
+ /* ${t.description} */`
563
+ : ""
564
+ }
565
+ ${t.fields
566
+ .map(
567
+ (f) =>
568
+ ` "${f.name}" ${
569
+ f.primary_key && f.type?.name === "Integer"
570
+ ? "SERIAL PRIMARY KEY"
571
+ : f.sql_type.replace(schemaPrefix, "")
572
+ }`
573
+ )
574
+ .join(",\n")}
575
+ )`
576
+ )
577
+ .join(";\n\n");
578
+ //console.log("sysprompt", systemPrompt);
533
579
 
534
580
  if (tools.length === 0) tools = undefined;
535
581
  return { tools, systemPrompt };
@@ -636,12 +682,13 @@ const process_interaction = async (
636
682
  if (action) {
637
683
  const trigger = Trigger.findOne({ name: action.trigger_name });
638
684
  const row = JSON.parse(tool_call.function.arguments);
639
- responses.push(
640
- wrapSegment(
641
- wrapCard(action.trigger_name, pre(JSON.stringify(row, null, 2))),
642
- "Copilot"
643
- )
644
- );
685
+ if (Object.keys(row || {}).length)
686
+ responses.push(
687
+ wrapSegment(
688
+ wrapCard(action.trigger_name, pre(JSON.stringify(row, null, 2))),
689
+ "Copilot"
690
+ )
691
+ );
645
692
  const result = await trigger.runWithoutRow({ user: req.user, row });
646
693
  console.log("ran trigger with result", {
647
694
  name: trigger.name,
@@ -649,7 +696,10 @@ const process_interaction = async (
649
696
  result,
650
697
  });
651
698
 
652
- if (typeof result === "object" && Object.keys(result || {}).length) {
699
+ if (
700
+ (typeof result === "object" && Object.keys(result || {}).length) ||
701
+ typeof result === "string"
702
+ ) {
653
703
  responses.push(
654
704
  wrapSegment(
655
705
  wrapCard(
@@ -667,7 +717,10 @@ const process_interaction = async (
667
717
  role: "tool",
668
718
  tool_call_id: tool_call.id,
669
719
  name: tool_call.function.name,
670
- content: result || "Action run",
720
+ content:
721
+ result && typeof result !== "string"
722
+ ? JSON.stringify(result)
723
+ : result || "Action run",
671
724
  },
672
725
  ],
673
726
  });
@@ -676,7 +729,46 @@ const process_interaction = async (
676
729
  name: tool_call.function.name.replace("Query", ""),
677
730
  });
678
731
  const query = JSON.parse(tool_call.function.arguments);
679
- const result = await table.getRows(query);
732
+ let result;
733
+ if (query.sql_id_query) {
734
+ const is_sqlite = db.isSQLite;
735
+
736
+ const client = is_sqlite ? db : await db.getClient();
737
+ await client.query(`BEGIN;`);
738
+ if (!is_sqlite) {
739
+ await client.query(
740
+ `SET LOCAL search_path TO "${db.getTenantSchema()}";`
741
+ );
742
+ await client.query(
743
+ `SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY;`
744
+ );
745
+ }
746
+
747
+ const { rows } = await client.query(query.sql_id_query);
748
+ await client.query(`ROLLBACK;`);
749
+
750
+ if (!is_sqlite) client.release(true);
751
+ result = await table.getRows(
752
+ { [table.pk_name]: { in: rows.map((r) => r[table.pk_name]) } },
753
+ {
754
+ forUser: req.user,
755
+ forPublic: !req.user,
756
+ }
757
+ );
758
+ responses.push(
759
+ wrapSegment(
760
+ wrapCard(
761
+ "Query " + tool_call.function.name.replace("Query", ""),
762
+ pre(query.sql_id_query)
763
+ ),
764
+ "Copilot"
765
+ )
766
+ );
767
+ } else
768
+ result = await table.getRows(query, {
769
+ forUser: req.user,
770
+ forPublic: !req.user,
771
+ });
680
772
  await addToContext(run, {
681
773
  interactions: [
682
774
  {