feat: connect chatbot to conversation database
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"conversation": "Conversation"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"conversation": "مکالمه"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class Conversation1791194400675 implements MigrationInterface {
|
||||||
|
name = 'Conversation1791194400675';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "conversation" ("id" SERIAL NOT NULL, "title" character varying, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "userId" integer, CONSTRAINT "PK_864528ec4274360a40f66c29845" PRIMARY KEY ("id"))`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TYPE "public"."message_role_enum" AS ENUM('user', 'assistant', 'doctor')`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "message" ("id" SERIAL NOT NULL, "role" "public"."message_role_enum" NOT NULL, "content" text NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "conversationId" integer, CONSTRAINT "PK_ba01f0a3e0123651915008bc578" PRIMARY KEY ("id"))`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "conversation" ADD CONSTRAINT "FK_c308b1cd542522bb66430fa860a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "message" ADD CONSTRAINT "FK_7cf4a4df1f2627f72bf6231635f" FOREIGN KEY ("conversationId") REFERENCES "conversation"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "message" DROP CONSTRAINT "FK_7cf4a4df1f2627f72bf6231635f"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "conversation" DROP CONSTRAINT "FK_c308b1cd542522bb66430fa860a"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "message"`);
|
||||||
|
await queryRunner.query(`DROP TYPE "public"."message_role_enum"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "conversation"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ Content-Type: application/json
|
|||||||
|
|
||||||
{
|
{
|
||||||
"phone": "09333026363",
|
"phone": "09333026363",
|
||||||
"otp": "55333"
|
"otp": "23122"
|
||||||
}
|
}
|
||||||
|
|
||||||
{{
|
{{
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
import { Body, Controller, Post, Res } from '@nestjs/common';
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
Post,
|
||||||
|
Res,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { ChatbotService } from './providers/chatbot.service';
|
import { ChatbotService } from './providers/chatbot.service';
|
||||||
import { Public } from '../auth/decorators/public.decorator';
|
|
||||||
import type { Response } from 'express';
|
import type { Response } from 'express';
|
||||||
|
import { ChatDTO } from './dtos/chat.dto';
|
||||||
|
import { ActiveUser } from '@/common/decorators/active-user.decorator';
|
||||||
|
import { User } from '../users/entities/user.entity';
|
||||||
|
|
||||||
@Controller('chatbot')
|
@Controller('chatbot')
|
||||||
export class ChatbotController {
|
export class ChatbotController {
|
||||||
@@ -13,24 +22,31 @@ export class ChatbotController {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post('chat')
|
@Post('chat')
|
||||||
// TEMP:
|
@HttpCode(HttpStatus.OK)
|
||||||
@Public()
|
public async chat(@Body() chatDto: ChatDTO, @ActiveUser() user: User) {
|
||||||
public async chat(@Body('message') message: string) {
|
return this.chatbotService.chat(
|
||||||
return this.chatbotService.chat(message);
|
user,
|
||||||
|
chatDto.message,
|
||||||
|
chatDto.conversationId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('stream')
|
@Post('stream')
|
||||||
// TEMP:
|
@HttpCode(HttpStatus.OK)
|
||||||
@Public()
|
|
||||||
public async streamChat(
|
public async streamChat(
|
||||||
@Body('message') message: string,
|
@Body() chatDto: ChatDTO,
|
||||||
|
@ActiveUser() user: User,
|
||||||
@Res() res: Response,
|
@Res() res: Response,
|
||||||
) {
|
) {
|
||||||
res.setHeader('Content-Type', 'text/event-stream');
|
res.setHeader('Content-Type', 'text/event-stream');
|
||||||
res.setHeader('Cache-Control', 'no-cache');
|
res.setHeader('Cache-Control', 'no-cache');
|
||||||
res.setHeader('Connection', 'keep-alive');
|
res.setHeader('Connection', 'keep-alive');
|
||||||
|
|
||||||
for await (const content of this.chatbotService.streamChat(message)) {
|
for await (const content of this.chatbotService.streamChat(
|
||||||
|
user,
|
||||||
|
chatDto.message,
|
||||||
|
chatDto.conversationId,
|
||||||
|
)) {
|
||||||
res.write(
|
res.write(
|
||||||
`data: ${JSON.stringify({
|
`data: ${JSON.stringify({
|
||||||
message: content,
|
message: content,
|
||||||
@@ -40,30 +56,4 @@ export class ChatbotController {
|
|||||||
|
|
||||||
res.end();
|
res.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Sse('stream')
|
|
||||||
// @Public()
|
|
||||||
// public streamChat(
|
|
||||||
// @Body('message') message: string,
|
|
||||||
// ): Observable<MessageEvent> {
|
|
||||||
// return new Observable((subscriber) => {
|
|
||||||
// const run = async () => {
|
|
||||||
// try {
|
|
||||||
// for await (const content of this.chatbotService.streamChat(message)) {
|
|
||||||
// subscriber.next({
|
|
||||||
// data: {
|
|
||||||
// message: content,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// subscriber.complete();
|
|
||||||
// } catch (err) {
|
|
||||||
// subscriber.error(err);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// void run();
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ChatbotController } from './chatbot.controller';
|
import { ChatbotController } from './chatbot.controller';
|
||||||
import { ChatbotService } from './providers/chatbot.service';
|
import { ChatbotService } from './providers/chatbot.service';
|
||||||
|
import { ConversationModule } from '../conversation/conversation.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [ConversationModule],
|
||||||
controllers: [ChatbotController],
|
controllers: [ChatbotController],
|
||||||
providers: [ChatbotService],
|
providers: [ChatbotService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
import { IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||||
import { i18nValidationMessage as t } from 'nestjs-i18n';
|
import { i18nValidationMessage as t } from 'nestjs-i18n';
|
||||||
|
|
||||||
export class ChatDTO {
|
export class ChatDTO {
|
||||||
@IsString({
|
@IsInt({
|
||||||
message: t('validation.wrongFieldFormat', {
|
message: t('validation.wrongFieldFormat', {
|
||||||
field: '$t(chatbot.fields.conversationId)',
|
field: '$t(chatbot.fields.conversationId)',
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
conversationId?: string;
|
conversationId?: number;
|
||||||
|
|
||||||
@IsString({
|
@IsString({
|
||||||
message: t('validation.wrongFieldFormat', {
|
message: t('validation.wrongFieldFormat', {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
POST {{baseUrl}}/chatbot/chat
|
POST {{baseUrl}}/chatbot/chat
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
# Authorization: Bearer {{$global.accessToken}}
|
Authorization: Bearer {{$global.accessToken}}
|
||||||
|
|
||||||
{
|
{
|
||||||
"message": "چطوری بفهمم به HPV مبتلا هستم یا خیر؟"
|
"message": "چطوری بفهمم به HPV مبتلا هستم یا خیر؟"
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { CHATBOT_SYSTEM_PROMPT } from '../constants';
|
import { CHATBOT_SYSTEM_PROMPT } from '../constants';
|
||||||
|
import { User } from '@/modules/users/entities/user.entity';
|
||||||
|
import { Conversation } from '@/modules/conversation/entities/conversation.entity';
|
||||||
|
import { ConversationService } from '@/modules/conversation/providers/conversation.service';
|
||||||
|
import { MessageRole } from '@/modules/conversation/enums/message-role.enum';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ChatbotService {
|
export class ChatbotService {
|
||||||
@@ -9,6 +13,11 @@ export class ChatbotService {
|
|||||||
* Inject Config Service
|
* Inject Config Service
|
||||||
*/
|
*/
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inject Conversation Service
|
||||||
|
*/
|
||||||
|
private readonly conversationService: ConversationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private async getOpenRouter() {
|
private async getOpenRouter() {
|
||||||
@@ -19,22 +28,61 @@ export class ChatbotService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async chat(message: string) {
|
private async getConversationMessages(
|
||||||
|
user: User,
|
||||||
|
message: string,
|
||||||
|
conversationId?: number,
|
||||||
|
) {
|
||||||
|
let conversation: Conversation;
|
||||||
|
|
||||||
|
if (conversationId) {
|
||||||
|
conversation = await this.conversationService.findConversationById(
|
||||||
|
conversationId,
|
||||||
|
user,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
conversation = await this.conversationService.createConversation(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.conversationService.addMessage(
|
||||||
|
conversation,
|
||||||
|
MessageRole.USER,
|
||||||
|
message,
|
||||||
|
);
|
||||||
|
|
||||||
|
const messages = await this.conversationService.getMessages(conversation);
|
||||||
|
|
||||||
|
return {
|
||||||
|
conversation,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'system' as const,
|
||||||
|
content: CHATBOT_SYSTEM_PROMPT.trim(),
|
||||||
|
},
|
||||||
|
...messages.map((message) => ({
|
||||||
|
role:
|
||||||
|
message.role === MessageRole.USER
|
||||||
|
? ('user' as const)
|
||||||
|
: ('assistant' as const),
|
||||||
|
content: message.content,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async chat(user: User, message: string, conversationId?: number) {
|
||||||
|
const { conversation, messages } = await this.getConversationMessages(
|
||||||
|
user,
|
||||||
|
message,
|
||||||
|
conversationId,
|
||||||
|
);
|
||||||
|
|
||||||
const openRouter = await this.getOpenRouter();
|
const openRouter = await this.getOpenRouter();
|
||||||
|
|
||||||
const response = await openRouter.chat.send({
|
const response = await openRouter.chat.send({
|
||||||
chatRequest: {
|
chatRequest: {
|
||||||
model: this.configService.getOrThrow<string>('openRouter.model'),
|
model: this.configService.getOrThrow<string>('openRouter.model'),
|
||||||
messages: [
|
messages,
|
||||||
{
|
|
||||||
role: 'system',
|
|
||||||
content: CHATBOT_SYSTEM_PROMPT.trim(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: 'user',
|
|
||||||
content: message,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -48,27 +96,35 @@ export class ChatbotService {
|
|||||||
throw new Error('OpenRouter returned an unexpected response');
|
throw new Error('OpenRouter returned an unexpected response');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.conversationService.addMessage(
|
||||||
|
conversation,
|
||||||
|
MessageRole.ASSISTANT,
|
||||||
|
content,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
conversationId: conversation.id,
|
||||||
message: content,
|
message: content,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async *streamChat(message: string) {
|
public async *streamChat(
|
||||||
|
user: User,
|
||||||
|
message: string,
|
||||||
|
conversationId?: number,
|
||||||
|
) {
|
||||||
|
const { conversation, messages } = await this.getConversationMessages(
|
||||||
|
user,
|
||||||
|
message,
|
||||||
|
conversationId,
|
||||||
|
);
|
||||||
|
|
||||||
const openRouter = await this.getOpenRouter();
|
const openRouter = await this.getOpenRouter();
|
||||||
|
|
||||||
const response = await openRouter.chat.send({
|
const response = await openRouter.chat.send({
|
||||||
chatRequest: {
|
chatRequest: {
|
||||||
model: this.configService.getOrThrow<string>('openRouter.model'),
|
model: this.configService.getOrThrow<string>('openRouter.model'),
|
||||||
messages: [
|
messages,
|
||||||
{
|
|
||||||
role: 'system',
|
|
||||||
content: CHATBOT_SYSTEM_PROMPT.trim(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: 'user',
|
|
||||||
content: message,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
stream: true,
|
stream: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -77,12 +133,26 @@ export class ChatbotService {
|
|||||||
throw new Error('Expected streaming response');
|
throw new Error('Expected streaming response');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let assistantMessage = '';
|
||||||
|
|
||||||
for await (const chunk of response) {
|
for await (const chunk of response) {
|
||||||
const content = chunk.choices[0]?.delta?.content;
|
const content = chunk.choices[0]?.delta?.content;
|
||||||
|
|
||||||
if (content) {
|
if (!content) {
|
||||||
yield content;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assistantMessage += content;
|
||||||
|
|
||||||
|
yield { conversationId: conversation.id, content };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assistantMessage) {
|
||||||
|
await this.conversationService.addMessage(
|
||||||
|
conversation,
|
||||||
|
MessageRole.ASSISTANT,
|
||||||
|
assistantMessage,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ import { Module } from '@nestjs/common';
|
|||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { Conversation } from './entities/conversation.entity';
|
import { Conversation } from './entities/conversation.entity';
|
||||||
import { Message } from './entities/message.entity';
|
import { Message } from './entities/message.entity';
|
||||||
|
import { ConversationService } from './providers/conversation.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Conversation, Message])],
|
imports: [TypeOrmModule.forFeature([Conversation, Message])],
|
||||||
|
providers: [ConversationService],
|
||||||
|
exports: [ConversationService],
|
||||||
})
|
})
|
||||||
export class ConversationModule {}
|
export class ConversationModule {}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { HttpStatus, Injectable } from '@nestjs/common';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { Conversation } from '../entities/conversation.entity';
|
import { Conversation } from '../entities/conversation.entity';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Message } from '../entities/message.entity';
|
import { Message } from '../entities/message.entity';
|
||||||
import { User } from '@/modules/users/entities/user.entity';
|
import { User } from '@/modules/users/entities/user.entity';
|
||||||
import { AppTimeoutException } from '@/common/exceptions/app-timeout.exception';
|
import { AppTimeoutException } from '@/common/exceptions/app-timeout.exception';
|
||||||
|
import { AppException } from '@/common/exceptions/app.exception';
|
||||||
|
import { MessageRole } from '../enums/message-role.enum';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ConversationService {
|
export class ConversationService {
|
||||||
@@ -38,7 +40,72 @@ export class ConversationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// public async findConversationById(id: number, user: User): Promise<Conversation> {
|
public async findConversationById(
|
||||||
// const
|
id: number,
|
||||||
// }
|
user: User,
|
||||||
|
): Promise<Conversation> {
|
||||||
|
let conversation: Conversation | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
conversation = await this.conversationRepository.findOne({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
throw new AppTimeoutException(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!conversation) {
|
||||||
|
throw new AppException(
|
||||||
|
'common.errors.entityNotFound',
|
||||||
|
HttpStatus.NOT_FOUND,
|
||||||
|
{ entity: '$t(conversations.fields.conversation)' },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return conversation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async addMessage(
|
||||||
|
conversation: Conversation,
|
||||||
|
role: MessageRole,
|
||||||
|
content: string,
|
||||||
|
): Promise<Message> {
|
||||||
|
const message = this.messageRepository.create({
|
||||||
|
conversation,
|
||||||
|
role,
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await this.messageRepository.save(message);
|
||||||
|
} catch (err) {
|
||||||
|
throw new AppTimeoutException(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getMessages(conversation: Conversation): Promise<Message[]> {
|
||||||
|
let messages: Array<Message> = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
messages = await this.messageRepository.find({
|
||||||
|
where: {
|
||||||
|
conversation: {
|
||||||
|
id: conversation.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
createdAt: 'ASC',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
throw new AppTimeoutException(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user