1
1
import json
2
2
import discord
3
3
from discord .ext import commands
4
- from dify_sdk import ChatClient
4
+ from dotenv import load_dotenv
5
+ import os
6
+ import aiohttp
5
7
6
- intents = discord .Intents .default ()
7
- intents .message_content = True
8
-
9
- bot = commands .Bot (command_prefix = '!' , intents = intents )
8
+ # Load environment variables
9
+ load_dotenv ()
10
10
11
+ DISCORD_BOT_TOKEN = os .getenv ('DISCORD_BOT_TOKEN' )
12
+ DIFY_API_KEY = os .getenv ('DIFY_API_KEY' )
13
+ DIFY_ENDPOINT = os .getenv ('DIFY_ENDPOINT' )
11
14
12
- DISCORD_BOT_TOKEN = 'MTMXXXXXXXXXXXXXXXXXXXXXXx'
13
- AGENT_DIFY_API_KEY = "app-XXXXXXXXXXXXXXXXXXXXXX"
14
- CHATBOT_DIFY_API_KEY = "app-XXXXXXXXXXXXXXXXXXXXXX"
15
- DIFY_ENDPOINT = "https://api.dify.ai/v1"
16
15
17
16
# 파일 카테고리별 확장자 정의
18
17
DIFY_FILE_CATEGORY_EXTENSIONS = {
29
28
DIFY_FILE_CATEGORIES = list (DIFY_FILE_CATEGORY_EXTENSIONS .keys ())
30
29
31
30
conversations_db = {}
31
+ intents = discord .Intents .default ()
32
+ intents .message_content = True
33
+
34
+ bot = commands .Bot (command_prefix = '!' , intents = intents )
32
35
33
36
34
37
# 파일 이름을 받아 해당 파일의 카테고리를 반환하는 함수
@@ -40,86 +43,113 @@ def get_dify_file_category(file_name: str) -> str:
40
43
return "custom" # 정의된 카테고리에 없는 경우 "custom" 반환
41
44
42
45
43
- @bot .command ()
44
- async def chat (ctx : commands .Context ):
45
- query = ctx .message .content .replace ("!chat " , "" )
46
- async with ctx .typing ():
47
- try :
48
- client = ChatClient (api_key = CHATBOT_DIFY_API_KEY ,
49
- base_url = DIFY_ENDPOINT )
50
- conversation_id = conversations_db .get (ctx .author .id , None )
51
- response = client .create_chat_message (
52
- inputs = {},
53
- query = query ,
54
- user = ctx .author .id ,
55
- response_mode = "blocking" ,
56
- conversation_id = conversation_id ,
57
- files = None
58
- )
59
- data = response .json ()
60
- conversations_db [ctx .author .id ] = data ["conversation_id" ]
61
- await ctx .send (data ["answer" ])
62
- except Exception as e :
63
- await ctx .send (f"Error: { e } " )
64
-
65
-
66
- @bot .command ()
67
- async def agent (ctx : commands .Context ):
68
- async with ctx .typing ():
69
- try :
70
- client = ChatClient (api_key = AGENT_DIFY_API_KEY ,
71
- base_url = DIFY_ENDPOINT )
72
- query = ctx .message .content .replace ("!agent " , "" )
73
- files = [
74
- {
75
- "url" : attachment .url ,
76
- "type" : get_dify_file_category (
77
- attachment .filename ),
78
- "transfer_method" : "remote_url"
46
+ @bot .event
47
+ async def on_ready ():
48
+ print (f'{ bot .user } 이 디스코드에 접속했습니다!' )
49
+
50
+
51
+ @bot .event
52
+ async def on_message (message : discord .Message ):
53
+ # 봇이 보낸 메시지는 무시
54
+ if message .author == bot .user :
55
+ return
56
+
57
+ # 봇이 멘션되었는지 확인
58
+ if bot .user .mentioned_in (message ):
59
+ # 멘션을 제거하고 메시지 내용만 추출
60
+ content = message .content .replace (f'<@{ bot .user .id } >' , '' ).strip ()
61
+
62
+ # 메시지 보내는 동안 타이핑 표시
63
+ async with message .channel .typing ():
64
+ try :
65
+ files = [
66
+ {
67
+ "url" : attachment .url ,
68
+ "type" : get_dify_file_category (attachment .filename ),
69
+ "transfer_method" : "remote_url"
70
+ }
71
+ for attachment in message .attachments
72
+ ]
73
+ # 메세지 작성자의 Conversation ID 참조, 첫 메세지일 경우 None
74
+ conversation_id = conversations_db .get (message .author .id , None )
75
+
76
+ # Dify API 호출
77
+ url = f"{ DIFY_ENDPOINT } /chat-messages"
78
+ headers = {
79
+ "Authorization" : f"Bearer { DIFY_API_KEY } " ,
80
+ "Content-Type" : "application/json"
81
+ }
82
+ payload = {
83
+ "inputs" : {},
84
+ "query" : content ,
85
+ "user" : message .author .id , # 메세지 작성자의 ID
86
+ "response_mode" : "streaming" ,
87
+ "conversation_id" : conversation_id , # 메세지 작성자의 Conversation ID
88
+ "files" : files # 첨부 파일 목록
79
89
}
80
- for attachment in ctx .message .attachments
81
- ]
82
- conversation_id = conversations_db .get (ctx .author .id , None )
83
- response = client .create_chat_message (
84
- inputs = {},
85
- query = query ,
86
- user = ctx .author .id ,
87
- response_mode = "streaming" , # dify agent 는 blocking 미지원
88
- conversation_id = conversation_id ,
89
- files = files
90
- )
91
- answer = ""
92
- for chunk in response .iter_lines ():
93
- if chunk :
94
- decoded_chunk = chunk .decode ("utf-8" )
95
- try :
96
- data = json .loads (decoded_chunk .strip ()[6 :])
97
- except json .decoder .JSONDecodeError :
98
- continue
99
- # print(data)
100
- match data ["event" ]:
101
- case "message" :
102
- answer += data ["answer" ]
103
- case "message_end" :
104
- conversations_db [ctx .author .id ] = data ["conversation_id" ]
105
- case "agent_message" :
106
- answer += data ["answer" ]
107
- case "agent_thought" :
108
- pass
109
- case "message_file" :
110
- await ctx .send (data ['url' ])
111
- case "error" :
112
- await ctx .send (f"Error: { data } " )
113
- case _:
114
- pass
115
- if answer :
116
- await ctx .send (answer )
117
- except Exception as e :
118
- await ctx .send (f"Error: { e } " )
90
+ answer = ""
91
+ async with aiohttp .ClientSession () as session :
92
+ async with session .post (url , json = payload , headers = headers ) as response :
93
+
94
+ # aiohttp 데이터 스트림 처리 (iter_lines 처럼 동작)
95
+ async for chunk in response .content :
96
+ # 빈 데이터는 무시
97
+ if not chunk :
98
+ continue
99
+
100
+ decoded_chunk = chunk .decode ("utf-8" )
101
+ try :
102
+ # SSE 이벤트 파싱
103
+ data = json .loads (decoded_chunk .strip ()[6 :])
104
+ except json .decoder .JSONDecodeError :
105
+ continue
106
+
107
+ # print(f"🔥 data: {data}")
108
+ match data ["event" ]:
109
+ case "message" :
110
+ # 메세지 이벤트의 경우 답변 추가
111
+ answer += data ["answer" ]
112
+ case "message_end" :
113
+ # 메세지 종료 이벤트의 경우 Conversation ID 저장
114
+ conversations_db [message .author .id ] = data ["conversation_id" ]
115
+ case "agent_message" :
116
+ # 에이전트 메세지 이벤트의 경우 답변 추가
117
+ answer += data ["answer" ]
118
+ case "agent_thought" :
119
+ print (f"🔥 agent_thought: { data } " )
120
+ pass
121
+ case "message_file" :
122
+ # 메세지 파일 이벤트의 경우 파일 URL 첨부
123
+ await message .channel .send (data ['url' ])
124
+ case "error" :
125
+ # 오류 이벤트의 경우 오류 메시지 출력
126
+ await message .channel .send (f"Error: { data } " )
127
+ case _:
128
+ pass
129
+
130
+ # 답변이 1000자 이상일 경우 1000자씩 잘라서 메시지 보내기
131
+ if len (answer ) > 1000 :
132
+ await message .channel .send (answer )
133
+ answer = ""
134
+
135
+ # 보내지 않은 답변이 남아있을 경우 마지막 메시지로 보내기
136
+ if answer :
137
+ await message .channel .send (answer )
138
+
139
+ except Exception as e :
140
+ import traceback
141
+ traceback .print_exc ()
142
+ await message .channel .send (f"Error: { e } " )
143
+
144
+ # 다른 명령어들을 처리하기 위해 이벤트를 계속 진행
145
+ await bot .process_commands (message )
119
146
120
147
121
148
@bot .command ()
122
149
async def clear (ctx : commands .Context ):
150
+ """!clear 명령어를 사용하면 대화 기록이 초기화됩니다."""
151
+
152
+ # 대화 기록 초기화
123
153
conversations_db .pop (ctx .author .id , None )
124
154
await ctx .send ("대화 기록이 초기화되었습니다." )
125
155
0 commit comments