1
1
import { injectable } from 'tsyringe' ;
2
2
import Anthropic from '@anthropic-ai/sdk' ;
3
- import { ClaudeResponse , FeedDetail } from './common/types' ;
3
+ import { ClaudeResponse , FeedAIQueueItem } from './common/types' ;
4
4
import { TagMapRepository } from './repository/tag-map.repository' ;
5
5
import { FeedRepository } from './repository/feed.repository' ;
6
6
import 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' ;
8
9
9
10
@injectable ( )
10
11
export class ClaudeService {
11
12
private readonly client : Anthropic ;
13
+ private readonly nameTag : string ;
12
14
13
15
constructor (
14
16
private readonly tagMapRepository : TagMapRepository ,
15
- private readonly feedRepository : FeedRepository
17
+ private readonly feedRepository : FeedRepository ,
18
+ private readonly redisConnection : RedisConnection ,
16
19
) {
17
20
this . client = new Anthropic ( {
18
21
apiKey : process . env . AI_API_KEY ,
19
22
} ) ;
23
+ this . nameTag = '[AI Service]' ;
20
24
}
21
25
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 (
24
58
feeds . map ( async ( feed ) => {
25
59
try {
60
+ logger . info ( `${ this . nameTag } AI 요청: ${ JSON . stringify ( feed ) } ` ) ;
26
61
const params : Anthropic . MessageCreateParams = {
27
62
max_tokens : 8192 ,
28
63
system : PROMPT_CONTENT ,
@@ -31,92 +66,62 @@ export class ClaudeService {
31
66
} ;
32
67
const message = await this . client . messages . create ( params ) ;
33
68
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 ;
45
77
} catch ( error ) {
46
78
logger . error (
47
- `${ feed . id } 의 태그 생성, 컨텐츠 요약 에러 발생: ` ,
48
- error
79
+ `${ this . nameTag } ${ feed . id } 의 태그 생성, 컨텐츠 요약 에러 발생:
80
+ 메시지: ${ error . message }
81
+ 스택 트레이스: ${ error . stack } ` ,
49
82
) ;
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 ) ;
66
83
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
+ }
76
97
}
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
+ } ) ,
86
99
) ;
87
-
88
- return [ ...successFeeds , ...failedFeeds ] ;
100
+ return feedsWithAIData . filter ( ( value ) => value !== undefined ) ;
89
101
}
90
102
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
+ } ) ;
117
120
}
118
121
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
+ ) ;
121
126
}
122
127
}
0 commit comments