@saltcorn/large-language-model 0.8.4 → 0.8.6

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 (3) hide show
  1. package/generate.js +62 -1
  2. package/index.js +161 -3
  3. package/package.json +1 -1
package/generate.js CHANGED
@@ -78,6 +78,23 @@ const getEmbedding = async (config, opts) => {
78
78
  }
79
79
  };
80
80
 
81
+ const getImageGeneration = async (config, opts) => {
82
+ switch (config.backend) {
83
+ case "OpenAI":
84
+ return await getImageGenOpenAICompatible(
85
+ {
86
+ imageEndpoint: "https://api.openai.com/v1/images/generations",
87
+ bearer: opts?.api_key || opts?.bearer || config.api_key,
88
+ model: opts?.model || config.model,
89
+ responses_api: config.responses_api,
90
+ },
91
+ opts
92
+ );
93
+ default:
94
+ throw new Error("Image generation not implemented for this backend");
95
+ }
96
+ };
97
+
81
98
  const getCompletion = async (config, opts) => {
82
99
  switch (config.backend) {
83
100
  case "OpenAI":
@@ -318,6 +335,50 @@ const getCompletionOpenAICompatible = async (
318
335
 
319
336
  const emptyToUndefined = (xs) => (xs.length ? xs : undefined);
320
337
 
338
+ const getImageGenOpenAICompatible = async (
339
+ config,
340
+ {
341
+ prompt,
342
+ model,
343
+ debugResult,
344
+ size,
345
+ quality,
346
+ n,
347
+ output_format,
348
+ response_format,
349
+ }
350
+ ) => {
351
+ const { imageEndpoint, bearer, apiKey, image_model } = config;
352
+ const headers = {
353
+ "Content-Type": "application/json",
354
+ Accept: "application/json",
355
+ };
356
+ if (bearer) headers.Authorization = "Bearer " + bearer;
357
+ if (apiKey) headers["api-key"] = apiKey;
358
+ const body = {
359
+ //prompt: "How are you?",
360
+ model: model || image_model || "gpt-image-1",
361
+ prompt,
362
+ size: size || "1024x1024",
363
+ n: n || 1,
364
+ };
365
+ if (quality) body.quality = quality;
366
+ if (output_format) body.output_format = output_format;
367
+ if (response_format) body.response_format = response_format;
368
+ if (n) body.n = n;
369
+ if (debugResult) console.log("OpenAI image request", imageEndpoint, body);
370
+
371
+ const rawResponse = await fetch(imageEndpoint, {
372
+ method: "POST",
373
+ headers,
374
+ body: JSON.stringify(body),
375
+ });
376
+ const results = await rawResponse.json();
377
+ if (debugResult) console.log("OpenAI image response", results);
378
+ if (results.error) throw new Error(`OpenAI error: ${results.error.message}`);
379
+ return results?.data?.[0];
380
+ };
381
+
321
382
  const getEmbeddingOpenAICompatible = async (
322
383
  config,
323
384
  { prompt, model, debugResult }
@@ -518,4 +579,4 @@ const getEmbeddingGoogleVertex = async (config, opts, oauth2Client) => {
518
579
  return embeddings;
519
580
  };
520
581
 
521
- module.exports = { getCompletion, getEmbedding };
582
+ module.exports = { getCompletion, getEmbedding, getImageGeneration };
package/index.js CHANGED
@@ -1,10 +1,16 @@
1
1
  const Workflow = require("@saltcorn/data/models/workflow");
2
2
  const Form = require("@saltcorn/data/models/form");
3
+ const File = require("@saltcorn/data/models/file");
4
+ const User = require("@saltcorn/data/models/user");
3
5
  const FieldRepeat = require("@saltcorn/data/models/fieldrepeat");
4
6
  const Plugin = require("@saltcorn/data/models/plugin");
5
7
  const { domReady } = require("@saltcorn/markup/tags");
6
8
  const db = require("@saltcorn/data/db");
7
- const { getCompletion, getEmbedding } = require("./generate");
9
+ const {
10
+ getCompletion,
11
+ getEmbedding,
12
+ getImageGeneration,
13
+ } = require("./generate");
8
14
  const { OPENAI_MODELS } = require("./constants.js");
9
15
  const { eval_expression } = require("@saltcorn/data/models/expression");
10
16
  const { interpolate } = require("@saltcorn/data/utils");
@@ -126,7 +132,17 @@ ${domReady(`
126
132
  ],
127
133
  },
128
134
  },
129
- {
135
+ {
136
+ name: "image_model",
137
+ label: "Image model", //gpt-3.5-turbo
138
+ type: "String",
139
+ required: true,
140
+ showIf: { backend: "OpenAI" },
141
+ attributes: {
142
+ options: ["gpt-image-1", "dall-e-2", "dall-e-3"],
143
+ },
144
+ },
145
+ {
130
146
  name: "client_id",
131
147
  label: "Client ID",
132
148
  sublabel: "OAuth2 client ID from your Google Cloud account",
@@ -219,7 +235,7 @@ ${domReady(`
219
235
  showIf: { backend: "Google Vertex AI" },
220
236
  default: "us-central1",
221
237
  },
222
-
238
+
223
239
  {
224
240
  name: "bearer_auth",
225
241
  label: "Bearer Auth",
@@ -318,6 +334,15 @@ const functions = (config) => {
318
334
  description: "Generate text with GPT",
319
335
  arguments: [{ name: "prompt", type: "String" }],
320
336
  },
337
+ llm_image_generate: {
338
+ run: async (prompt, opts) => {
339
+ const result = await getImageGeneration(config, { prompt, ...opts });
340
+ return result;
341
+ },
342
+ isAsync: true,
343
+ description: "Generate image",
344
+ arguments: [{ name: "prompt", type: "String" }],
345
+ },
321
346
  llm_embedding: {
322
347
  run: async (prompt, opts) => {
323
348
  const result = await getEmbedding(config, { prompt, ...opts });
@@ -567,6 +592,139 @@ module.exports = {
567
592
  else await table.updateRow(upd, row[table.pk_name]);
568
593
  },
569
594
  },
595
+ llm_generate_image: {
596
+ description: "Generate image with AI based on a text prompt",
597
+ requireRow: true,
598
+ configFields: async ({ table, mode }) => {
599
+ const roleOptions = (await User.get_roles()).map((r) => ({
600
+ value: r.id,
601
+ label: r.role,
602
+ }));
603
+ const commonFields = [
604
+ {
605
+ label: "Minimum role to access",
606
+ name: "min_role",
607
+ input_type: "select",
608
+ sublabel: "User must have this role or higher access image file",
609
+ options: roleOptions,
610
+ },
611
+ ];
612
+ if (mode === "workflow") {
613
+ return [
614
+ {
615
+ name: "prompt_template",
616
+ label: "Prompt",
617
+ sublabel:
618
+ "Prompt text. Use interpolations {{ }} to access variables in the context",
619
+ type: "String",
620
+ fieldview: "textarea",
621
+ required: true,
622
+ },
623
+ {
624
+ name: "answer_field",
625
+ label: "Answer variable",
626
+ sublabel:
627
+ "Set the generated image filename to this context variable",
628
+ type: "String",
629
+ required: true,
630
+ },
631
+ {
632
+ name: "model",
633
+ label: "Model",
634
+ sublabel: "Override default model name",
635
+ type: "String",
636
+ },
637
+ ...commonFields,
638
+ ];
639
+ } else if (table) {
640
+ const textFields = table.fields
641
+ .filter((f) => f.type?.sql_name === "text")
642
+ .map((f) => f.name);
643
+ const fileFields = table.fields
644
+ .filter((f) => f.type === "File")
645
+ .map((f) => f.name);
646
+
647
+ return [
648
+ {
649
+ name: "prompt_field",
650
+ label: "Prompt field",
651
+ sublabel: "Field with the text of the prompt",
652
+ type: "String",
653
+ required: true,
654
+ attributes: { options: [...textFields, "Formula"] },
655
+ },
656
+ {
657
+ name: "prompt_formula",
658
+ label: "Prompt formula",
659
+ type: "String",
660
+ showIf: { prompt_field: "Formula" },
661
+ },
662
+ {
663
+ name: "answer_field",
664
+ label: "Answer field",
665
+ sublabel: "Output field will be set to the generated image file",
666
+ type: "String",
667
+ required: true,
668
+ attributes: { options: fileFields },
669
+ },
670
+ ...commonFields,
671
+ ];
672
+ }
673
+ },
674
+ run: async ({
675
+ row,
676
+ table,
677
+ user,
678
+ mode,
679
+ configuration: {
680
+ prompt_field,
681
+ prompt_formula,
682
+ prompt_template,
683
+ answer_field,
684
+ min_role,
685
+ model,
686
+ },
687
+ }) => {
688
+ let prompt;
689
+ if (mode === "workflow")
690
+ prompt = interpolate(prompt_template, row, user);
691
+ else if (prompt_field === "Formula" || mode === "workflow")
692
+ prompt = eval_expression(
693
+ prompt_formula,
694
+ row,
695
+ user,
696
+ "llm_generate prompt formula"
697
+ );
698
+ else prompt = row[prompt_field];
699
+ const opts = { debugResult: true }; // response_format: "b64_json" };
700
+
701
+ if (model) opts.model = model;
702
+ let history = [];
703
+
704
+ const ans = await getImageGeneration(config, {
705
+ prompt,
706
+ ...opts,
707
+ });
708
+ const upd = {};
709
+
710
+ if (ans.url) {
711
+ //fetch url
712
+ } else if (ans.b64_json) {
713
+ const imgContents = Buffer.from(ans.b64_json, "base64");
714
+ const file = await File.from_contents(
715
+ "generated.png",
716
+ "image/png",
717
+ imgContents,
718
+ user?.id,
719
+ min_role || 1
720
+ );
721
+ upd[answer_field] = file.path_to_serve;
722
+ }
723
+ if (mode === "workflow") return upd;
724
+ else await table.updateRow(upd, row[table.pk_name]);
725
+ },
726
+ },
727
+
570
728
  llm_generate_json: {
571
729
  description:
572
730
  "Generate JSON with AI based on a text prompt. You must sppecify the JSON fields in the configuration.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/large-language-model",
3
- "version": "0.8.4",
3
+ "version": "0.8.6",
4
4
  "description": "Large language models and functionality for Saltcorn",
5
5
  "main": "index.js",
6
6
  "dependencies": {