feat: add chatbot module and service
This commit is contained in:
Generated
+18
@@ -19,6 +19,7 @@
|
|||||||
"@nestjs/swagger": "^11.2.6",
|
"@nestjs/swagger": "^11.2.6",
|
||||||
"@nestjs/throttler": "^6.5.0",
|
"@nestjs/throttler": "^6.5.0",
|
||||||
"@nestjs/typeorm": "^11.0.0",
|
"@nestjs/typeorm": "^11.0.0",
|
||||||
|
"@openrouter/sdk": "^1.3.33",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.3",
|
"class-validator": "^0.14.3",
|
||||||
@@ -3086,6 +3087,15 @@
|
|||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@openrouter/sdk": {
|
||||||
|
"version": "1.3.33",
|
||||||
|
"resolved": "https://registry.npmjs.org/@openrouter/sdk/-/sdk-1.3.33.tgz",
|
||||||
|
"integrity": "sha512-MrrmDSxbHxX5XrhWlT+cfN1yMf8xu078LiQKtelazpQdHwrHSbyPBd+2y7rx77P3Uix+7uj8MTOX5lPrpx7nwQ==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"zod": "^3.25.0 || ^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@paralleldrive/cuid2": {
|
"node_modules/@paralleldrive/cuid2": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz",
|
||||||
@@ -12063,6 +12073,14 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zod": {
|
||||||
|
"version": "4.6.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz",
|
||||||
|
"integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
"@nestjs/swagger": "^11.2.6",
|
"@nestjs/swagger": "^11.2.6",
|
||||||
"@nestjs/throttler": "^6.5.0",
|
"@nestjs/throttler": "^6.5.0",
|
||||||
"@nestjs/typeorm": "^11.0.0",
|
"@nestjs/typeorm": "^11.0.0",
|
||||||
|
"@openrouter/sdk": "^1.3.33",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.3",
|
"class-validator": "^0.14.3",
|
||||||
|
|||||||
+10
-1
@@ -24,6 +24,8 @@ import { AdminModule } from './modules/admin/admin.module';
|
|||||||
import { AppThrottlerGuard } from './common/guards/app-throttler.guard';
|
import { AppThrottlerGuard } from './common/guards/app-throttler.guard';
|
||||||
import { ThrottlerModule } from '@nestjs/throttler';
|
import { ThrottlerModule } from '@nestjs/throttler';
|
||||||
import { LoggingModule } from './common/modules/logging/logging.module';
|
import { LoggingModule } from './common/modules/logging/logging.module';
|
||||||
|
import { ChatbotModule } from './modules/chatbot/chatbot.module';
|
||||||
|
import openRouterConfig from './config/open-router.config';
|
||||||
|
|
||||||
const ENV = process.env.NODE_ENV;
|
const ENV = process.env.NODE_ENV;
|
||||||
|
|
||||||
@@ -48,7 +50,13 @@ const ENV = process.env.NODE_ENV;
|
|||||||
ConfigModule.forRoot({
|
ConfigModule.forRoot({
|
||||||
isGlobal: true,
|
isGlobal: true,
|
||||||
envFilePath: !ENV ? '.env' : `.env.${ENV}`,
|
envFilePath: !ENV ? '.env' : `.env.${ENV}`,
|
||||||
load: [appConfig, databaseConfig, jwtConfig, redisConfig],
|
load: [
|
||||||
|
appConfig,
|
||||||
|
databaseConfig,
|
||||||
|
jwtConfig,
|
||||||
|
redisConfig,
|
||||||
|
openRouterConfig,
|
||||||
|
],
|
||||||
validationSchema: environmentValidation,
|
validationSchema: environmentValidation,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -97,6 +105,7 @@ const ENV = process.env.NODE_ENV;
|
|||||||
ScoringModule,
|
ScoringModule,
|
||||||
AdminModule,
|
AdminModule,
|
||||||
LoggingModule,
|
LoggingModule,
|
||||||
|
ChatbotModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,4 +23,6 @@ export default Joi.object({
|
|||||||
CORS_SUBDOMAIN: Joi.string().required(),
|
CORS_SUBDOMAIN: Joi.string().required(),
|
||||||
SWAGGER_SERVERS: Joi.string().required(),
|
SWAGGER_SERVERS: Joi.string().required(),
|
||||||
LOG_LEVEL: Joi.string().default('info'),
|
LOG_LEVEL: Joi.string().default('info'),
|
||||||
|
OPENROUTER_API_KEY: Joi.string().required(),
|
||||||
|
OPENROUTER_MODEL: Joi.string().required(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
|
export default registerAs('openRouter', () => ({
|
||||||
|
apiKey: process.env.OPENROUTER_API_KEY,
|
||||||
|
model: process.env.OPENROUTER_MODEL,
|
||||||
|
}));
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Body, Controller, Post } from '@nestjs/common';
|
||||||
|
import { ChatbotService } from './providers/chatbot.service';
|
||||||
|
import { Public } from '../auth/decorators/public.decorator';
|
||||||
|
|
||||||
|
@Controller('chatbot')
|
||||||
|
export class ChatbotController {
|
||||||
|
constructor(
|
||||||
|
/**
|
||||||
|
* Inject Chatbot Service
|
||||||
|
*/
|
||||||
|
private readonly chatbotService: ChatbotService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post('chat')
|
||||||
|
// TEMP:
|
||||||
|
@Public()
|
||||||
|
public async chat(@Body('message') message: string) {
|
||||||
|
return this.chatbotService.chat(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ChatbotController } from './chatbot.controller';
|
||||||
|
import { ChatbotService } from './providers/chatbot.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [ChatbotController],
|
||||||
|
providers: [ChatbotService],
|
||||||
|
})
|
||||||
|
export class ChatbotModule {}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
POST {{baseUrl}}/chatbot/chat
|
||||||
|
Content-Type: application/json
|
||||||
|
# Authorization: Bearer {{$global.accessToken}}
|
||||||
|
|
||||||
|
{
|
||||||
|
"message": "چطوری بفهمم به HPV مبتلا هستم یا خیر؟"
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ChatbotService {
|
||||||
|
private openRouter: any;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
/**
|
||||||
|
* Inject Config Service
|
||||||
|
*/
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private async getOpenRouter() {
|
||||||
|
if (!this.openRouter) {
|
||||||
|
const { OpenRouter } = await import('@openrouter/sdk');
|
||||||
|
|
||||||
|
this.openRouter = new OpenRouter({
|
||||||
|
apiKey: this.configService.get<string>('openRouter.apiKey'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
|
return this.openRouter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async chat(message: string) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
const openRouter = await this.getOpenRouter();
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||||
|
const response = await openRouter.chat.send({
|
||||||
|
chatRequest: {
|
||||||
|
model: this.configService.get<string>('openRouter.model'),
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: `
|
||||||
|
You are a medical AI assistant named Heala (Or in persian: هیلا).
|
||||||
|
|
||||||
|
Your role is to provide general medical information
|
||||||
|
and health education.
|
||||||
|
|
||||||
|
- Do not provide definitive diagnoses.
|
||||||
|
- Do not prescribe medications or dosages.
|
||||||
|
- Do not fabricate medical information.
|
||||||
|
- If symptoms may indicate an emergency, recommend
|
||||||
|
seeking urgent medical care.
|
||||||
|
- Clearly communicate uncertainty when appropriate.
|
||||||
|
- Answer in Persian when the user asks in Persian.
|
||||||
|
`.trim(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: message,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response instanceof ReadableStream) {
|
||||||
|
throw new Error('Expected non-streaming response');
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||||
|
const content = response.choices[0]?.message?.content;
|
||||||
|
|
||||||
|
if (typeof content !== 'string') {
|
||||||
|
throw new Error('OpenRouter returned an unexpected response');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: content,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user