Files
variet-agent/handlers/debate_handler.py

281 lines
9.1 KiB
Python

"""Debate Room Handler — AG 인스턴스 간 토론 중재.
#variet-debate 채널에서 !debate-start로 시작.
사회자(이 봇)가 #ag-debate_gemini와 #ag-debate_opus 사이에서 턴을 관리.
"""
import asyncio
import logging
import random
from dataclasses import dataclass, field
from typing import Optional
import discord
logger = logging.getLogger("variet.debate")
# ── 채널 ID ──
DEBATE_CONTROL_CH = 1484167157620146227 # #variet-debate
DEBATE_AGENTS = {
"gemini": 1484156194187771905, # #ag-debate_gemini
"opus": 1484156521209401476, # #ag-debate_opus
}
# Embed 색상
AGENT_COLORS = {
"gemini": 0x2ECC71, # 초록
"opus": 0x3498DB, # 파랑
}
AGENT_EMOJI = {
"gemini": "🟢",
"opus": "🔵",
}
MAX_ROUNDS = 5
RESPONSE_TIMEOUT = 180 # AG 응답 대기 (초)
@dataclass
class DebateSession:
topic: str = ""
round: int = 0
max_rounds: int = MAX_ROUNDS
active: bool = False
current_speaker: str = ""
history: list = field(default_factory=list)
_response_event: Optional[asyncio.Event] = field(default=None, repr=False)
_last_response: str = ""
class DebateHandler:
def __init__(self, bot: discord.ext.commands.Bot):
self.bot = bot
self.session = DebateSession()
# ── 공개 API ──
async def start(self, ctx, topic: str):
"""토론 시작."""
if self.session.active:
await ctx.reply("⚠️ 이미 진행 중인 토론이 있습니다. `!debate-stop`으로 먼저 종료하세요.")
return
self.session = DebateSession(
topic=topic,
active=True,
max_rounds=MAX_ROUNDS,
)
# 시작 알림
embed = discord.Embed(
title="🏛️ Debate Room 시작",
description=f"**주제**: {topic}\n**최대 라운드**: {MAX_ROUNDS}\n**참여자**: 🟢 Gemini · 🔵 Opus",
color=0x9B59B6,
)
control_ch = self.bot.get_channel(DEBATE_CONTROL_CH)
if control_ch:
await control_ch.send(embed=embed)
# 첫 턴 시작
await self._next_turn()
async def stop(self, ctx):
"""토론 중단."""
if not self.session.active:
await ctx.reply("진행 중인 토론이 없습니다.")
return
self.session.active = False
await self._post_conclusion(ctx.channel)
async def inject(self, ctx, opinion: str):
"""사용자 의견 삽입."""
if not self.session.active:
await ctx.reply("진행 중인 토론이 없습니다.")
return
self.session.history.append({
"speaker": "user",
"content": opinion,
})
embed = discord.Embed(
title="👤 사용자 의견",
description=opinion,
color=0xF39C12,
)
control_ch = self.bot.get_channel(DEBATE_CONTROL_CH)
if control_ch:
await control_ch.send(embed=embed)
# 다음 턴에 반영
await self._next_turn(user_injected=opinion)
async def on_agent_message(self, message: discord.Message):
"""AG 채널에서 응답 수신 시 호출."""
if not self.session.active:
return
if not self.session._response_event:
return
# 어떤 에이전트가 응답했는지 확인
agent_name = None
for name, ch_id in DEBATE_AGENTS.items():
if message.channel.id == ch_id:
agent_name = name
break
if not agent_name:
return
# 현재 대기 중인 에이전트의 응답만 처리
if agent_name != self.session.current_speaker:
return
# 응답 저장 (여러 메시지 합치기 - 마지막 메시지로)
self.session._last_response = message.content
self.session._response_event.set()
# ── 내부 로직 ──
async def _next_turn(self, user_injected: str = ""):
"""다음 턴 실행."""
if not self.session.active:
return
self.session.round += 1
if self.session.round > self.session.max_rounds:
control_ch = self.bot.get_channel(DEBATE_CONTROL_CH)
if control_ch:
await control_ch.send("⏹️ 최대 라운드 도달. 토론을 종료합니다.")
await self._post_conclusion(control_ch)
return
# 발언자 선택 (랜덤, 단 직전과 다른 에이전트)
agents = list(DEBATE_AGENTS.keys())
if self.session.current_speaker:
agents.remove(self.session.current_speaker)
speaker = random.choice(agents)
self.session.current_speaker = speaker
# 프롬프트 구성
prompt = self._build_prompt(speaker, user_injected)
# 진행 상태 표시
control_ch = self.bot.get_channel(DEBATE_CONTROL_CH)
if control_ch:
await control_ch.send(
f"{AGENT_EMOJI[speaker]} **Round {self.session.round}** — "
f"`{speaker}` 발언 대기 중..."
)
# AG 채널에 프롬프트 전송
agent_ch = self.bot.get_channel(DEBATE_AGENTS[speaker])
if not agent_ch:
logger.error(f"토론 채널 접근 불가: {speaker}")
return
await agent_ch.send(prompt)
# 응답 대기
self.session._response_event = asyncio.Event()
self.session._last_response = ""
try:
# 첫 응답 후 추가 메시지가 있을 수 있으므로 약간의 딜레이
await asyncio.wait_for(
self.session._response_event.wait(),
timeout=RESPONSE_TIMEOUT,
)
# AG가 여러 메시지를 보낼 수 있으므로 잠시 대기
await asyncio.sleep(5)
except asyncio.TimeoutError:
if control_ch:
await control_ch.send(f"⏰ `{speaker}` 응답 시간 초과 ({RESPONSE_TIMEOUT}초)")
return
response = self.session._last_response
if not response:
return
# 응답 기록
self.session.history.append({
"speaker": speaker,
"content": response,
})
# #variet-debate에 게시
if control_ch:
embed = discord.Embed(
title=f"{AGENT_EMOJI[speaker]} Round {self.session.round}{speaker}",
description=response[:4000],
color=AGENT_COLORS[speaker],
)
if len(response) > 4000:
embed.add_field(
name="(계속)",
value=response[4000:4000+1024] + "...",
inline=False,
)
await control_ch.send(embed=embed)
# 사용자에게 계속 진행 여부 물어보기
await control_ch.send(
f"**다음 턴으로 진행할까요?**\n"
f"✅ `!debate-next` — 계속\n"
f"✏️ `!debate-inject 의견` — 의견 추가 후 계속\n"
f"⏹️ `!debate-stop` — 종료"
)
def _build_prompt(self, speaker: str, user_injected: str = "") -> str:
"""에이전트에게 보낼 프롬프트 구성."""
other = "opus" if speaker == "gemini" else "gemini"
parts = [f"## 토론 주제: {self.session.topic}\n"]
parts.append(f"현재 라운드: {self.session.round}/{self.session.max_rounds}\n")
# 직전 발언 포함
prev = [h for h in self.session.history if h["speaker"] != "user"]
if prev:
last = prev[-1]
parts.append(f"### 상대방({last['speaker']})의 직전 발언:\n")
parts.append(last["content"])
parts.append("\n\n---\n")
parts.append(
f"위 발언에 오류가 없는지 확인하고, "
f"더 개선된 방향이나 잘못된 부분을 지적하세요.\n"
)
# 사용자 의견
if user_injected:
parts.append(f"\n### 사용자 의견:\n{user_injected}\n")
parts.append("사용자의 의견을 반영하여 답변하세요.\n")
if not prev and not user_injected:
parts.append(
"당신이 첫 번째 발언자입니다. "
"주제에 대해 자유롭게 의견을 제시하세요.\n"
)
return "\n".join(parts)
async def _post_conclusion(self, channel):
"""토론 종료 — 결론 정리."""
self.session.active = False
summary_parts = []
for h in self.session.history:
emoji = AGENT_EMOJI.get(h["speaker"], "👤")
content = h["content"][:300]
summary_parts.append(f"{emoji} **{h['speaker']}**: {content}...")
embed = discord.Embed(
title="🏛️ 토론 종료",
description=f"**주제**: {self.session.topic}\n"
f"**라운드**: {self.session.round}\n\n"
+ "\n\n".join(summary_parts[-6:]), # 최근 6개만
color=0x9B59B6,
)
await channel.send(embed=embed)