Skip to content

Commit f4f0b5e

Browse files
committed
내용 보완
1 parent 49ba0c1 commit f4f0b5e

File tree

12 files changed

+338
-666
lines changed

12 files changed

+338
-666
lines changed

.env.example

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Discord Bot Token
2+
DISCORD_BOT_TOKEN=MTMXXXXXXXXXXXXXX.XXXXXXXX.XXXXXXXX
3+
4+
# Dify API Keys
5+
DIFY_API_KEY=app-XXXXXXXXXXXXXXXXXXXXXX
6+
DIFY_ENDPOINT=https://api.dify.ai/v1
7+
8+
# LangGraph API Keys
9+
# docker 로 실행할 경우 localhost 를 host.docker.internal 로 변경
10+
LANGGRAPH_ENDPOINT=http://localhost:2024
11+
LANGGRAPH_API_KEY=
12+
LANGGRAPH_ASSISTANT_ID=agent

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
__pycache__
22
.venv
3+
.env

Dockerfile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
FROM python:3.12-slim
2+
3+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
4+
5+
WORKDIR /app
6+
7+
COPY . .
8+
9+
RUN uv sync

README.md

Lines changed: 85 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,104 @@
1-
## 1. discord developer 페이지 접속
1+
# 🧵 Dify & LangGraph Platform Discord Bot Usecase
2+
3+
## 기능 미리보기
4+
5+
- 멀티턴 대화
6+
![](assets/10.png)
7+
8+
- typing 표시
9+
![](assets/11.png)
10+
11+
- 기본적인 마크다운 표시
12+
![](assets/12.png)
13+
14+
- 이미지 업로드 가능
15+
![](assets/13.png)
16+
17+
- 이미지 결과물 표시 (Dify)
18+
![](assets/14.png)
19+
20+
## 프로젝트 구조
21+
22+
```mermaid
23+
graph LR
24+
A[유저의 멘션] --> B[discord bot 프로그램]
25+
B -->|Dify API 요청| C[Dify]
26+
C -->|응답 결과| B
27+
B -->|유저에게 답변| A
28+
```
29+
30+
## 앱 설정
31+
32+
현재 디렉토리에 `.env` 파일을 생성하고 `.env.example`의 내용을 복사하여 다음 내용을 설정하세요:
33+
34+
```
35+
# 하단의 [Discord 봇 설정 방법] 을 참고해주세요
36+
DISCORD_BOT_TOKEN=MTMXXXXXXXXXXXXXX.XXXXXXXX.XXXXXXXX
37+
38+
# Dify API Keys : Dify 예시를 사용하려면 이 값을 설정해주세요
39+
DIFY_API_KEY=app-XXXXXXXXXXXXXXXXXXXXXX
40+
DIFY_ENDPOINT=https://api.dify.ai/v1
41+
42+
# LangGraph API Keys : LangGraph 예시를 사용하려면 이 값을 설정해주세요
43+
# docker 로 실행할 경우 localhost 를 host.docker.internal 로 변경
44+
LANGGRAPH_ENDPOINT=http://localhost:2024
45+
LANGGRAPH_API_KEY=
46+
LANGGRAPH_ASSISTANT_ID=agent
47+
```
48+
49+
## 실행 방법
50+
51+
### 1. 바로 실행
52+
53+
```bash
54+
# 의존성 설치
55+
uv sync
56+
```
57+
58+
```bash
59+
# 실행
60+
uv run dify_example.py
61+
```
62+
63+
### 2. docker compose 사용
64+
65+
```bash
66+
docker compose up --build -d
67+
```
68+
69+
langgraph 앱을 실행하고 싶으면 `docker-compose.yml`
70+
`dify_example.py``langgraph_example.py` 로 바꿔주세요.
71+
72+
## Discord 봇 설정 방법
73+
74+
### 1. discord developer 페이지 접속
275

376
https://discord.com/developers/applications
477
![](assets/1.png)
578

6-
## 2. application 생성
79+
### 2. application 생성
780

881
![](assets/2.png)
982

10-
## 3. 생성 후 Bot 탭에서 Reset Token
83+
### 3. 생성 후 Bot 탭에서 Reset Token
1184

1285
![](assets/3.png)
1386

1487
토큰 복사해두기
1588
![](assets/4.png)
1689

17-
## 4. 같은 Bot 탭에서 Message Content Intent 활성화 후 저장
90+
### 4. 같은 Bot 탭에서 Message Content Intent 활성화 후 저장
1891

92+
메세지 내용을 읽을 수 있게 하기 위해 필요한 설정입니다.
1993
![](assets/5.png)
2094

21-
## 5. OAuth2 탭에서 URL 생성
22-
23-
![](assets/6.png)
24-
![](assets/7.png)
25-
![](assets/8.png)
26-
27-
## 6. 생성된 URL 로 채널에 봇 추가
28-
29-
![](assets/9.png)
30-
31-
## 7. 추가된 봇과 대화
32-
33-
멀티턴 대화
34-
![](assets/10.png)
95+
### 5. Installation 탭 설정 & 생성된 URL 로 접속
3596

36-
typing 표시
37-
![](assets/11.png)
97+
- Guild Install Settings 의 SCOPES 에 bot 추가 (봇으로 동작하게 하기 위해 필요한 스코프입니다.)
98+
- Guild Install Settings 의 PERMISSIONS 에 Send Messages 추가 (서버에 메세지를 보내기 위해 필요한 권한입니다.)
99+
![](assets/15.png)
100+
![](assets/9.png)
38101

39-
기본적인 마크다운 표시
40-
![](assets/12.png)
102+
### 7. 추가된 봇과 대화
41103

42-
이미지 업로드 가능
43-
![](assets/13.png)
104+
![](assets/14.png)

assets/14.png

458 KB
Loading

assets/15.png

367 KB
Loading

dify_example.py

Lines changed: 114 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
import json
22
import discord
33
from discord.ext import commands
4-
from dify_sdk import ChatClient
4+
from dotenv import load_dotenv
5+
import os
6+
import aiohttp
57

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()
1010

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')
1114

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"
1615

1716
# 파일 카테고리별 확장자 정의
1817
DIFY_FILE_CATEGORY_EXTENSIONS = {
@@ -29,6 +28,10 @@
2928
DIFY_FILE_CATEGORIES = list(DIFY_FILE_CATEGORY_EXTENSIONS.keys())
3029

3130
conversations_db = {}
31+
intents = discord.Intents.default()
32+
intents.message_content = True
33+
34+
bot = commands.Bot(command_prefix='!', intents=intents)
3235

3336

3437
# 파일 이름을 받아 해당 파일의 카테고리를 반환하는 함수
@@ -40,86 +43,113 @@ def get_dify_file_category(file_name: str) -> str:
4043
return "custom" # 정의된 카테고리에 없는 경우 "custom" 반환
4144

4245

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 # 첨부 파일 목록
7989
}
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)
119146

120147

121148
@bot.command()
122149
async def clear(ctx: commands.Context):
150+
"""!clear 명령어를 사용하면 대화 기록이 초기화됩니다."""
151+
152+
# 대화 기록 초기화
123153
conversations_db.pop(ctx.author.id, None)
124154
await ctx.send("대화 기록이 초기화되었습니다.")
125155

0 commit comments

Comments
 (0)