ai question list
deploy to danak / build_and_deploy (push) Has been cancelled

This commit is contained in:
2026-07-23 11:41:09 +03:30
parent 27c45974f4
commit c439fdd6f9
6 changed files with 144 additions and 5 deletions
+2 -1
View File
@@ -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 {}
+17 -2
View File
@@ -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')
+44
View File
@@ -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);
}
}
}
+37
View File
@@ -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';
}
+10 -2
View File
@@ -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),
},
};
}
}