Implements discord.py Bot Gateway to receive @agent mentions and enqueue tasks. Includes deterministic thread ID generation and message parsing for issue numbers, repo names, and freeform requests. Supports message queuing for running threads. Files: - agent/integrations/discord_handler.py: DiscordHandler class with event handling - tests/test_discord_handler.py: 4 tests for parsing and thread ID generation All tests pass (95 total).
121 lines
4.1 KiB
Python
121 lines
4.1 KiB
Python
# agent/integrations/discord_handler.py
|
|
"""Discord Bot Gateway 수신 핸들러.
|
|
|
|
discord.py를 사용하여 @agent 멘션을 수신하고 작업을 큐에 추가한다.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import logging
|
|
import os
|
|
import re
|
|
|
|
import discord
|
|
from discord.ext import commands
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def parse_discord_message(content: str, bot_user_id: int) -> dict:
|
|
"""Discord 메시지를 파싱하여 작업 정보를 추출한다."""
|
|
# 봇 멘션 제거 (<@123456>)
|
|
cleaned = re.sub(rf"<@!?{bot_user_id}>", "", content).strip()
|
|
|
|
# 이슈 번호 추출
|
|
issue_match = re.search(r"#(\d+)", cleaned)
|
|
issue_number = int(issue_match.group(1)) if issue_match else 0
|
|
|
|
# 리포 이름 추출
|
|
repo_name = os.environ.get("DEFAULT_REPO_NAME", "galaxis-po")
|
|
repo_match = re.search(r"\b(galaxis-\w+)\b", cleaned)
|
|
if repo_match:
|
|
repo_name = repo_match.group(1)
|
|
|
|
# 순수 메시지
|
|
message = re.sub(r"@agent\b", "", cleaned, flags=re.IGNORECASE)
|
|
message = re.sub(rf"\b{re.escape(repo_name)}\b", "", message)
|
|
message = re.sub(r"이슈\s*#\d+", "", message)
|
|
message = re.sub(r"#\d+", "", message)
|
|
message = message.strip()
|
|
|
|
return {
|
|
"issue_number": issue_number,
|
|
"repo_name": repo_name,
|
|
"message": message or cleaned,
|
|
}
|
|
|
|
|
|
def generate_discord_thread_id(channel_id: int, message_id: int) -> str:
|
|
"""Discord 메시지에서 결정론적 스레드 ID를 생성한다."""
|
|
raw = hashlib.sha256(f"discord:{channel_id}:{message_id}".encode()).hexdigest()
|
|
return f"{raw[:8]}-{raw[8:12]}-{raw[12:16]}-{raw[16:20]}-{raw[20:32]}"
|
|
|
|
|
|
class DiscordHandler:
|
|
"""Discord Bot Gateway 핸들러."""
|
|
|
|
def __init__(self):
|
|
intents = discord.Intents.default()
|
|
intents.message_content = True
|
|
intents.guild_messages = True
|
|
self.bot = commands.Bot(command_prefix="!", intents=intents)
|
|
self._setup_events()
|
|
|
|
def _setup_events(self):
|
|
@self.bot.event
|
|
async def on_ready():
|
|
logger.info("Discord bot connected as %s", self.bot.user)
|
|
|
|
@self.bot.event
|
|
async def on_message(message: discord.Message):
|
|
if message.author == self.bot.user:
|
|
return
|
|
if not self.bot.user or not self.bot.user.mentioned_in(message):
|
|
return
|
|
await self._handle_mention(message)
|
|
|
|
async def _handle_mention(self, message: discord.Message):
|
|
"""@agent 멘션을 처리한다."""
|
|
parsed = parse_discord_message(
|
|
message.content, self.bot.user.id if self.bot.user else 0
|
|
)
|
|
thread_id = generate_discord_thread_id(message.channel.id, message.id)
|
|
repo_owner = os.environ.get("DEFAULT_REPO_OWNER", "quant")
|
|
|
|
from agent.task_queue import get_task_queue
|
|
from agent.message_store import get_message_store
|
|
|
|
task_queue = await get_task_queue()
|
|
|
|
if parsed["issue_number"] and await task_queue.has_running_task(thread_id):
|
|
store = await get_message_store()
|
|
await store.push_message(thread_id, {
|
|
"role": "human",
|
|
"content": parsed["message"],
|
|
})
|
|
await message.reply("메시지를 대기열에 추가했습니다. 현재 작업이 완료되면 확인하겠습니다.")
|
|
return
|
|
|
|
task_id = await task_queue.enqueue(
|
|
thread_id=thread_id,
|
|
source="discord",
|
|
payload={
|
|
"issue_number": parsed["issue_number"],
|
|
"repo_owner": repo_owner,
|
|
"repo_name": parsed["repo_name"],
|
|
"message": parsed["message"],
|
|
"channel_id": str(message.channel.id),
|
|
"message_id": str(message.id),
|
|
},
|
|
)
|
|
await message.reply(f"작업을 대기열에 추가했습니다. (task: {task_id[:8]})")
|
|
logger.info("Discord task enqueued: %s (thread %s)", task_id, thread_id)
|
|
|
|
async def start(self, token: str):
|
|
"""Bot Gateway를 시작한다."""
|
|
await self.bot.start(token)
|
|
|
|
async def close(self):
|
|
"""Bot Gateway를 종료한다."""
|
|
await self.bot.close()
|