From 84f52683b23f4f3d3078094b9de43f7e239b85a6 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Wed, 26 Apr 2023 15:55:44 +0100 Subject: [PATCH 1/5] chatgpt automation block --- packages/server/package.json | 1 + packages/server/src/automations/actions.ts | 3 + .../server/src/automations/steps/openai.ts | 104 ++++++++++++++++++ .../src/automations/tests/openai.spec.ts | 85 ++++++++++++++ packages/server/src/environment.ts | 1 + .../types/src/documents/app/automation.ts | 1 + yarn.lock | 20 +++- 7 files changed, 209 insertions(+), 6 deletions(-) create mode 100644 packages/server/src/automations/steps/openai.ts create mode 100644 packages/server/src/automations/tests/openai.spec.ts diff --git a/packages/server/package.json b/packages/server/package.json index 6aadfd15a0..2e53c0e7ac 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -99,6 +99,7 @@ "mysql2": "2.3.3", "node-fetch": "2.6.7", "open": "8.4.0", + "openai": "^3.2.1", "pg": "8.5.1", "posthog-node": "1.3.0", "pouchdb": "7.3.0", diff --git a/packages/server/src/automations/actions.ts b/packages/server/src/automations/actions.ts index 2a6b760725..de92efb676 100644 --- a/packages/server/src/automations/actions.ts +++ b/packages/server/src/automations/actions.ts @@ -14,6 +14,7 @@ import * as filter from "./steps/filter" import * as delay from "./steps/delay" import * as queryRow from "./steps/queryRows" import * as loop from "./steps/loop" +import * as openai from "./steps/openai" import env from "../environment" import { AutomationStepSchema, @@ -39,6 +40,7 @@ const ACTION_IMPLS: Record< DELAY: delay.run, FILTER: filter.run, QUERY_ROWS: queryRow.run, + OPEN_AI: openai.run, // these used to be lowercase step IDs, maintain for backwards compat discord: discord.run, slack: slack.run, @@ -59,6 +61,7 @@ export const BUILTIN_ACTION_DEFINITIONS: Record = FILTER: filter.definition, QUERY_ROWS: queryRow.definition, LOOP: loop.definition, + OPEN_AI: openai.definition, // these used to be lowercase step IDs, maintain for backwards compat discord: discord.definition, slack: slack.definition, diff --git a/packages/server/src/automations/steps/openai.ts b/packages/server/src/automations/steps/openai.ts new file mode 100644 index 0000000000..79586bb712 --- /dev/null +++ b/packages/server/src/automations/steps/openai.ts @@ -0,0 +1,104 @@ +import { Configuration, OpenAIApi } from "openai"; +import { + AutomationActionStepId, + AutomationStepSchema, + AutomationStepInput, + AutomationStepType, + AutomationIOType, +} from "@budibase/types" +import * as automationUtils from "../automationUtils" +import environment from "../../environment"; + +enum Model { + GPT_35_TURBO = "gpt-3.5-turbo", + // will only work with api keys that have access to the GPT4 API + // GPT_4 = "gpt-4", +} + +export const definition: AutomationStepSchema = { + name: "OpenAI", + tagline: "Send prompts to ChatGPT", + icon: "Algorithm", + description: "Interact with the OpenAI ChatGPT API.", + type: AutomationStepType.ACTION, + internal: true, + stepId: AutomationActionStepId.OPEN_AI, + inputs: { + prompt: "", + }, + schema: { + inputs: { + properties: { + prompt: { + type: AutomationIOType.STRING, + title: "Prompt", + }, + model: { + type: AutomationIOType.STRING, + title: "Model", + enum: Object.values(Model), + }, + }, + required: ["prompt", "model"], + }, + outputs: { + properties: { + success: { + type: AutomationIOType.BOOLEAN, + description: "Whether the action was successful", + }, + response: { + type: AutomationIOType.STRING, + description: "What was output", + }, + }, + required: ["success", "response"], + }, + }, +} + +export async function run({ inputs, context }: AutomationStepInput) { + if (!environment.OPENAI_API_KEY) { + return { + success: false, + response: "OpenAI API Key not configured - please add the OPENAI_API_KEY environment variable.", + } + } + + if (inputs.prompt == null) { + return { + success: false, + response: "Budibase OpenAI Automation Failed: No prompt supplied", + } + } + + try { + const configuration = new Configuration({ + apiKey: environment.OPENAI_API_KEY, + }); + + const openai = new OpenAIApi(configuration); + + const completion = await openai.createChatCompletion({ + model: inputs.model, + messages: [ + { + role: "user", + content: inputs.prompt + } + ], + }); + + let response = completion?.data?.choices[0]?.message?.content + + return { + response, + success: true, + } + } catch (err) { + return { + success: false, + response: automationUtils.getError(err), + } + } +} diff --git a/packages/server/src/automations/tests/openai.spec.ts b/packages/server/src/automations/tests/openai.spec.ts new file mode 100644 index 0000000000..3ba9463f21 --- /dev/null +++ b/packages/server/src/automations/tests/openai.spec.ts @@ -0,0 +1,85 @@ +const setup = require("./utilities") +import environment from "../../environment"; +import openai from "openai" + +jest.mock("openai", jest.fn(() => ({ + Configuration: jest.fn(), + OpenAIApi: jest.fn(() => ({ + createChatCompletion: jest.fn(() => ({ + data: { + choices: [ + { + message: { + content: "This is a test" + }, + } + ] + } + })) + })) +}))) + +const OPENAI_PROMPT = "What is the meaning of life?" + +describe("test the openai action", () => { + let config = setup.getConfig() + + beforeAll(async () => { + await config.init() + }) + + beforeEach(() => { + environment.OPENAI_API_KEY = "abc123" + }) + + afterAll(setup.afterAll) + + + it("should present the correct error message when the OPENAI_API_KEY variable isn't set", async () => { + delete environment.OPENAI_API_KEY + + let res = await setup.runStep("OPEN_AI", + { + prompt: OPENAI_PROMPT + } + ) + expect(res.response).toEqual("OpenAI API Key not configured - please add the OPENAI_API_KEY environment variable.") + expect(res.success).toBeFalsy() + }) + + it("should be able to receive a response from ChatGPT given a prompt", async () => { + const res = await setup.runStep("OPEN_AI", + { + prompt: OPENAI_PROMPT + } + ) + expect(res.response).toEqual("This is a test") + expect(res.success).toBeTruthy() + }) + + + it("should present the correct error message when a prompt is not provided", async () => { + const res = await setup.runStep("OPEN_AI", + { + prompt: null + } + ) + expect(res.response).toEqual("Budibase OpenAI Automation Failed: No prompt supplied") + expect(res.success).toBeFalsy() + }) + + it("should present the correct error message when an error is thrown from the createChatCompletion call", async () => { + openai.OpenAIApi.mockImplementation(() => ({ + createChatCompletion: jest.fn(() => { + throw new Error("An error occurred while calling createChatCompletion"); + }), + })); + + const res = await setup.runStep("OPEN_AI", { + prompt: OPENAI_PROMPT, + }); + + expect(res.response).toEqual("Error: An error occurred while calling createChatCompletion") + expect(res.success).toBeFalsy() + }); +}) diff --git a/packages/server/src/environment.ts b/packages/server/src/environment.ts index 1bd5a6486c..9a52b18e08 100644 --- a/packages/server/src/environment.ts +++ b/packages/server/src/environment.ts @@ -72,6 +72,7 @@ const environment = { BB_ADMIN_USER_EMAIL: process.env.BB_ADMIN_USER_EMAIL, BB_ADMIN_USER_PASSWORD: process.env.BB_ADMIN_USER_PASSWORD, PLUGINS_DIR: process.env.PLUGINS_DIR || "/plugins", + OPENAI_API_KEY: process.env.OPENAI_API_KEY, // flags ALLOW_DEV_AUTOMATIONS: process.env.ALLOW_DEV_AUTOMATIONS, DISABLE_THREADING: process.env.DISABLE_THREADING, diff --git a/packages/types/src/documents/app/automation.ts b/packages/types/src/documents/app/automation.ts index aa600c6375..eaff533761 100644 --- a/packages/types/src/documents/app/automation.ts +++ b/packages/types/src/documents/app/automation.ts @@ -56,6 +56,7 @@ export enum AutomationActionStepId { FILTER = "FILTER", QUERY_ROWS = "QUERY_ROWS", LOOP = "LOOP", + OPEN_AI = "OPEN_AI", // these used to be lowercase step IDs, maintain for backwards compat discord = "discord", slack = "slack", diff --git a/yarn.lock b/yarn.lock index 260f0ae6a6..3d233dbe76 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1486,15 +1486,15 @@ pouchdb-promise "^6.0.4" through2 "^2.0.0" -"@budibase/pro@2.5.6-alpha.29": - version "2.5.6-alpha.29" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.5.6-alpha.29.tgz#71414f68a296535ef53ffb0453352ea137c4aeab" - integrity sha512-tQuzMOo2WFxKvsUgYAfUEcLabRpmAD7hPlhBhCFzYasaXNbJiPhcwv4i52US0i0Wr2IXMb2X0d7fwa8tnbKzIA== +"@budibase/pro@2.5.6-alpha.30": + version "2.5.6-alpha.30" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.5.6-alpha.30.tgz#9b8089a983fd61a062f31a8e5757d7bb5b56fb8c" + integrity sha512-YTyjMHK/wsSOFJkON7a5WRJSgAr8Gh/cflRzifm6Jw1Gb8S8B8Z6uTWW/S7+psVBRGeUfV1s8biYNr71tXz2Ng== dependencies: - "@budibase/backend-core" "2.5.6-alpha.29" + "@budibase/backend-core" "2.5.6-alpha.30" "@budibase/shared-core" "2.4.44-alpha.1" "@budibase/string-templates" "2.4.44-alpha.1" - "@budibase/types" "2.5.6-alpha.29" + "@budibase/types" "2.5.6-alpha.30" "@koa/router" "8.0.8" bull "4.10.1" joi "17.6.0" @@ -18358,6 +18358,14 @@ open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" +openai@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/openai/-/openai-3.2.1.tgz#1fa35bdf979cbde8453b43f2dd3a7d401ee40866" + integrity sha512-762C9BNlJPbjjlWZi4WYK9iM2tAVAv0uUp1UmI34vb0CN5T2mjB/qM6RYBmNKMh/dN9fC+bxqPwWJZUTWW052A== + dependencies: + axios "^0.26.0" + form-data "^4.0.0" + openapi-response-validator@^9.2.0: version "9.3.1" resolved "https://registry.yarnpkg.com/openapi-response-validator/-/openapi-response-validator-9.3.1.tgz#54284d8be608ef53283cbe7448accce8106b1c56" From 4f020a4db4766974d092827e8c843c2a79050fb1 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Wed, 26 Apr 2023 15:56:46 +0100 Subject: [PATCH 2/5] lint --- .../server/src/automations/steps/openai.ts | 17 +++-- .../src/automations/tests/openai.spec.ts | 75 ++++++++++--------- 2 files changed, 47 insertions(+), 45 deletions(-) diff --git a/packages/server/src/automations/steps/openai.ts b/packages/server/src/automations/steps/openai.ts index 79586bb712..a7c22ffd0c 100644 --- a/packages/server/src/automations/steps/openai.ts +++ b/packages/server/src/automations/steps/openai.ts @@ -1,4 +1,4 @@ -import { Configuration, OpenAIApi } from "openai"; +import { Configuration, OpenAIApi } from "openai" import { AutomationActionStepId, AutomationStepSchema, @@ -7,7 +7,7 @@ import { AutomationIOType, } from "@budibase/types" import * as automationUtils from "../automationUtils" -import environment from "../../environment"; +import environment from "../../environment" enum Model { GPT_35_TURBO = "gpt-3.5-turbo", @@ -61,7 +61,8 @@ export async function run({ inputs, context }: AutomationStepInput) { if (!environment.OPENAI_API_KEY) { return { success: false, - response: "OpenAI API Key not configured - please add the OPENAI_API_KEY environment variable.", + response: + "OpenAI API Key not configured - please add the OPENAI_API_KEY environment variable.", } } @@ -75,19 +76,19 @@ export async function run({ inputs, context }: AutomationStepInput) { try { const configuration = new Configuration({ apiKey: environment.OPENAI_API_KEY, - }); + }) - const openai = new OpenAIApi(configuration); + const openai = new OpenAIApi(configuration) const completion = await openai.createChatCompletion({ model: inputs.model, messages: [ { role: "user", - content: inputs.prompt - } + content: inputs.prompt, + }, ], - }); + }) let response = completion?.data?.choices[0]?.message?.content diff --git a/packages/server/src/automations/tests/openai.spec.ts b/packages/server/src/automations/tests/openai.spec.ts index 3ba9463f21..032f670db1 100644 --- a/packages/server/src/automations/tests/openai.spec.ts +++ b/packages/server/src/automations/tests/openai.spec.ts @@ -1,23 +1,26 @@ const setup = require("./utilities") -import environment from "../../environment"; +import environment from "../../environment" import openai from "openai" -jest.mock("openai", jest.fn(() => ({ - Configuration: jest.fn(), - OpenAIApi: jest.fn(() => ({ - createChatCompletion: jest.fn(() => ({ - data: { - choices: [ - { - message: { - content: "This is a test" +jest.mock( + "openai", + jest.fn(() => ({ + Configuration: jest.fn(), + OpenAIApi: jest.fn(() => ({ + createChatCompletion: jest.fn(() => ({ + data: { + choices: [ + { + message: { + content: "This is a test", + }, }, - } - ] - } - })) + ], + }, + })), + })), })) -}))) +) const OPENAI_PROMPT = "What is the meaning of life?" @@ -34,52 +37,50 @@ describe("test the openai action", () => { afterAll(setup.afterAll) - it("should present the correct error message when the OPENAI_API_KEY variable isn't set", async () => { delete environment.OPENAI_API_KEY - let res = await setup.runStep("OPEN_AI", - { - prompt: OPENAI_PROMPT - } + let res = await setup.runStep("OPEN_AI", { + prompt: OPENAI_PROMPT, + }) + expect(res.response).toEqual( + "OpenAI API Key not configured - please add the OPENAI_API_KEY environment variable." ) - expect(res.response).toEqual("OpenAI API Key not configured - please add the OPENAI_API_KEY environment variable.") expect(res.success).toBeFalsy() }) it("should be able to receive a response from ChatGPT given a prompt", async () => { - const res = await setup.runStep("OPEN_AI", - { - prompt: OPENAI_PROMPT - } - ) + const res = await setup.runStep("OPEN_AI", { + prompt: OPENAI_PROMPT, + }) expect(res.response).toEqual("This is a test") expect(res.success).toBeTruthy() }) - it("should present the correct error message when a prompt is not provided", async () => { - const res = await setup.runStep("OPEN_AI", - { - prompt: null - } + const res = await setup.runStep("OPEN_AI", { + prompt: null, + }) + expect(res.response).toEqual( + "Budibase OpenAI Automation Failed: No prompt supplied" ) - expect(res.response).toEqual("Budibase OpenAI Automation Failed: No prompt supplied") expect(res.success).toBeFalsy() }) it("should present the correct error message when an error is thrown from the createChatCompletion call", async () => { openai.OpenAIApi.mockImplementation(() => ({ createChatCompletion: jest.fn(() => { - throw new Error("An error occurred while calling createChatCompletion"); + throw new Error("An error occurred while calling createChatCompletion") }), - })); + })) const res = await setup.runStep("OPEN_AI", { prompt: OPENAI_PROMPT, - }); + }) - expect(res.response).toEqual("Error: An error occurred while calling createChatCompletion") + expect(res.response).toEqual( + "Error: An error occurred while calling createChatCompletion" + ) expect(res.success).toBeFalsy() - }); + }) }) From e70e3ae662fee5fa11e1123bf787640f9e750627 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Wed, 26 Apr 2023 15:58:21 +0100 Subject: [PATCH 3/5] rename --- packages/server/src/automations/actions.ts | 4 ++-- packages/server/src/automations/steps/openai.ts | 4 ++-- packages/server/src/automations/tests/openai.spec.ts | 8 ++++---- packages/types/src/documents/app/automation.ts | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/server/src/automations/actions.ts b/packages/server/src/automations/actions.ts index de92efb676..f0feab006c 100644 --- a/packages/server/src/automations/actions.ts +++ b/packages/server/src/automations/actions.ts @@ -40,7 +40,7 @@ const ACTION_IMPLS: Record< DELAY: delay.run, FILTER: filter.run, QUERY_ROWS: queryRow.run, - OPEN_AI: openai.run, + OPENAI: openai.run, // these used to be lowercase step IDs, maintain for backwards compat discord: discord.run, slack: slack.run, @@ -61,7 +61,7 @@ export const BUILTIN_ACTION_DEFINITIONS: Record = FILTER: filter.definition, QUERY_ROWS: queryRow.definition, LOOP: loop.definition, - OPEN_AI: openai.definition, + OPENAI: openai.definition, // these used to be lowercase step IDs, maintain for backwards compat discord: discord.definition, slack: slack.definition, diff --git a/packages/server/src/automations/steps/openai.ts b/packages/server/src/automations/steps/openai.ts index a7c22ffd0c..265a40d466 100644 --- a/packages/server/src/automations/steps/openai.ts +++ b/packages/server/src/automations/steps/openai.ts @@ -22,7 +22,7 @@ export const definition: AutomationStepSchema = { description: "Interact with the OpenAI ChatGPT API.", type: AutomationStepType.ACTION, internal: true, - stepId: AutomationActionStepId.OPEN_AI, + stepId: AutomationActionStepId.OPENAI, inputs: { prompt: "", }, @@ -90,7 +90,7 @@ export async function run({ inputs, context }: AutomationStepInput) { ], }) - let response = completion?.data?.choices[0]?.message?.content + const response = completion?.data?.choices[0]?.message?.content return { response, diff --git a/packages/server/src/automations/tests/openai.spec.ts b/packages/server/src/automations/tests/openai.spec.ts index 032f670db1..31f7e48305 100644 --- a/packages/server/src/automations/tests/openai.spec.ts +++ b/packages/server/src/automations/tests/openai.spec.ts @@ -40,7 +40,7 @@ describe("test the openai action", () => { it("should present the correct error message when the OPENAI_API_KEY variable isn't set", async () => { delete environment.OPENAI_API_KEY - let res = await setup.runStep("OPEN_AI", { + let res = await setup.runStep("OPENAI", { prompt: OPENAI_PROMPT, }) expect(res.response).toEqual( @@ -50,7 +50,7 @@ describe("test the openai action", () => { }) it("should be able to receive a response from ChatGPT given a prompt", async () => { - const res = await setup.runStep("OPEN_AI", { + const res = await setup.runStep("OPENAI", { prompt: OPENAI_PROMPT, }) expect(res.response).toEqual("This is a test") @@ -58,7 +58,7 @@ describe("test the openai action", () => { }) it("should present the correct error message when a prompt is not provided", async () => { - const res = await setup.runStep("OPEN_AI", { + const res = await setup.runStep("OPENAI", { prompt: null, }) expect(res.response).toEqual( @@ -74,7 +74,7 @@ describe("test the openai action", () => { }), })) - const res = await setup.runStep("OPEN_AI", { + const res = await setup.runStep("OPENAI", { prompt: OPENAI_PROMPT, }) diff --git a/packages/types/src/documents/app/automation.ts b/packages/types/src/documents/app/automation.ts index eaff533761..11e868023f 100644 --- a/packages/types/src/documents/app/automation.ts +++ b/packages/types/src/documents/app/automation.ts @@ -56,7 +56,7 @@ export enum AutomationActionStepId { FILTER = "FILTER", QUERY_ROWS = "QUERY_ROWS", LOOP = "LOOP", - OPEN_AI = "OPEN_AI", + OPENAI = "OPENAI", // these used to be lowercase step IDs, maintain for backwards compat discord = "discord", slack = "slack", From db5d051755bf1017b093a4147a4d62109870b370 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Wed, 24 May 2023 15:26:27 +0100 Subject: [PATCH 4/5] GPT4 support --- packages/server/src/automations/steps/openai.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/automations/steps/openai.ts b/packages/server/src/automations/steps/openai.ts index 265a40d466..88d3fb8b85 100644 --- a/packages/server/src/automations/steps/openai.ts +++ b/packages/server/src/automations/steps/openai.ts @@ -12,7 +12,7 @@ import environment from "../../environment" enum Model { GPT_35_TURBO = "gpt-3.5-turbo", // will only work with api keys that have access to the GPT4 API - // GPT_4 = "gpt-4", + GPT_4 = "gpt-4", } export const definition: AutomationStepSchema = { From f86d321e2ea7978057f4e2231949dd3bc96c4f36 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Wed, 24 May 2023 17:17:23 +0100 Subject: [PATCH 5/5] restrict openai just to self host --- packages/server/src/automations/actions.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/server/src/automations/actions.ts b/packages/server/src/automations/actions.ts index 01a9731d20..2f64e75816 100644 --- a/packages/server/src/automations/actions.ts +++ b/packages/server/src/automations/actions.ts @@ -14,7 +14,6 @@ import * as filter from "./steps/filter" import * as delay from "./steps/delay" import * as queryRow from "./steps/queryRows" import * as loop from "./steps/loop" -import * as openai from "./steps/openai" import env from "../environment" import { AutomationStepSchema, @@ -40,7 +39,6 @@ const ACTION_IMPLS: Record< DELAY: delay.run, FILTER: filter.run, QUERY_ROWS: queryRow.run, - OPENAI: openai.run, // these used to be lowercase step IDs, maintain for backwards compat discord: discord.run, slack: slack.run, @@ -61,7 +59,6 @@ export const BUILTIN_ACTION_DEFINITIONS: Record = FILTER: filter.definition, QUERY_ROWS: queryRow.definition, LOOP: loop.definition, - OPENAI: openai.definition, // these used to be lowercase step IDs, maintain for backwards compat discord: discord.definition, slack: slack.definition, @@ -74,10 +71,15 @@ export const BUILTIN_ACTION_DEFINITIONS: Record = // ran at all if (env.SELF_HOSTED) { const bash = require("./steps/bash") + const openai = require("./steps/openai") + // @ts-ignore ACTION_IMPLS["EXECUTE_BASH"] = bash.run // @ts-ignore BUILTIN_ACTION_DEFINITIONS["EXECUTE_BASH"] = bash.definition + + ACTION_IMPLS.OPENAI = openai.run + BUILTIN_ACTION_DEFINITIONS.OPENAI = openai.definition } export async function getActionDefinitions() {