This commit is contained in:
@@ -6,11 +6,12 @@ import { AiService } from './providers/ai.service';
|
||||
import { AiChatRepository } from './repositories/ai-chat.repository';
|
||||
import { AiChat } from './entities/ai-chat.entity';
|
||||
import { RestaurantsModule } from '../restaurants/restaurants.module';
|
||||
import { AiCrone } from './crone/ai.crone';
|
||||
|
||||
@Module({
|
||||
imports: [MikroOrmModule.forFeature([AiChat]), JwtModule, RestaurantsModule],
|
||||
controllers: [AiController],
|
||||
providers: [AiService, AiChatRepository],
|
||||
providers: [AiService, AiChatRepository, AiCrone],
|
||||
exports: [AiChatRepository],
|
||||
})
|
||||
export class AiModule {}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiBody, ApiHeader, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { Body, Controller, Get, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiBody, ApiHeader, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||
import { AiService } from '../providers/ai.service';
|
||||
import { ChatDto } from '../dto/chat.dto';
|
||||
import { FindAiChatsDto } from '../dto/find-ai-chats.dto';
|
||||
import { GenerateFoodDescriptionDto } from '../dto/generate-food-description.dto';
|
||||
import { GenerateFoodContentDto } from '../dto/generate-food-content.dto';
|
||||
import { OptionalAuthGuard } from '../../auth/guards/optinalAuth.guard';
|
||||
@@ -42,6 +43,20 @@ export class AiController {
|
||||
});
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Permissions(Permission.UPDATE_RESTAURANT)
|
||||
@Get('admin/ai/chats')
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Get paginated list of AI chats for the restaurant' })
|
||||
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||
@ApiQuery({ name: 'search', required: false, type: String })
|
||||
@ApiQuery({ name: 'orderBy', required: false, type: String })
|
||||
@ApiQuery({ name: 'order', required: false, enum: ['asc', 'desc'] })
|
||||
async findAllPaginated(@RestId() restId: string, @Query() dto: FindAiChatsDto) {
|
||||
return this.aiService.findAllPaginated(restId, dto);
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Permissions(Permission.MANAGE_FOODS)
|
||||
@Post('admin/ai/food-description')
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import { CreateRequestContext } from '@mikro-orm/core';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { AiChat } from '../entities/ai-chat.entity';
|
||||
|
||||
@Injectable()
|
||||
export class AiCrone {
|
||||
private readonly logger = new Logger(AiCrone.name);
|
||||
|
||||
constructor(private readonly em: EntityManager) {}
|
||||
|
||||
// run every day at 03:00 (3:00 AM)
|
||||
@Cron('0 3 * * *', {
|
||||
name: 'deleteOldAiChats',
|
||||
timeZone: 'UTC',
|
||||
})
|
||||
@CreateRequestContext()
|
||||
async handleCron() {
|
||||
try {
|
||||
this.logger.debug('Starting daily AI chat cleanup (removing questions older than 1 month)');
|
||||
|
||||
const oneMonthAgo = new Date();
|
||||
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
|
||||
|
||||
const oldAiChats = await this.em.find(AiChat, {
|
||||
createdAt: { $lt: oneMonthAgo },
|
||||
});
|
||||
|
||||
if (!oldAiChats || oldAiChats.length === 0) {
|
||||
this.logger.debug('No old AI chats found to delete');
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(`Found ${oldAiChats.length} AI chats older than 1 month to delete`);
|
||||
|
||||
await this.em.removeAndFlush(oldAiChats);
|
||||
|
||||
this.logger.log(`Successfully deleted ${oldAiChats.length} old AI chats`);
|
||||
} catch (err) {
|
||||
this.logger.error(`AiCrone failed: ${err?.message}`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { IsOptional, IsNumber, IsString, IsIn, Min } from 'class-validator';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
const sortOrderOptions = ['asc', 'desc'] as const;
|
||||
type SortOrder = (typeof sortOrderOptions)[number];
|
||||
|
||||
export class FindAiChatsDto {
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ example: 1, description: 'Page number', minimum: 1, default: 1 })
|
||||
page?: number = 1;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ example: 10, description: 'Items per page', minimum: 1, default: 10 })
|
||||
limit?: number = 10;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ example: 'گیاهخوار', description: 'Search in question text' })
|
||||
search?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ example: 'createdAt', description: 'Field to sort by', default: 'createdAt' })
|
||||
orderBy?: string = 'createdAt';
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(sortOrderOptions)
|
||||
@ApiPropertyOptional({ example: 'desc', enum: sortOrderOptions, default: 'desc' })
|
||||
order?: SortOrder = 'desc';
|
||||
}
|
||||
@@ -11,9 +11,11 @@ import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { AxiosError } from 'axios';
|
||||
import { catchError, firstValueFrom, throwError } from 'rxjs';
|
||||
import { ChatDto } from '../dto/chat.dto';
|
||||
import { FindAiChatsDto } from '../dto/find-ai-chats.dto';
|
||||
import { GenerateFoodDescriptionDto } from '../dto/generate-food-description.dto';
|
||||
import { GenerateFoodContentDto } from '../dto/generate-food-content.dto';
|
||||
import { AiChat } from '../entities/ai-chat.entity';
|
||||
import { AiChatRepository } from '../repositories/ai-chat.repository';
|
||||
import { AiChatCompletionResponse, AiChatResult } from '../interface/ai-chat.interface';
|
||||
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
|
||||
import { User } from '../../users/entities/user.entity';
|
||||
@@ -24,6 +26,7 @@ import {
|
||||
RestaurantCreditTransactionType,
|
||||
} from '../../restaurants/interface/restaurant-credit-transaction.interface';
|
||||
import { AiMessage, RestMessage } from 'src/common/enums/message.enum';
|
||||
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
|
||||
|
||||
@Injectable()
|
||||
export class AiService {
|
||||
@@ -34,8 +37,13 @@ export class AiService {
|
||||
private readonly httpService: HttpService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly restaurantWalletService: RestaurantWalletService,
|
||||
private readonly aiChatRepository: AiChatRepository,
|
||||
) { }
|
||||
|
||||
findAllPaginated(restId: string, dto: FindAiChatsDto): Promise<PaginatedResult<AiChat>> {
|
||||
return this.aiChatRepository.findAllPaginated(restId, dto);
|
||||
}
|
||||
|
||||
async chat(dto: ChatDto, params: { restId?: string; userId?: string; slug?: string }): Promise<AiChatResult> {
|
||||
const restaurant = await this.resolveRestaurant(params.restId, params.slug);
|
||||
const user = params.userId ? await this.em.findOne(User, { id: params.userId }) : null;
|
||||
@@ -82,7 +90,7 @@ export class AiService {
|
||||
'لحن باید جذاب، مختصر و مناسب منوی دیجیتال باشد.',
|
||||
].join('\n'),
|
||||
userPrompt: `نام غذا: ${dto.foodName}`,
|
||||
maxTokens: 150,
|
||||
maxTokens: 20000,
|
||||
temperature: 0.7,
|
||||
});
|
||||
|
||||
@@ -105,7 +113,7 @@ export class AiService {
|
||||
'مواد باید رایج، مختصر و مناسب منوی دیجیتال باشد.',
|
||||
].join('\n'),
|
||||
userPrompt: `نام غذا: ${dto.foodName}`,
|
||||
maxTokens: 150,
|
||||
maxTokens: 20000,
|
||||
temperature: 0.7,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,44 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||
import { FilterQuery } from '@mikro-orm/core';
|
||||
import { AiChat } from '../entities/ai-chat.entity';
|
||||
import { FindAiChatsDto } from '../dto/find-ai-chats.dto';
|
||||
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
|
||||
|
||||
@Injectable()
|
||||
export class AiChatRepository extends EntityRepository<AiChat> {
|
||||
constructor(readonly em: EntityManager) {
|
||||
super(em, AiChat);
|
||||
}
|
||||
|
||||
async findAllPaginated(restaurantId: string, dto: FindAiChatsDto): Promise<PaginatedResult<AiChat>> {
|
||||
const { page = 1, limit = 10, search, orderBy = 'createdAt', order = 'desc' } = dto;
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const where: FilterQuery<AiChat> = {
|
||||
restaurant: { id: restaurantId },
|
||||
};
|
||||
|
||||
if (search) {
|
||||
where.question = { $ilike: `%${search}%` };
|
||||
}
|
||||
|
||||
const [data, total] = await this.findAndCount(where, {
|
||||
limit,
|
||||
offset,
|
||||
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
|
||||
populate: ['user'],
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user