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"