feat: add chatbot module and service

This commit is contained in:
2026-09-27 17:45:11 +03:30
parent 17b47ffd91
commit 76f6c97a9e
9 changed files with 150 additions and 1 deletions
+10 -1
View File
@@ -24,6 +24,8 @@ import { AdminModule } from './modules/admin/admin.module';
import { AppThrottlerGuard } from './common/guards/app-throttler.guard';
import { ThrottlerModule } from '@nestjs/throttler';
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;
@@ -48,7 +50,13 @@ const ENV = process.env.NODE_ENV;
ConfigModule.forRoot({
isGlobal: true,
envFilePath: !ENV ? '.env' : `.env.${ENV}`,
load: [appConfig, databaseConfig, jwtConfig, redisConfig],
load: [
appConfig,
databaseConfig,
jwtConfig,
redisConfig,
openRouterConfig,
],
validationSchema: environmentValidation,
}),
@@ -97,6 +105,7 @@ const ENV = process.env.NODE_ENV;
ScoringModule,
AdminModule,
LoggingModule,
ChatbotModule,
],
providers: [
{
+2
View File
@@ -23,4 +23,6 @@ export default Joi.object({
CORS_SUBDOMAIN: Joi.string().required(),
SWAGGER_SERVERS: Joi.string().required(),
LOG_LEVEL: Joi.string().default('info'),
OPENROUTER_API_KEY: Joi.string().required(),
OPENROUTER_MODEL: Joi.string().required(),
});
+6
View File
@@ -0,0 +1,6 @@
import { registerAs } from '@nestjs/config';
export default registerAs('openRouter', () => ({
apiKey: process.env.OPENROUTER_API_KEY,
model: process.env.OPENROUTER_MODEL,
}));
+20
View File
@@ -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);
}
}
+9
View File
@@ -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,
};
}
}