diff --git a/backend/package.json b/backend/package.json index 41511814f..7d9343596 100644 --- a/backend/package.json +++ b/backend/package.json @@ -41,6 +41,7 @@ "fastify": "4.28.1", "knex": "3.1.0", "objection": "3.1.4", + "openai": "4.56.0", "pg": "8.12.0", "pino": "9.3.2", "pino-pretty": "10.3.1", diff --git a/backend/src/common/config/base-config.package.ts b/backend/src/common/config/base-config.package.ts index dfb59ebcd..853871b74 100644 --- a/backend/src/common/config/base-config.package.ts +++ b/backend/src/common/config/base-config.package.ts @@ -41,6 +41,12 @@ class BaseConfig implements Config { env: 'PORT', default: null, }, + OPEN_AI_KEY: { + doc: 'Key for Open Ai', + format: String, + env: 'OPEN_AI_KEY', + default: null, + }, }, DB: { CONNECTION_STRING: { diff --git a/backend/src/common/config/types/environment-schema.type.ts b/backend/src/common/config/types/environment-schema.type.ts index 6014f5e49..cbc23223f 100644 --- a/backend/src/common/config/types/environment-schema.type.ts +++ b/backend/src/common/config/types/environment-schema.type.ts @@ -5,6 +5,7 @@ type EnvironmentSchema = { APP: { PORT: number; ENVIRONMENT: ValueOf; + OPEN_AI_KEY: string; }; DB: { CONNECTION_STRING: string; diff --git a/backend/src/common/services/open-ai/libs/constants/chat-model.constant.ts b/backend/src/common/services/open-ai/libs/constants/chat-model.constant.ts new file mode 100644 index 000000000..385b505d5 --- /dev/null +++ b/backend/src/common/services/open-ai/libs/constants/chat-model.constant.ts @@ -0,0 +1,3 @@ +const CHAT_MODEL = 'gpt-4o-mini'; + +export { CHAT_MODEL }; diff --git a/backend/src/common/services/open-ai/libs/constants/constants.ts b/backend/src/common/services/open-ai/libs/constants/constants.ts new file mode 100644 index 000000000..b470965ed --- /dev/null +++ b/backend/src/common/services/open-ai/libs/constants/constants.ts @@ -0,0 +1,2 @@ +export { CHAT_MODEL } from './chat-model.constant.js'; +export { MAX_TOKEN } from './max-token.constant.js'; diff --git a/backend/src/common/services/open-ai/libs/constants/max-token.constant.ts b/backend/src/common/services/open-ai/libs/constants/max-token.constant.ts new file mode 100644 index 000000000..e403fe4b1 --- /dev/null +++ b/backend/src/common/services/open-ai/libs/constants/max-token.constant.ts @@ -0,0 +1,3 @@ +const MAX_TOKEN = 16_000; + +export { MAX_TOKEN }; diff --git a/backend/src/common/services/open-ai/libs/enums/enums.ts b/backend/src/common/services/open-ai/libs/enums/enums.ts new file mode 100644 index 000000000..58dc0594a --- /dev/null +++ b/backend/src/common/services/open-ai/libs/enums/enums.ts @@ -0,0 +1 @@ +export { OpenAIRole } from './open-ai-role.enum.js'; diff --git a/backend/src/common/services/open-ai/libs/enums/open-ai-role.enum.ts b/backend/src/common/services/open-ai/libs/enums/open-ai-role.enum.ts new file mode 100644 index 000000000..b4ad8c85a --- /dev/null +++ b/backend/src/common/services/open-ai/libs/enums/open-ai-role.enum.ts @@ -0,0 +1,7 @@ +const OpenAIRole = { + USER: 'user', + SYSTEM: 'system', + ASSISTANT: 'assistant', +} as const; + +export { OpenAIRole }; diff --git a/backend/src/common/services/open-ai/libs/types/message.type.ts b/backend/src/common/services/open-ai/libs/types/message.type.ts new file mode 100644 index 000000000..1bd4ee658 --- /dev/null +++ b/backend/src/common/services/open-ai/libs/types/message.type.ts @@ -0,0 +1,10 @@ +import { type ValueOf } from 'shared'; + +import { type OpenAIRole } from '../enums/enums.js'; + +type Message = { + role: ValueOf; + content: string; +}; + +export { type Message }; diff --git a/backend/src/common/services/open-ai/libs/types/open-ai-service.type.ts b/backend/src/common/services/open-ai/libs/types/open-ai-service.type.ts new file mode 100644 index 000000000..9963d9ccd --- /dev/null +++ b/backend/src/common/services/open-ai/libs/types/open-ai-service.type.ts @@ -0,0 +1,7 @@ +import { type Message } from './message.type.js'; + +type OpenAIService = { + generateText(messages: Message[]): Promise; +}; + +export { type OpenAIService }; diff --git a/backend/src/common/services/open-ai/libs/types/types.ts b/backend/src/common/services/open-ai/libs/types/types.ts new file mode 100644 index 000000000..4b69fcdc4 --- /dev/null +++ b/backend/src/common/services/open-ai/libs/types/types.ts @@ -0,0 +1,2 @@ +export { type Message } from './message.type.js'; +export { type OpenAIService } from './open-ai-service.type.js'; diff --git a/backend/src/common/services/open-ai/open-ai.service.ts b/backend/src/common/services/open-ai/open-ai.service.ts new file mode 100644 index 000000000..99a7dfedb --- /dev/null +++ b/backend/src/common/services/open-ai/open-ai.service.ts @@ -0,0 +1,41 @@ +import OpenAI from 'openai'; + +import { type BaseConfig } from '~/common/config/base-config.package.js'; + +import { CHAT_MODEL, MAX_TOKEN } from './libs/constants/constants.js'; +import { + type Message, + type OpenAIService as OpenAIServiceModule, +} from './libs/types/types.js'; + +type Constructor = { + config: BaseConfig; +}; + +class OpenAIService implements OpenAIServiceModule { + private config: BaseConfig; + private openAi: OpenAI; + + public constructor({ config }: Constructor) { + this.config = config; + this.openAi = this.initOpenAi(); + } + + private initOpenAi = (): OpenAI => { + return new OpenAI({ + apiKey: this.config.ENV.APP.OPEN_AI_KEY, + }); + }; + + public async generateText(messages: Message[]): Promise { + const completion = await this.openAi.chat.completions.create({ + messages, + model: CHAT_MODEL, + max_tokens: MAX_TOKEN, + }); + + return completion.choices[0]?.message.content || ''; + } +} + +export { OpenAIService }; diff --git a/package-lock.json b/package-lock.json index ee1e59c29..8dcddd8bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,6 +51,7 @@ "fastify": "4.28.1", "knex": "3.1.0", "objection": "3.1.4", + "openai": "4.56.0", "pg": "8.12.0", "pino": "9.3.2", "pino-pretty": "10.3.1", @@ -5246,11 +5247,19 @@ "version": "22.3.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.3.0.tgz", "integrity": "sha512-nrWpWVaDZuaVc5X84xJ0vNrLvomM205oQyLsRt7OHNZbSHslcWsvgFR7O7hire2ZonjLrWBbedmotmIlJDVd6g==", - "dev": true, "dependencies": { "undici-types": "~6.18.2" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", @@ -5765,6 +5774,17 @@ "node": ">= 6.0.0" } }, + "node_modules/agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -6081,6 +6101,11 @@ "retry": "0.12.0" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -6676,6 +6701,17 @@ "node": ">=0.1.90" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -7206,6 +7242,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -8678,6 +8722,36 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, "node_modules/formik": { "version": "2.4.6", "resolved": "https://registry.npmjs.org/formik/-/formik-2.4.6.tgz", @@ -9425,6 +9499,14 @@ "node": ">=16.17.0" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/hyperlinker": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/hyperlinker/-/hyperlinker-1.0.0.tgz", @@ -10862,6 +10944,25 @@ "node": ">=10.0.0" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", @@ -11018,6 +11119,24 @@ "integrity": "sha512-qN8v/s2PAJwGUtr1/hYTpNKlD6Y9rc4p8KSmJXyGdYGZsDGKXrGThikLFP9OCHFeLeEpQzPwiAtdIvBLqm//Hw==", "dev": true }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -11358,6 +11477,44 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openai": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.56.0.tgz", + "integrity": "sha512-zcag97+3bG890MNNa0DQD9dGmmTWL8unJdNkulZzWRXrl+QeD+YkBI4H58rJcwErxqGK6a0jVPZ4ReJjhDGcmw==", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/openai/node_modules/@types/node": { + "version": "18.19.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.45.tgz", + "integrity": "sha512-VZxPKNNhjKmaC1SUYowuXSRSMGyQGmQjvvA1xE4QZ0xce2kLtEhPDS+kqpCPBZYgqblCLQ2DAjSzmgCM5auvhA==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/openai/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "node_modules/openapi-types": { "version": "12.1.3", "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", @@ -14237,8 +14394,7 @@ "node_modules/undici-types": { "version": "6.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.18.2.tgz", - "integrity": "sha512-5ruQbENj95yDYJNS3TvcaxPMshV7aizdv/hWYjGIKoANWKjhWNBsr2YEuYZKodQulB1b8l7ILOuDQep3afowQQ==", - "dev": true + "integrity": "sha512-5ruQbENj95yDYJNS3TvcaxPMshV7aizdv/hWYjGIKoANWKjhWNBsr2YEuYZKodQulB1b8l7ILOuDQep3afowQQ==" }, "node_modules/unicorn-magic": { "version": "0.1.0", @@ -14851,6 +15007,14 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "engines": { + "node": ">= 14" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",