diff --git a/package-lock.json b/package-lock.json index 0519634..20c668a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "license": "UNLICENSED", "dependencies": { + "@nestjs/axios": "^4.0.1", "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", @@ -19,6 +20,7 @@ "@nestjs/swagger": "^11.2.0", "@prisma/client": "^6.18.0", "@quixo3/prisma-session-store": "^3.1.13", + "axios": "^1.13.2", "bullmq": "^5.58.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", @@ -2965,6 +2967,17 @@ "node": ">= 10" } }, + "node_modules/@nestjs/axios": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-4.0.1.tgz", + "integrity": "sha512-68pFJgu+/AZbWkGu65Z3r55bTsCPlgyKaV4BSG8yUAD72q1PPuyVRgUwFv6BxdnibTUHlyxm06FmYWNC+bjN7A==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "axios": "^1.3.1", + "rxjs": "^7.0.0" + } + }, "node_modules/@nestjs/cli": { "version": "11.0.14", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.14.tgz", @@ -5474,8 +5487,18 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } }, "node_modules/b4a": { "version": "1.6.7", @@ -6255,7 +6278,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -6601,7 +6623,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -6850,7 +6871,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", @@ -7615,6 +7635,26 @@ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/fork-ts-checker-webpack-plugin": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz", @@ -7646,7 +7686,6 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -7672,7 +7711,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -7681,7 +7719,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -8018,7 +8055,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -10478,6 +10514,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index 2b2f2bc..5195085 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "db:reset": "prisma migrate reset --force" }, "dependencies": { + "@nestjs/axios": "^4.0.1", "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", @@ -35,6 +36,7 @@ "@nestjs/swagger": "^11.2.0", "@prisma/client": "^6.18.0", "@quixo3/prisma-session-store": "^3.1.13", + "axios": "^1.13.2", "bullmq": "^5.58.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 05b9dfa..905d890 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -33,6 +33,7 @@ model userModuleAccess { employee_management Boolean @default(false) personal_profile Boolean @default(false) dashboard Boolean @default(false) + chatbot Boolean @default(false) user Users @relation("UserModuleAccess", fields: [user_id], references: [id]) @@map("user_module_access") @@ -377,6 +378,7 @@ enum Modules { employee_management personal_profile dashboard + chatbot @@map("modules") } diff --git a/src/app.module.ts b/src/app.module.ts index b953e41..93c4836 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -12,6 +12,7 @@ import { TimeAndAttendanceModule } from 'src/time-and-attendance/time-and-attend import { AuthenticationModule } from 'src/identity-and-account/authentication/auth.module'; import { IdentityAndAccountModule } from 'src/identity-and-account/identity-and-account.module'; import { PrismaLegacyModule } from 'src/prisma-legacy/prisma.module'; +import { ChatbotModule } from 'src/chatbot/chatbot.module'; @Module({ imports: [ @@ -23,6 +24,7 @@ import { PrismaLegacyModule } from 'src/prisma-legacy/prisma.module'; PrismaLegacyModule, TimeAndAttendanceModule, IdentityAndAccountModule, + ChatbotModule, ], controllers: [AppController], providers: [ diff --git a/src/chatbot/chatbot.controller.ts b/src/chatbot/chatbot.controller.ts new file mode 100644 index 0000000..e0b1d31 --- /dev/null +++ b/src/chatbot/chatbot.controller.ts @@ -0,0 +1,29 @@ +import { Controller, Post, Body, Get, Query, Param } from '@nestjs/common'; +import { Message } from 'src/chatbot/dtos/dialog-message.dto'; +import { UserMessageDto } from 'src/chatbot/dtos/user-message.dto'; +import { ChatbotService } from 'src/chatbot/chatbot.service'; +import { PageContextDto } from 'src/chatbot/dtos/page-context.dto'; +import { Access } from 'src/common/decorators/module-access.decorators'; + +@Controller('chatbot') +export class ChatbotController { + constructor(private readonly chatbotService: ChatbotService) {} + + @Post('') + async testConnection(@Body() body: UserMessageDto): Promise { + return await this.chatbotService.pingExternalApi(body); + } + + @Post('context') + async sendContext(@Body() body: PageContextDto): Promise { + const sendPageContext = await this.chatbotService.sendPageContext(body); + return sendPageContext; + } + + // Will have to modify later on to accomodate newer versions of User Auth/User type Structure + @Post('user') + async sendUserCredentials(@Access('email') email: string,): Promise { + const sendUserContext = await this.chatbotService.sendUserContext(email); + return sendUserContext; + } +} diff --git a/src/chatbot/chatbot.module.ts b/src/chatbot/chatbot.module.ts new file mode 100644 index 0000000..0e61e04 --- /dev/null +++ b/src/chatbot/chatbot.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { ChatbotController } from 'src/chatbot/chatbot.controller'; +import { HttpModule } from '@nestjs/axios'; +import { ChatbotService } from 'src/chatbot/chatbot.service'; + +@Module({ + imports: [HttpModule], + controllers: [ChatbotController], + providers: [ChatbotService], + exports: [], +}) +export class ChatbotModule {} diff --git a/src/chatbot/chatbot.service.ts b/src/chatbot/chatbot.service.ts new file mode 100644 index 0000000..4cf2eb2 --- /dev/null +++ b/src/chatbot/chatbot.service.ts @@ -0,0 +1,62 @@ +import { Injectable } from '@nestjs/common'; +import { UserMessageDto } from 'src/chatbot/dtos/user-message.dto'; +import { HttpService } from '@nestjs/axios'; +import { firstValueFrom } from 'rxjs'; +import { PageContextDto } from 'src/chatbot/dtos/page-context.dto'; +import { UserDto } from 'src/identity-and-account/users-management/user.dto'; +import { Message } from 'src/chatbot/dtos/dialog-message.dto'; + +@Injectable() +export class ChatbotService { + constructor(private readonly httpService: HttpService) { } + sessionId: string; + + async pingExternalApi(body: UserMessageDto): Promise { + const response = await firstValueFrom( + this.httpService.post( + 'https://n8nai.targo.ca/webhook/984c578e-59c7-4ca1-97f8-e225fd2acf01', + { userInput: body.userInput, userId: this.sessionId }, + ), + ); + + const cleanText = + Array.isArray(response) && response[0]?.output + ? response[0].output + : JSON.stringify(response); + + return { + text: cleanText, + sent: false, + }; + } + + async sendPageContext(body: PageContextDto) { + const response = await firstValueFrom( + this.httpService.post( + 'https://n8nai.targo.ca/webhook/984c578e-59c7-4ca1-97f8-e225fd2acf01', + { features: body, userId: this.sessionId, userInput: '' }, + ), + ); + return response.data; + } + + // Will have to modify later on to accomodate newer versions of User Auth/User type Structure + async sendUserContext(user_email: string) { + if (!this.sessionId) { + this.sessionId = 'SessionId = ' + user_email; + } + + const response = await firstValueFrom( + this.httpService.post( + 'https://n8nai.targo.ca/webhook/984c578e-59c7-4ca1-97f8-e225fd2acf01', + { + userId: this.sessionId, + userInput: '', + features: '', + }, + { headers: { 'Content-Tyoe': 'application/json' } }, + ), + ); + return response.data; + } +} diff --git a/src/chatbot/dtos/dialog-message.dto.ts b/src/chatbot/dtos/dialog-message.dto.ts new file mode 100644 index 0000000..0ba70e8 --- /dev/null +++ b/src/chatbot/dtos/dialog-message.dto.ts @@ -0,0 +1,9 @@ +import { IsBoolean, IsString } from 'class-validator'; + +export class Message { + @IsString() + text!: string; + + @IsBoolean() + sent!: boolean; +} diff --git a/src/chatbot/dtos/page-context.dto.ts b/src/chatbot/dtos/page-context.dto.ts new file mode 100644 index 0000000..aeee68f --- /dev/null +++ b/src/chatbot/dtos/page-context.dto.ts @@ -0,0 +1,15 @@ +import { IsArray, IsString } from 'class-validator'; + +export class PageContextDto { + @IsString() + name: string; + + @IsString() + description: string; + + @IsArray() + features: string[]; + + @IsString() + path?: string; +} diff --git a/src/chatbot/dtos/user-message.dto.ts b/src/chatbot/dtos/user-message.dto.ts new file mode 100644 index 0000000..5ee99b1 --- /dev/null +++ b/src/chatbot/dtos/user-message.dto.ts @@ -0,0 +1,9 @@ +import { Transform } from 'class-transformer'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class UserMessageDto { + @IsString() + @IsNotEmpty() + @Transform(({ value }) => value.trim()) + userInput!: string; +} diff --git a/src/identity-and-account/users-management/user.dto.ts b/src/identity-and-account/users-management/user.dto.ts index 3b5924a..95092bc 100644 --- a/src/identity-and-account/users-management/user.dto.ts +++ b/src/identity-and-account/users-management/user.dto.ts @@ -2,9 +2,9 @@ import { Modules, Roles } from "@prisma/client"; import { IsArray, IsEmail, IsEnum, IsString } from "class-validator"; export class UserDto { - @IsString() first_name!: string; - @IsString() last_name!: string; - @IsEmail() email!: string; - @IsEnum(Roles) role!: string; + @IsString() first_name: string; + @IsString() last_name: string; + @IsEmail() email: string; + @IsEnum(Roles) role: string; @IsArray() @IsEnum(Modules, { each: true }) user_module_access!: Modules[]; } \ No newline at end of file