@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/user-copilot.js +154 -70
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/copilot",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
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({
@@ -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
- table.fields
429
- .filter((f) => !f.primary_key)
430
- .forEach((field) => {
431
- properties[field.name] = {
432
- description: field.label + " " + field.description || "",
433
- };
434
- if (field.type.name === "String") {
435
- properties[field.name].anyOf = [
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.length === 0) return "No rows found";
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 (typeof result === "object" && Object.keys(result || {}).length) {
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: result ? JSON.stringify(result) : "Action run",
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
- const result = await table.getRows(query, {
678
- forUser: req.user,
679
- forPublic: !req.user,
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
  {