11import { injectable } from 'tsyringe' ;
22import Anthropic from '@anthropic-ai/sdk' ;
3- import { ClaudeResponse , FeedDetail } from './common/types' ;
3+ import { ClaudeResponse , FeedAIQueueItem } from './common/types' ;
44import { TagMapRepository } from './repository/tag-map.repository' ;
55import { FeedRepository } from './repository/feed.repository' ;
66import logger from './common/logger' ;
7- import { PROMPT_CONTENT } from './common/constant' ;
7+ import { PROMPT_CONTENT , redisConstant } from './common/constant' ;
8+ import { RedisConnection } from './common/redis-access' ;
89
910@injectable ( )
1011export class ClaudeService {
1112 private readonly client : Anthropic ;
13+ private readonly nameTag : string ;
1214
1315 constructor (
1416 private readonly tagMapRepository : TagMapRepository ,
15- private readonly feedRepository : FeedRepository
17+ private readonly feedRepository : FeedRepository ,
18+ private readonly redisConnection : RedisConnection ,
1619 ) {
1720 this . client = new Anthropic ( {
1821 apiKey : process . env . AI_API_KEY ,
1922 } ) ;
23+ this . nameTag = '[AI Service]' ;
2024 }
2125
22- async useCaludeService ( feeds : FeedDetail [ ] ) {
23- const processedFeeds = await Promise . allSettled (
26+ async startRequestAI ( ) {
27+ const feedList : FeedAIQueueItem [ ] = await this . loadFeeds ( ) ;
28+ const feedListWithAI = await this . requestAI ( feedList ) ;
29+ await Promise . all ( [
30+ this . insertTag ( feedListWithAI ) ,
31+ this . updateSummary ( feedListWithAI ) ,
32+ ] ) ;
33+ }
34+
35+ private async loadFeeds ( ) {
36+ try {
37+ const redisSearchResult = await this . redisConnection . executePipeline (
38+ ( pipeline ) => {
39+ for ( let i = 0 ; i < parseInt ( process . env . AI_RATE_LIMIT_COUNT ) ; i ++ ) {
40+ pipeline . rpop ( redisConstant . FEED_AI_QUEUE ) ;
41+ }
42+ } ,
43+ ) ;
44+ const feedObjectList : FeedAIQueueItem [ ] = redisSearchResult
45+ . map ( ( result ) => JSON . parse ( result [ 1 ] as string ) )
46+ . filter ( ( value ) => value !== null ) ;
47+ return feedObjectList ;
48+ } catch ( error ) {
49+ logger . error ( `${ this . nameTag } Redis 로드한 데이터 JSON Parse 중 오류 발생:
50+ 메시지: ${ error . message }
51+ 스택 트레이스: ${ error . stack }
52+ ` ) ;
53+ }
54+ }
55+
56+ private async requestAI ( feeds : FeedAIQueueItem [ ] ) {
57+ const feedsWithAIData = await Promise . all (
2458 feeds . map ( async ( feed ) => {
2559 try {
60+ logger . info ( `${ this . nameTag } AI 요청: ${ JSON . stringify ( feed ) } ` ) ;
2661 const params : Anthropic . MessageCreateParams = {
2762 max_tokens : 8192 ,
2863 system : PROMPT_CONTENT ,
@@ -31,92 +66,62 @@ export class ClaudeService {
3166 } ;
3267 const message = await this . client . messages . create ( params ) ;
3368 let responseText : string = message . content [ 0 ] [ 'text' ] ;
34- responseText = responseText . replace ( / \n / g, '' ) ;
35- const result : ClaudeResponse = JSON . parse ( responseText ) ;
36-
37- await Promise . all ( [
38- this . generateTag ( feed , result [ 'tags' ] ) ,
39- this . summarize ( feed , result [ 'summary' ] ) ,
40- ] ) ;
41- return {
42- succeeded : true ,
43- feed,
44- } ;
69+ responseText = responseText . replace ( / [ \n \r \t \s ] + / g, ' ' ) ;
70+ logger . info (
71+ `${ this . nameTag } ${ feed . id } AI 요청 응답: ${ responseText } ` ,
72+ ) ;
73+ const responseObject : ClaudeResponse = JSON . parse ( responseText ) ;
74+ feed . summary = responseObject . summary ;
75+ feed . tagList = Object . keys ( responseObject . tags ) ;
76+ return feed ;
4577 } catch ( error ) {
4678 logger . error (
47- `${ feed . id } 의 태그 생성, 컨텐츠 요약 에러 발생: ` ,
48- error
79+ `${ this . nameTag } ${ feed . id } 의 태그 생성, 컨텐츠 요약 에러 발생:
80+ 메시지: ${ error . message }
81+ 스택 트레이스: ${ error . stack } ` ,
4982 ) ;
50- return {
51- succeeded : false ,
52- feed,
53- } ;
54- }
55- } )
56- ) ;
57-
58- // TODO: Refactor
59- const successFeeds = processedFeeds
60- . map ( ( result ) =>
61- result . status === 'fulfilled' && result . value . succeeded === true
62- ? result . value . feed
63- : null
64- )
65- . filter ( ( result ) => result !== null ) ;
6683
67- // TODO: Refactor
68- const failedFeeds = processedFeeds
69- . map ( ( result , index ) => {
70- if ( result . status === 'rejected' ) {
71- const failedFeed = feeds [ index ] ;
72- return {
73- succeeded : false ,
74- feed : failedFeed ,
75- } ;
84+ if ( feed . deathCount < 3 ) {
85+ feed . deathCount ++ ;
86+ this . redisConnection . rpush ( redisConstant . FEED_AI_QUEUE , [
87+ JSON . stringify ( feed ) ,
88+ ] ) ;
89+ } else {
90+ logger . error (
91+ `${ this . nameTag } ${ feed . id } 의 Death Count 3회 이상 발생 AI 요청 금지:
92+ 메시지: ${ error . message }
93+ 스택 트레이스: ${ error . stack } ` ,
94+ ) ;
95+ this . feedRepository . updateNullSummary ( feed . id ) ;
96+ }
7697 }
77- return result . status === 'fulfilled' && result . value . succeeded === false
78- ? result . value
79- : null ;
80- } )
81- . filter ( ( result ) => result !== null && result . succeeded === false )
82- . map ( ( result ) => result . feed ) ;
83-
84- logger . info (
85- `${ successFeeds . length } 개의 태그 생성 및 컨텐츠 요약이 성공했습니다.\n ${ failedFeeds . length } 개의 태그 생성 및 컨텐츠 요약이 실패했습니다.`
98+ } ) ,
8699 ) ;
87-
88- return [ ...successFeeds , ...failedFeeds ] ;
100+ return feedsWithAIData . filter ( ( value ) => value !== undefined ) ;
89101 }
90102
91- private async generateTag ( feed : FeedDetail , tags : Record < string , number > ) {
92- try {
93- const tagList = Object . keys ( tags ) ;
94- if ( tagList . length === 0 ) return ;
95- await this . tagMapRepository . insertTags ( feed . id , tagList ) ;
96- feed . tag = tagList ;
97- } catch ( error ) {
98- logger . error (
99- `[DB] 태그 데이터를 저장하는 도중 에러가 발생했습니다.
100- 에러 메시지: ${ error . message }
101- 스택 트레이스: ${ error . stack } `
102- ) ;
103- }
104- }
105-
106- private async summarize ( feed : FeedDetail , summary : string ) {
107- try {
108- await this . feedRepository . insertSummary ( feed . id , summary ) ;
109- feed . summary = summary ;
110- } catch ( error ) {
111- logger . error (
112- `[DB] 게시글 요약 데이터를 저장하는 도중 에러가 발생했습니다.
113- 에러 메시지: ${ error . message }
114- 스택 트레이스: ${ error . stack } `
115- ) ;
116- }
103+ private insertTag ( feedWithAIList : FeedAIQueueItem [ ] ) {
104+ return feedWithAIList . map ( async ( feed ) => {
105+ try {
106+ await this . tagMapRepository . insertTags ( feed . id , feed . tagList ) ;
107+ await this . redisConnection . hset (
108+ `feed:recent:${ feed . id } ` ,
109+ 'tag' ,
110+ feed . tagList . join ( ',' ) ,
111+ ) ;
112+ } catch ( error ) {
113+ logger . error (
114+ `${ this . nameTag } ${ feed . id } 의 태그 저장 중 에러 발생:
115+ 메시지: ${ error . message }
116+ 스택 트레이스: ${ error . stack } ` ,
117+ ) ;
118+ }
119+ } ) ;
117120 }
118121
119- public async saveAiQueue ( feeds : FeedDetail [ ] ) {
120- await this . feedRepository . saveAiQueue ( feeds ) ;
122+ private updateSummary ( feedWithAIList : FeedAIQueueItem [ ] ) {
123+ return feedWithAIList . map ( ( feed ) =>
124+ this . feedRepository . updateSummary ( feed . id , feed . summary ) ,
125+ ) ;
121126 }
122127}
0 commit comments