feat(debate): v2 구현 — 자동 루프, 파일 기반 I/O, Flash 합의 판정, !debate-status

This commit is contained in:
2026-03-20 06:19:46 +09:00
parent 0508d7be8c
commit 2c3998a036
3 changed files with 376 additions and 212 deletions

View File

@@ -1260,18 +1260,30 @@ async def debate_stop(ctx: commands.Context):
await _debate_handler.stop(ctx) await _debate_handler.stop(ctx)
@bot.command(name="debate-next") @bot.command(name="debate-status")
async def debate_next(ctx: commands.Context): async def debate_status(ctx: commands.Context):
"""다음 턴 진행.""" """토론 진행 상태 확인."""
if not _debate_handler.session.active: s = _debate_handler.session
if not s.active:
await ctx.reply("진행 중인 토론이 없습니다.") await ctx.reply("진행 중인 토론이 없습니다.")
return return
await _debate_handler._next_turn() status = "⏸️ 사용자 응답 대기" if s.paused else "▶️ 자동 진행 중"
embed = discord.Embed(
title="🏛️ Debate Status",
description=(
f"**주제**: {s.topic}\n"
f"**라운드**: {s.round}/{s.max_rounds}\n"
f"**상태**: {status}\n"
f"**현재 발언자**: {s.current_speaker or '-'}"
),
color=0x9B59B6,
)
await ctx.reply(embed=embed)
@bot.command(name="debate-inject") @bot.command(name="debate-inject")
async def debate_inject(ctx: commands.Context, *, opinion: str = ""): async def debate_inject(ctx: commands.Context, *, opinion: str = ""):
"""사용자 의견 삽입 후 다음 턴. 사용법: !debate-inject 의견""" """사용자 의견 삽입. 사용법: !debate-inject 의견"""
if not opinion: if not opinion:
await ctx.reply("⚠️ 의견을 입력하세요: `!debate-inject 의견`") await ctx.reply("⚠️ 의견을 입력하세요: `!debate-inject 의견`")
return return

View File

@@ -1,16 +1,19 @@
"""Debate Room Handler — AG 인스턴스 간 토론 중재. """Debate Room Handler v2 — 파일 기반 자동 토론.
흐름: 흐름:
1. AG가 로컬 response.md에 전문 작성 1. 사회자가 input.md + wiki/ 동기화
2. Discord"작성 완료" 시그널 2. Discord 시그널로 AG에게 알림
3. 사회자(봇)가 response.md 읽기 → NC transcript에 append → response.md 비우기 3. AG가 response.md에 전문 작성
4. Discord #debate에 요약 게시 4. Discord 시그널 감지 → response.md 읽기
5. 상대 AG 채널에 지시 전달 5. 합의 판정 (Flash)
6. 자동 반복 or 사용자 질문 or 종료
""" """
import asyncio import asyncio
import json
import logging import logging
import random import random
import time
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
@@ -22,8 +25,8 @@ logger = logging.getLogger("variet.debate")
# ── 채널 ID ── # ── 채널 ID ──
DEBATE_CONTROL_CH = 1484167157620146227 # #variet-debate DEBATE_CONTROL_CH = 1484167157620146227 # #variet-debate
DEBATE_AGENTS = { DEBATE_AGENTS = {
"gemini": 1484156194187771905, # #ag-debate_gemini "gemini": 1484156194187771905,
"opus": 1484156521209401476, # #ag-debate_opus "opus": 1484156521209401476,
} }
# ── 로컬 프로젝트 경로 ── # ── 로컬 프로젝트 경로 ──
@@ -32,71 +35,71 @@ AGENT_PATHS = {
"opus": Path(r"C:\Users\Variet-Worker\Desktop\debate_opus"), "opus": Path(r"C:\Users\Variet-Worker\Desktop\debate_opus"),
} }
# Embed 색상 / 이모지
AGENT_COLORS = {"gemini": 0x2ECC71, "opus": 0x3498DB} AGENT_COLORS = {"gemini": 0x2ECC71, "opus": 0x3498DB}
AGENT_EMOJI = {"gemini": "🟢", "opus": "🔵"} AGENT_EMOJI = {"gemini": "🟢", "opus": "🔵"}
MAX_ROUNDS = 5 MAX_ROUNDS = 10
RESPONSE_TIMEOUT = 300 # 긴 답변 대기 (5분) RESPONSE_TIMEOUT = 300 # 5분
FILE_POLL_INTERVAL = 5 # 파일 확인 간격 (초) FILE_CHECK_INTERVAL = 3 # 파일 확인 간격 (초)
FILE_STABLE_DELAY = 5 # 파일 작성 완료 대기 (초)
@dataclass @dataclass
class DebateSession: class DebateSession:
topic: str = "" topic: str = ""
topic_short: str = "" # 파일명용 약어
round: int = 0 round: int = 0
max_rounds: int = MAX_ROUNDS max_rounds: int = MAX_ROUNDS
active: bool = False active: bool = False
paused: bool = False # 사용자 응답 대기 중
current_speaker: str = "" current_speaker: str = ""
history: list = field(default_factory=list) history: list = field(default_factory=list)
pending_question: str = "" # 사용자에게 보낸 질문
class DebateHandler: class DebateHandler:
def __init__(self, bot: discord.ext.commands.Bot): def __init__(self, bot: discord.ext.commands.Bot):
self.bot = bot self.bot = bot
self.session = DebateSession() self.session = DebateSession()
self._debate_task: Optional[asyncio.Task] = None
# ── 공개 API ── # ═══════════════════════════════════════════
# 공개 API
# ═══════════════════════════════════════════
async def start(self, ctx, topic: str): async def start(self, ctx, topic: str):
"""토론 시작.""" """토론 시작 — 자동 루프 개시."""
if self.session.active: if self.session.active:
await ctx.reply("⚠️ 이미 진행 중인 토론이 있습니다. `!debate-stop`으로 먼저 종료하세요.") await ctx.reply("⚠️ 이미 진행 중. `!debate-stop`으로 먼저 종료.")
return return
# 약어 생성 (첫 10자, 공백은 _)
short = topic[:10].replace(" ", "_").replace("/", "_")
self.session = DebateSession( self.session = DebateSession(
topic=topic, topic=topic, active=True, max_rounds=MAX_ROUNDS,
topic_short=short,
active=True,
max_rounds=MAX_ROUNDS,
) )
# 각 AG 프로젝트에 response.md 초기화 # 폴더 초기화
for name, path in AGENT_PATHS.items(): for name, path in AGENT_PATHS.items():
resp_file = path / "response.md" (path / "response.md").write_text("", encoding="utf-8")
resp_file.write_text("", encoding="utf-8") (path / "input.md").write_text("", encoding="utf-8")
wiki_dir = path / "wiki"
wiki_dir.mkdir(exist_ok=True)
# 시작 알림 # 시작 알림
embed = discord.Embed( ctrl = self.bot.get_channel(DEBATE_CONTROL_CH)
title="🏛️ Debate Room 시작", if ctrl:
description=( embed = discord.Embed(
f"**주제**: {topic}\n" title="🏛️ Debate Room 시작",
f"**최대 라운드**: {MAX_ROUNDS}\n" description=(
f"**참여자**: 🟢 Gemini · 🔵 Opus\n\n" f"**주제**: {topic}\n"
f"AG는 `response.md`에 답변을 작성합니다." f"**최대 라운드**: {MAX_ROUNDS}\n"
), f"**참여자**: 🟢 Gemini · 🔵 Opus\n"
color=0x9B59B6, f"자동 진행 — 합의 시 또는 의견 필요 시 알립니다."
) ),
control_ch = self.bot.get_channel(DEBATE_CONTROL_CH) color=0x9B59B6,
if control_ch: )
await control_ch.send(embed=embed) await ctrl.send(embed=embed)
# 첫 턴 # 자동 루프 시작
await self._next_turn() self._debate_task = asyncio.create_task(self._auto_loop())
async def stop(self, ctx): async def stop(self, ctx):
"""토론 중단.""" """토론 중단."""
@@ -104,187 +107,355 @@ class DebateHandler:
await ctx.reply("진행 중인 토론이 없습니다.") await ctx.reply("진행 중인 토론이 없습니다.")
return return
self.session.active = False self.session.active = False
if self._debate_task:
self._debate_task.cancel()
await self._post_conclusion(ctx.channel) await self._post_conclusion(ctx.channel)
async def inject(self, ctx, opinion: str): async def inject(self, ctx, opinion: str):
"""사용자 의견 삽입.""" """사용자 의견 삽입 + 진행 재개."""
if not self.session.active: if not self.session.active:
await ctx.reply("진행 중인 토론이 없습니다.") await ctx.reply("진행 중인 토론이 없습니다.")
return return
self.session.history.append({"speaker": "user", "content": opinion}) self.session.history.append({"speaker": "user", "content": opinion})
embed = discord.Embed( ctrl = self.bot.get_channel(DEBATE_CONTROL_CH)
title="👤 사용자 의견", description=opinion, color=0xF39C12, if ctrl:
) embed = discord.Embed(
control_ch = self.bot.get_channel(DEBATE_CONTROL_CH) title="👤 사용자 의견 삽입",
if control_ch: description=opinion[:500],
await control_ch.send(embed=embed) color=0xF39C12,
)
await ctrl.send(embed=embed)
await self._next_turn(user_injected=opinion) # 일시정지 상태였으면 재개
if self.session.paused:
self.session.paused = False
self.session.pending_question = ""
async def on_agent_message(self, message: discord.Message): async def on_agent_message(self, message: discord.Message):
"""AG 채널에서 메시지 수신 — response.md 파일 완성 시그널로 사용.""" """AG 채널 메시지 감지 — response.md 체크 트리거."""
if not self.session.active: # 별도 처리 불필요 — auto_loop에서 파일 폴링으로 처리
return pass
# 어떤 에이전트인지 확인 # ═══════════════════════════════════════════
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 or agent_name != self.session.current_speaker:
return
# response.md에 내용이 있는지 확인 async def _auto_loop(self):
resp_file = AGENT_PATHS[agent_name] / "response.md" """합의까지 자동 반복하는 메인 루프."""
if resp_file.exists(): try:
content = resp_file.read_text(encoding="utf-8").strip() while self.session.active:
if content: # 일시정지 상태면 대기
await self._process_response(agent_name, content) while self.session.paused:
await asyncio.sleep(2)
if not self.session.active:
return
# ── 내부 로직 ── self.session.round += 1
if self.session.round > self.session.max_rounds:
ctrl = self.bot.get_channel(DEBATE_CONTROL_CH)
if ctrl:
await ctrl.send("⏹️ 최대 라운드 도달.")
await self._post_conclusion(ctrl)
return
async def _next_turn(self, user_injected: str = ""): # 발언자 선택 (교대)
"""다음 턴.""" speaker = self._pick_speaker()
if not self.session.active:
return
self.session.round += 1 # ① input.md 작성 + wiki/ 동기화
if self.session.round > self.session.max_rounds: await self._prepare_input(speaker)
control_ch = self.bot.get_channel(DEBATE_CONTROL_CH)
if control_ch:
await control_ch.send("⏹️ 최대 라운드 도달.")
await self._post_conclusion(control_ch)
return
# 발언자 선택 (직전과 다른 에이전트) # ② Discord 시그널 전송
await self._send_signal(speaker)
# ③ response.md 대기
response = await self._wait_for_response(speaker)
if not response:
continue
# ④ 기록
self.session.history.append({
"speaker": speaker, "content": response,
})
# ⑤ #debate에 요약 게시
await self._post_summary(speaker, response)
# ⑥ 합의 판정
decision = await self._judge_consensus(speaker, response)
if decision == "conclude":
ctrl = self.bot.get_channel(DEBATE_CONTROL_CH)
if ctrl:
await ctrl.send("✅ **양측 합의 도달!** Wiki에 최종 기록합니다.")
await self._post_conclusion(ctrl)
return
elif decision == "ask_user":
self.session.paused = True
ctrl = self.bot.get_channel(DEBATE_CONTROL_CH)
if ctrl:
await ctrl.send(
f"❓ **사용자 의견 필요**\n"
f"{self.session.pending_question}\n\n"
f"`!debate-inject 의견` 으로 응답해주세요."
)
# paused — 루프 상단에서 대기
# continue — 다음 턴 자동 진행
except asyncio.CancelledError:
logger.info("토론 루프 취소됨")
except Exception as e:
logger.error(f"토론 루프 오류: {e}", exc_info=True)
ctrl = self.bot.get_channel(DEBATE_CONTROL_CH)
if ctrl:
await ctrl.send(f"⚠️ 토론 오류: {str(e)[:200]}")
# ═══════════════════════════════════════════
# 턴 관리
# ═══════════════════════════════════════════
def _pick_speaker(self) -> str:
"""교대 발언자 선택."""
agents = list(DEBATE_AGENTS.keys()) agents = list(DEBATE_AGENTS.keys())
if self.session.current_speaker: if self.session.current_speaker:
agents.remove(self.session.current_speaker) agents.remove(self.session.current_speaker)
speaker = random.choice(agents) speaker = random.choice(agents)
self.session.current_speaker = speaker self.session.current_speaker = speaker
return speaker
# response.md 비우기 async def _prepare_input(self, speaker: str):
resp_file = AGENT_PATHS[speaker] / "response.md" """사회자가 input.md + wiki/ 작성."""
resp_file.write_text("", encoding="utf-8") # 사회자(Flash) 프롬프트 생성
moderated_prompt = await self._build_moderator_prompt(speaker)
# 사회자(Flash)가 프롬프트 생 # input.md 작
prompt = await self._build_prompt(speaker, user_injected) input_path = AGENT_PATHS[speaker] / "input.md"
input_path.write_text(moderated_prompt, encoding="utf-8")
# 진행 상태 표시 # wiki/ 동기화 (TODO: Wiki.js에서 가져와서 로컬에 쓰기)
control_ch = self.bot.get_channel(DEBATE_CONTROL_CH) # 현재는 히스토리 기반으로 working_document 생성
if control_ch: wiki_dir = AGENT_PATHS[speaker] / "wiki"
await control_ch.send( wiki_dir.mkdir(exist_ok=True)
f"{AGENT_EMOJI[speaker]} **Round {self.session.round}** — " working_doc = self._build_working_document()
f"`{speaker}` 답변 대기 중...\n" (wiki_dir / "working_document.md").write_text(
f"📝 `response.md`에 작성 후 Discord에 알려주세요." working_doc, encoding="utf-8",
) )
# AG 채널에 프롬프트 전송 (분할) async def _send_signal(self, speaker: str):
agent_ch = self.bot.get_channel(DEBATE_AGENTS[speaker]) """AG에게 Discord 시그널."""
if not agent_ch: ch = self.bot.get_channel(DEBATE_AGENTS[speaker])
logger.error(f"토론 채널 접근 불가: {speaker}") if not ch:
return return
rd = self.session.round
await ch.send(
f"📥 **Round {rd}** — `input.md`를 읽고 `response.md`에 답변을 작성하세요."
)
# 2000자씩 분할 전송 async def _wait_for_response(self, speaker: str) -> str:
for i in range(0, len(prompt), 1900): """response.md에 내용이 채워질 때까지 폴링."""
await agent_ch.send(prompt[i:i+1900]) resp_path = AGENT_PATHS[speaker] / "response.md"
async def _process_response(self, speaker: str, content: str):
"""AG 응답 처리 — 기록 + 게시 + 파일 비우기."""
# 히스토리에 추가
self.session.history.append({
"speaker": speaker,
"content": content,
})
# response.md 비우기 # response.md 비우기
resp_file = AGENT_PATHS[speaker] / "response.md" resp_path.write_text("", encoding="utf-8")
resp_file.write_text("", encoding="utf-8")
# NC에 transcript 저장 (TODO: nc_client 연동) ctrl = self.bot.get_channel(DEBATE_CONTROL_CH)
# await self._append_to_nc_transcript(speaker, content) if ctrl:
await ctrl.send(
# #variet-debate에 요약 게시 f"{AGENT_EMOJI[speaker]} **Round {self.session.round}** — "
control_ch = self.bot.get_channel(DEBATE_CONTROL_CH) f"`{speaker}` 답변 대기 중..."
if control_ch:
# 요약 (첫 500자)
summary = content[:500]
if len(content) > 500:
summary += f"\n\n... (전문 {len(content)}자)"
embed = discord.Embed(
title=f"{AGENT_EMOJI[speaker]} Round {self.session.round}{speaker}",
description=summary,
color=AGENT_COLORS[speaker],
)
await control_ch.send(embed=embed)
await control_ch.send(
f"**다음 턴?**\n"
f"✅ `!debate-next` — 계속\n"
f"✏️ `!debate-inject 의견` — 의견 추가 후 계속\n"
f"⏹️ `!debate-stop` — 종료"
) )
async def _build_prompt(self, speaker: str, user_injected: str = "") -> str: start = time.time()
"""사회자(Flash)가 에이전트에게 보낼 프롬프트 생성.""" last_size = 0
stable_since = 0
while time.time() - start < RESPONSE_TIMEOUT:
if not self.session.active:
return ""
await asyncio.sleep(FILE_CHECK_INTERVAL)
if not resp_path.exists():
continue
content = resp_path.read_text(encoding="utf-8").strip()
if not content:
continue
current_size = len(content)
if current_size == last_size and current_size > 0:
# 파일 크기 안정 — 작성 완료로 판단
if stable_since == 0:
stable_since = time.time()
elif time.time() - stable_since >= FILE_STABLE_DELAY:
return content
else:
last_size = current_size
stable_since = 0
# 타임아웃
if ctrl:
await ctrl.send(f"⏰ `{speaker}` 응답 시간 초과 ({RESPONSE_TIMEOUT}초)")
return ""
# ═══════════════════════════════════════════
# 사회자 로직 (Flash)
# ═══════════════════════════════════════════
async def _build_moderator_prompt(self, speaker: str) -> str:
"""사회자(Flash)가 에이전트에게 보낼 input 생성."""
other = "opus" if speaker == "gemini" else "gemini" other = "opus" if speaker == "gemini" else "gemini"
prev = [h for h in self.session.history if h["speaker"] != "user"]
last_opponent = prev[-1] if prev else None
user_msgs = [h for h in self.session.history if h["speaker"] == "user"]
last_user = user_msgs[-1] if user_msgs else None
prev_opinions = [h for h in self.session.history if h["speaker"] != "user"] meta = f"""당신은 AI 토론 사회자입니다.
last_opponent = prev_opinions[-1] if prev_opinions else None
user_opinions = [h for h in self.session.history if h["speaker"] == "user"]
last_user = user_opinions[-1] if user_opinions else None
moderator_prompt = f"""당신은 AI 토론 사회자입니다. ## 상황
- 주제: {self.session.topic}
## 현재 상황
- 토론 주제: {self.session.topic}
- 라운드: {self.session.round}/{self.session.max_rounds} - 라운드: {self.session.round}/{self.session.max_rounds}
- 다음 발언자: {speaker} - 발언자: {speaker} / 상대: {other}
- 상대: {other}
""" """
if last_opponent: if last_opponent:
moderator_prompt += f""" meta += f"\n## 상대방({last_opponent['speaker']}) 직전 발언 (전문):\n{last_opponent['content']}\n"
## 상대방({last_opponent['speaker']})의 직전 발언 (전문):
{last_opponent['content']}
"""
if last_user:
moderator_prompt += f"""
## 사용자의 의견:
{last_user['content']}
"""
if user_injected:
moderator_prompt += f"""
## 사용자 추가 의견:
{user_injected}
"""
moderator_prompt += f"""
## 지시
다음 발언자({speaker})에게 보낼 메시지를 작성하세요.
규칙: if last_user:
1. 상대 발언이 있으면 **전문 그대로 포함** meta += f"\n## 사용자 의견:\n{last_user['content']}\n"
2. 사용자의 어떤 판단에 기반하는지 설명
3. 오류/논리적 허점 확인 + 개선 방향 지시 meta += f"""
## 지시
발언자({speaker})에게 보낼 메시지를 작성하세요.
1. 상대 발언이 있으면 **전문 포함** (요약 금지)
2. 사용자의 판단이 있으면 맥락 설명
3. 오류/허점 확인 + 개선 방향 지시
4. 핵심 쟁점 방향 제시 4. 핵심 쟁점 방향 제시
5. 첫 발언이면 주제 설명 + 자유 의견 안내 5. 첫 발언이면 주제 설명 + 자유 의견 안내
6. **중요: 답변 response.md 파일에 작성하라고 안내** 6. **답변 response.md에 작성하라고 안내**
7. **참고할 합의 사항은 wiki/working_document.md에 있다고 안내**
(사회자 메시지만 출력) (사회자 메시지만 출력)
""" """
try: try:
from core.gemini_caller import GeminiCaller from core.gemini_caller import GeminiCaller
caller = GeminiCaller() caller = GeminiCaller()
return await caller.call_simple(moderator_prompt, timeout=60) return await caller.call_simple(meta, timeout=90)
except Exception as e: except Exception as e:
logger.error(f"Flash 호출 실패: {e}") logger.error(f"Flash 호출 실패: {e}")
return self._build_fallback_prompt(speaker, user_injected) return self._fallback_prompt(speaker)
def _build_fallback_prompt(self, speaker: str, user_injected: str = "") -> str: async def _judge_consensus(self, speaker: str, response: str) -> str:
"""합의 판정 — continue / ask_user / conclude."""
prev = [h for h in self.session.history if h["speaker"] != "user"]
if len(prev) < 2:
return "continue"
judge_prompt = f"""당신은 AI 토론 심판입니다.
## 토론 주제: {self.session.topic}
## 라운드: {self.session.round}/{self.session.max_rounds}
## 최근 2개 발언:
### {prev[-2]['speaker']}:
{prev[-2]['content'][:2000]}
### {prev[-1]['speaker']}:
{prev[-1]['content'][:2000]}
## 판정 기준:
- 양쪽이 핵심 사항에서 동의하고 있으면 → conclude
- 한쪽 또는 양쪽이 사용자 의견을 구하고 있으면 → ask_user
- 아직 대립 중이면 → continue
반드시 아래 JSON만 출력:
{{"decision": "continue|ask_user|conclude", "reason": "판정 근거", "question": "사용자에게 물을 질문 (ask_user일 때)"}}
"""
try:
from core.gemini_caller import GeminiCaller
caller = GeminiCaller()
raw = await caller.call_simple(judge_prompt, timeout=60)
# JSON 파싱
raw = raw.strip()
if "```" in raw:
raw = raw.split("```")[1]
if raw.startswith("json"):
raw = raw[4:]
raw = raw.strip()
result = json.loads(raw)
decision = result.get("decision", "continue")
if decision == "ask_user":
self.session.pending_question = result.get("question", "의견을 주세요.")
logger.info(f"합의 판정: {decision}{result.get('reason', '')}")
return decision
except Exception as e:
logger.warning(f"합의 판정 실패: {e}, continue로 처리")
return "continue"
# ═══════════════════════════════════════════
# 출력
# ═══════════════════════════════════════════
async def _post_summary(self, speaker: str, content: str):
"""#debate에 요약 게시."""
ctrl = self.bot.get_channel(DEBATE_CONTROL_CH)
if not ctrl:
return
summary = content[:800]
if len(content) > 800:
summary += f"\n\n... *(전문 {len(content)}자 — response.md 참조)*"
embed = discord.Embed(
title=f"{AGENT_EMOJI[speaker]} Round {self.session.round}{speaker}",
description=summary,
color=AGENT_COLORS[speaker],
)
await ctrl.send(embed=embed)
async def _post_conclusion(self, channel):
"""토론 종료."""
self.session.active = False
parts = []
for h in self.session.history[-8:]:
emoji = AGENT_EMOJI.get(h["speaker"], "👤")
parts.append(f"{emoji} **{h['speaker']}**: {h['content'][:200]}...")
embed = discord.Embed(
title="🏛️ 토론 종료",
description=(
f"**주제**: {self.session.topic}\n"
f"**라운드**: {self.session.round}\n\n"
+ "\n\n".join(parts)
),
color=0x9B59B6,
)
await channel.send(embed=embed)
# ═══════════════════════════════════════════
# 유틸
# ═══════════════════════════════════════════
def _build_working_document(self) -> str:
"""현재까지 합의/논의 사항을 working document로 생성."""
lines = [f"# {self.session.topic} — Working Document\n"]
lines.append(f"라운드: {self.session.round}/{self.session.max_rounds}\n")
lines.append("---\n")
for h in self.session.history:
emoji = AGENT_EMOJI.get(h["speaker"], "👤")
lines.append(f"## {emoji} {h['speaker']} (Round {self.session.history.index(h)+1})")
lines.append(h["content"][:3000])
lines.append("\n---\n")
return "\n".join(lines)
def _fallback_prompt(self, speaker: str) -> str:
"""Flash 실패 시 기본 프롬프트.""" """Flash 실패 시 기본 프롬프트."""
parts = [f"## 토론 주제: {self.session.topic}"] parts = [f"## 토론 주제: {self.session.topic}"]
parts.append(f"라운드: {self.session.round}/{self.session.max_rounds}\n") parts.append(f"라운드: {self.session.round}/{self.session.max_rounds}\n")
@@ -294,33 +465,9 @@ class DebateHandler:
last = prev[-1] last = prev[-1]
parts.append(f"### 상대방({last['speaker']})의 발언:\n{last['content']}") parts.append(f"### 상대방({last['speaker']})의 발언:\n{last['content']}")
parts.append("\n---\n오류를 확인하고 개선점을 지적하세요.") parts.append("\n---\n오류를 확인하고 개선점을 지적하세요.")
else:
if user_injected:
parts.append(f"\n### 사용자 의견:\n{user_injected}")
if not prev and not user_injected:
parts.append("첫 번째 발언자입니다. 주제에 대해 의견을 제시하세요.") parts.append("첫 번째 발언자입니다. 주제에 대해 의견을 제시하세요.")
parts.append("\n\n**답변은 response.md 파일에 작성하세요.**") parts.append("\n\n**답변은 response.md에 작성하세요.**")
parts.append("**합의 사항은 wiki/working_document.md를 참고하세요.**")
return "\n".join(parts) 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:])
),
color=0x9B59B6,
)
await channel.send(embed=embed)

View File

@@ -2,22 +2,26 @@
당신은 AI 토론 참여자입니다. 사회자의 지시에 따라 주어진 주제에 대해 의견을 제시하고, 상대방의 주장을 검증합니다. 당신은 AI 토론 참여자입니다. 사회자의 지시에 따라 주어진 주제에 대해 의견을 제시하고, 상대방의 주장을 검증합니다.
## ⚠️ 답변 방식 (필수) ## ⚠️ 파일 기반 답변 (필수)
**답변은 반드시 프로젝트 루트의 `response.md` 파일에 작성하세요.** ### 읽을 파일
- **`input.md`** — 사회자가 작성한 상대 의견 + 방향 지시. **반드시 먼저 읽으세요.**
- **`wiki/working_document.md`** — 현재까지의 합의 사항. 참고용.
- Discord에는 "답변을 response.md에 작성했습니다"라고만 게시 ### 쓸 파일
- response.md에는 길이 제한 없이 전문을 작성 - **`response.md`** — 여기에 전문 답변을 작성하세요.
- "이하생략", "나머지는 생략" 절대 금지 — 전부 작성 - Discord에는 "response.md에 작성 완료" 한 줄만 게시.
- **"이하생략" 절대 금지** — 전부 작성.
## 행동 규칙 ## 행동 규칙
1. **사회자 지시 우선** — 사회자가 보내는 메시지에 주제, 상대 의견, 방향이 포함됩니다 1. **input.md 먼저 읽기** — 사회자의 지시, 상대 의견, 방향이 담겨 있음
2. **전문으로 답변** — 요약하지 마세요. 논거를 구체적으로 전개하세요 2. **전문으로 답변** — 요약하지 마세요. 논거를 구체적으로 전개하세요
3. **상대 의견 검증** — 오류·누락·논리적 허점을 먼저 확인하세요 3. **상대 의견 검증** — 오류·누락·논리적 허점을 확인하세요
4. **개선안 제시** — 단순 반론이 아니라 대안/보강을 함께 제시하세요 4. **개선안 제시** — 단순 반론이 아니라 대안/보강을 함께 제시하세요
5. **근거 명시** 주장에는 기술적 근거, 사례, 레퍼런스를 포함하세요 5. **근거 명시** — 기술적 근거, 사례, 레퍼런스를 포함하세요
6. **합의 가능 시 인정** — 상대 의견이 맞으면 솔직히 인정하고 발전시키세요 6. **합의 가능 시 인정** — 상대 의견이 맞으면 솔직히 인정하고 발전시키세요
7. **wiki/ 참고** — 이전에 합의된 사항과 모순되지 않게 답변하세요
## 금지 사항 ## 금지 사항
@@ -26,3 +30,4 @@
- ❌ 주제에서 벗어난 발언 - ❌ 주제에서 벗어난 발언
- ❌ 근거 없는 주장 - ❌ 근거 없는 주장
- ❌ "이하생략" 또는 답변 축약 - ❌ "이하생략" 또는 답변 축약
- ❌ Discord에 긴 답변 직접 게시 (반드시 response.md에)