Skip to content

Commit a8a41ff

Browse files
authored
Merge pull request #353 from Quickchive/feat/QA-346/add-category-v2
feat: add auto categories v2
2 parents 5626724 + e4255ed commit a8a41ff

File tree

7 files changed

+144
-14
lines changed

7 files changed

+144
-14
lines changed

src/categories/category.module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Category } from './category.entity';
88
import { CategoryController } from './category.controller';
99
import { ContentRepository } from '../contents/repository/content.repository';
1010
import { CategoryRepository } from './category.repository';
11+
import { CategoryV2Controller } from './v2/category.v2.controller';
1112

1213
@Module({
1314
imports: [
@@ -16,7 +17,7 @@ import { CategoryRepository } from './category.repository';
1617
OpenaiModule,
1718
UsersModule,
1819
],
19-
controllers: [CategoryController],
20+
controllers: [CategoryController, CategoryV2Controller],
2021
providers: [CategoryService, ContentRepository, CategoryRepository],
2122
exports: [CategoryRepository],
2223
})

src/categories/category.repository.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,4 +135,12 @@ export class CategoryRepository extends Repository<Category> {
135135
relations: ['contents'],
136136
});
137137
}
138+
139+
async findByUserId(userId: number): Promise<Category[]> {
140+
return await this.find({
141+
where: {
142+
user: { id: userId },
143+
},
144+
});
145+
}
138146
}

src/categories/category.service.ts

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
Injectable,
33
NotFoundException,
44
ConflictException,
5+
InternalServerErrorException,
56
} from '@nestjs/common';
67
import { EntityManager } from 'typeorm';
78
import {
@@ -212,9 +213,7 @@ export class CategoryService {
212213
queryRunnerManager: EntityManager,
213214
): Promise<DeleteCategoryOutput> {
214215
try {
215-
const category = await this.categoryRepository.findOne({
216-
where: { id: categoryId },
217-
});
216+
const category = await this.categoryRepository.findById(categoryId);
218217
if (!category) {
219218
throw new NotFoundException('Category not found.');
220219
}
@@ -476,6 +475,70 @@ export class CategoryService {
476475
}
477476
}
478477

478+
async autoCategorizeWithId(user: User, link: string) {
479+
try {
480+
const categories = await this.categoryRepository.findByUserId(user.id);
481+
if (categories.length === 0) {
482+
throw new NotFoundException('Categories not found');
483+
}
484+
485+
const { title, siteName, description } = await getLinkInfo(link);
486+
487+
const content = await getLinkContent(link);
488+
489+
const question = `You are a machine tasked with auto-categorizing articles based on information obtained through web scraping.
490+
You can only answer a single category name. Here is the article's information:
491+
<title>${title && `title: "${title.trim()}"`}</title>
492+
<content>${
493+
content &&
494+
`content: "${content.replace(/\s/g, '').slice(0, 300).trim()}"`
495+
}</content>
496+
<description>${
497+
description && `description: "${description.trim()}"`
498+
}</description>
499+
<siteName>${siteName && `site name: "${siteName.trim()}"`}</siteName>
500+
Please provide the most suitable category among the following. Here is Category options: [${[
501+
...categories,
502+
'None',
503+
].join(', ')}]
504+
505+
Given the following categories, please provide the most suitable category for the article.
506+
<categories>${categories
507+
.map((category) =>
508+
JSON.stringify({ id: category.id, name: category.name }),
509+
)
510+
.join('\n')}</categories>
511+
512+
Present your reply options in JSON format below.
513+
\`\`\`json
514+
{
515+
"id": id,
516+
"name": "category name"
517+
}
518+
\`\`\`
519+
`;
520+
521+
const response = await this.openaiService.createChatCompletion({
522+
question,
523+
temperature: 0,
524+
responseType: 'json',
525+
});
526+
527+
const categoryStr = response.choices[0].message?.content;
528+
529+
if (categoryStr) {
530+
const { id, name } = JSON.parse(
531+
categoryStr.replace(/^```json|```$/g, '').trim(),
532+
);
533+
return { category: { id, name } };
534+
}
535+
536+
throw new InternalServerErrorException('Failed to categorize');
537+
} catch (e) {
538+
throw e;
539+
}
540+
}
541+
479542
async autoCategorizeForTest(
480543
autoCategorizeBody: AutoCategorizeBodyDto,
481544
): Promise<AutoCategorizeOutput> {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
2+
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
3+
import { JwtAuthGuard } from '../../auth/jwt/jwt.guard';
4+
import { CategoryService } from '../category.service';
5+
import { AuthUser } from '../../auth/auth-user.decorator';
6+
import { User } from '../../users/entities/user.entity';
7+
import { RecommendedCategoryResponseDto } from './dto/recommended-category-response.dto';
8+
9+
@Controller('v2/categories')
10+
@ApiTags('Category v2')
11+
@ApiBearerAuth()
12+
@UseGuards(JwtAuthGuard)
13+
export class CategoryV2Controller {
14+
constructor(private readonly categoryService: CategoryService) {}
15+
16+
@ApiOperation({
17+
summary: '아티클 카테고리 자동 지정',
18+
description:
19+
'아티클에 적절한 카테고리를 유저의 카테고리 목록에서 찾는 메서드',
20+
})
21+
@UseGuards(JwtAuthGuard)
22+
@Get('auto-categorize')
23+
async autoCategorize(@AuthUser() user: User, @Query('link') link: string) {
24+
const { category } = await this.categoryService.autoCategorizeWithId(
25+
user,
26+
link,
27+
);
28+
29+
return new RecommendedCategoryResponseDto(category);
30+
}
31+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
3+
export class RecommendedCategoryResponseDto {
4+
@ApiProperty({
5+
description: '카테고리 id',
6+
})
7+
id: number;
8+
9+
@ApiProperty({
10+
description: '카테고리 이름',
11+
})
12+
name: string;
13+
14+
constructor({ id, name }: { id: number; name: string }) {
15+
this.id = id;
16+
this.name = name;
17+
}
18+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import { ResponseType } from 'axios';
2+
13
export class CreateCompletionBodyDto {
24
question!: string;
35
model?: string;
46
temperature?: number;
7+
responseType?: ResponseType;
58
}

src/openai/openai.service.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,24 @@ export class OpenaiService {
1919
question,
2020
model,
2121
temperature,
22+
responseType,
2223
}: CreateCompletionBodyDto): Promise<CreateChatCompletionResponse> {
2324
try {
24-
const { data } = await this.openAIApi.createChatCompletion({
25-
model: model || 'gpt-4o-mini',
26-
messages: [
27-
{
28-
role: 'user',
29-
content: question,
30-
},
31-
],
32-
temperature: temperature || 0.1,
33-
});
25+
const { data } = await this.openAIApi.createChatCompletion(
26+
{
27+
model: model || 'gpt-4o-mini',
28+
messages: [
29+
{
30+
role: 'user',
31+
content: question,
32+
},
33+
],
34+
temperature: temperature || 0.1,
35+
},
36+
{
37+
...(responseType && { responseType }),
38+
},
39+
);
3440

3541
return data;
3642
} catch (e) {

0 commit comments

Comments
 (0)