@saltcorn/copilot 0.4.2 → 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 +86 -93
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/copilot",
3
- "version": "0.4.2",
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
@@ -107,6 +107,13 @@ const configuration_workflow = (req) =>
107
107
  calcOptions: ["table_name", list_view_opts],
108
108
  },
109
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
+ },
110
117
  ],
111
118
  }),
112
119
  ],
@@ -441,71 +448,19 @@ const getCompletionArguments = async (config) => {
441
448
 
442
449
  const table = Table.findOne({ name: tableCfg.table_name });
443
450
 
444
- table.fields
445
- .filter((f) => !f.primary_key)
446
- .forEach((field) => {
447
- properties[field.name] = {
448
- description: field.label + " " + field.description || "",
449
- };
450
- if (field.type.name === "String") {
451
- properties[field.name].anyOf = [
452
- { type: "string", description: "Match this value exactly" },
453
- {
454
- type: "object",
455
- properties: {
456
- ilike: {
457
- type: "string",
458
- description: "Case insensitive substring match.",
459
- },
460
- in: {
461
- type: "array",
462
- description: "Match any one of these values",
463
- items: {
464
- type: "string",
465
- },
466
- },
467
- },
468
- },
469
- ];
470
- }
471
- if (field.type.name === "Integer" || field.type.name === "Float") {
472
- const basetype = field.type.name === "Integer" ? "integer" : "number";
473
- properties[field.name].anyOf = [
474
- { type: basetype, description: "Match this value exactly" },
475
- {
476
- type: "object",
477
- properties: {
478
- gt: {
479
- type: basetype,
480
- description: "Greater than this value",
481
- },
482
- lt: {
483
- type: basetype,
484
- description: "Less than this value",
485
- },
486
- in: {
487
- type: "array",
488
- description: "Match any one of these values",
489
- items: {
490
- type: basetype,
491
- },
492
- },
493
- },
494
- },
495
- ];
496
- } else Object.assign(fieldProperties(field), properties[field.name]);
497
- });
498
451
  properties.sql_id_query = {
499
452
  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`,
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`,
501
458
  };
502
459
  tools.push({
503
460
  type: "function",
504
461
  function: {
505
462
  name: "Query" + table.name,
506
- description: `Query the ${table.name} table. ${
507
- table.description || ""
508
- }`,
463
+ description: `Query the ${table.name} table and show the results to the user in a grid format`,
509
464
  parameters: {
510
465
  type: "object",
511
466
  //required: ["action_javascript_code", "action_name"],
@@ -574,7 +529,13 @@ ${t.fields
574
529
  .join(",\n")}
575
530
  )`
576
531
  )
577
- .join(";\n\n");
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
+ `;
578
539
  //console.log("sysprompt", systemPrompt);
579
540
 
580
541
  if (tools.length === 0) tools = undefined;
@@ -611,7 +572,24 @@ const interact = async (table_id, viewname, config, body, { req, res }) => {
611
572
  };
612
573
 
613
574
  const renderQueryInteraction = async (table, result, config, req) => {
614
- 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
+ );
615
593
 
616
594
  const tableCfg = config.tables.find((t) => t.table_name === table.name);
617
595
  let viewRes = "";
@@ -728,47 +706,62 @@ const process_interaction = async (
728
706
  const table = Table.findOne({
729
707
  name: tool_call.function.name.replace("Query", ""),
730
708
  });
709
+ const tableCfg = config.tables.find((t) => t.table_name === table.name);
731
710
  const query = JSON.parse(tool_call.function.arguments);
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
711
 
747
- const { rows } = await client.query(query.sql_id_query);
748
- await client.query(`ROLLBACK;`);
712
+ const is_sqlite = db.isSQLite;
749
713
 
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
- }
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()}";`
757
719
  );
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
- )
720
+ await client.query(
721
+ `SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY;`
766
722
  );
767
- } else
768
- result = await table.getRows(query, {
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, {
769
741
  forUser: req.user,
770
742
  forPublic: !req.user,
771
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
+
772
765
  await addToContext(run, {
773
766
  interactions: [
774
767
  {