feat(debate): 분산 토론 시스템 — Wiki.js 기반 통신, AG 프로젝트 스캐폴딩, handler 리팩토링

- wiki_client.py: find_page()를 singleByPath 직접 조회로 교체 (O(1))
- debate-agent/: gemini/opus AG 프로젝트 생성 (GEMINI.md + wiki_debate.py + /start)
- debate_handler.py: 로컬 파일 I/O → Wiki.js API 전환, _sync_wiki() 삭제
- response 페이지 매 턴 초기화 (누적 방지)
This commit is contained in:
2026-03-21 20:52:12 +09:00
parent 5fb4179857
commit cbc9db0439
10 changed files with 633 additions and 89 deletions

View File

@@ -15,7 +15,6 @@ import logging
import random
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional
import discord
@@ -29,12 +28,6 @@ DEBATE_AGENTS = {
"opus": 1484156521209401476,
}
# ── 로컬 프로젝트 경로 ──
AGENT_PATHS = {
"gemini": Path(r"C:\Users\Variet-Worker\Desktop\debate_gemini"),
"opus": Path(r"C:\Users\Variet-Worker\Desktop\debate_opus"),
}
AGENT_COLORS = {"gemini": 0x2ECC71, "opus": 0x3498DB}
AGENT_EMOJI = {"gemini": "🟢", "opus": "🔵"}
@@ -64,6 +57,18 @@ class DebateHandler:
self.session = DebateSession()
self._debate_task: Optional[asyncio.Task] = None
self._response_event = asyncio.Event()
self._wiki = None # lazy init
def _get_wiki(self):
"""WikiClient lazy 초기화."""
if self._wiki is None:
from tools.wiki_client import WikiClient
self._wiki = WikiClient()
return self._wiki
def _wiki_path(self, page: str) -> str:
"""Wiki.js debate 페이지 경로 생성."""
return f"debates/{self.session.topic_slug}/{page}"
# ═══════════════════════════════════════════
# 공개 API
@@ -101,12 +106,21 @@ class DebateHandler:
active=True, max_rounds=MAX_ROUNDS,
)
# 폴더 초기화
for name, path in AGENT_PATHS.items():
(path / "response.md").write_text("", encoding="utf-8")
(path / "input.md").write_text("", encoding="utf-8")
wiki_dir = path / "wiki"
wiki_dir.mkdir(exist_ok=True)
# Wiki.js 초기 페이지 생성
wiki = self._get_wiki()
for page in ["working-document", "round-log", "control",
"input-gemini", "input-opus",
"response-gemini", "response-opus"]:
try:
await wiki.upsert_page(
self._wiki_path(page),
f"{short_title}{page}",
f"# {self.session.topic}\n\n*(토론 시작 대기 중)*\n"
if page == "working-document" else "",
tags=["debate", slug],
)
except Exception as e:
logger.warning(f"Wiki 초기화 실패 ({page}): {e}")
# 시작 알림
ctrl = self.bot.get_channel(DEBATE_CONTROL_CH)
@@ -222,8 +236,6 @@ class DebateHandler:
# ⑥ Working Document 통합 편집 (Flash + Wiki.js)
await self._update_working_document(speaker, response)
# ⑦ 양쪽 wiki/ 동기화
self._sync_wiki()
# ⑧ #debate에 요약 게시
await self._post_summary(speaker, response)
@@ -273,11 +285,19 @@ class DebateHandler:
return speaker
async def _prepare_input(self, speaker: str):
"""사회자가 input.md + wiki/ 작성."""
# 사회자(Flash) 해설 생성
"""사회자가 Wiki.js input 페이지 작성 + response 초기화."""
wiki = self._get_wiki()
# ① response 페이지 초기화 (이전 답변 누적 방지)
await self._wiki_upsert(
self._wiki_path(f"response-{speaker}"),
f"Response - {speaker}", "",
)
# ② 사회자(Flash) 해설 생성
moderator_commentary = await self._build_moderator_prompt(speaker)
# 상대방 전문을 코드에서 직접 삽입 (Flash의 요약에 의존하지 않음)
# 상대방 전문을 코드에서 직접 삽입
prev = [h for h in self.session.history if h["speaker"] != "user"]
opponent_section = ""
if prev:
@@ -289,20 +309,13 @@ class DebateHandler:
f"---\n"
)
# input.md = 사회자 해설 + 상대방 전문 (코드가 보장)
# input = 사회자 해설 + 상대방 전문
full_input = moderator_commentary + opponent_section
input_path = AGENT_PATHS[speaker] / "input.md"
input_path.write_text(full_input, encoding="utf-8")
# wiki/ — 기존 working_document.md가 있으면 그대로 유지
wiki_dir = AGENT_PATHS[speaker] / "wiki"
wiki_dir.mkdir(exist_ok=True)
wd_path = wiki_dir / "working_document.md"
if not wd_path.exists():
# 첫 라운드에만 초기 스텁 생성
wd_path.write_text(
self._build_working_document(), encoding="utf-8",
)
await self._wiki_upsert(
self._wiki_path(f"input-{speaker}"),
f"Input - {speaker} (Round {self.session.round})",
full_input,
)
async def _send_signal(self, speaker: str):
"""AG에게 Discord 시그널."""
@@ -315,11 +328,7 @@ class DebateHandler:
)
async def _wait_for_response(self, speaker: str) -> str:
"""AG의 Discord 완료 메시지 대기 후 response.md 읽기."""
resp_path = AGENT_PATHS[speaker] / "response.md"
# response.md 비우기
resp_path.write_text("", encoding="utf-8")
"""AG의 Discord 완료 메시지 대기 후 Wiki.js response 읽기."""
ctrl = self.bot.get_channel(DEBATE_CONTROL_CH)
if ctrl:
await ctrl.send(
@@ -330,21 +339,23 @@ class DebateHandler:
self._response_event.clear()
try:
# Discord에서 "작업 종료" 메시지가 올 때까지 대기
# Discord에서 "step XX 종료" 또는 "작성 완료" 메시지가 올 때까지 대기
await asyncio.wait_for(self._response_event.wait(), timeout=RESPONSE_TIMEOUT)
if not self.session.active:
return ""
# 완료 시그널을 받으면 파일 읽기
if resp_path.exists():
content = resp_path.read_text(encoding="utf-8").strip()
if content:
return content
else:
if ctrl:
await ctrl.send(f"⚠️ `{speaker}` 완료 신호를 받았으나 `response.md`가 비어있습니다.")
# 완료 시그널을 받으면 Wiki.js에서 response 읽기
wiki = self._get_wiki()
page = await wiki.find_page(self._wiki_path(f"response-{speaker}"))
if page and page.content.strip():
return page.content.strip()
else:
if ctrl:
await ctrl.send(
f"⚠️ `{speaker}` 완료 신호를 받았으나 "
f"`response-{speaker}` 페이지가 비어있습니다."
)
return ""
except asyncio.TimeoutError:
@@ -476,11 +487,15 @@ class DebateHandler:
"""토론 종료 — Discord + Wiki.js에 결론 기록."""
self.session.active = False
# Working Document 최종본 읽기
wd_path = AGENT_PATHS["gemini"] / "wiki" / "working_document.md"
# Working Document 최종본 읽기 (Wiki.js)
wiki = self._get_wiki()
final_doc = ""
if wd_path.exists():
final_doc = wd_path.read_text(encoding="utf-8").strip()
try:
wd_page = await wiki.find_page(self._wiki_path("working-document"))
if wd_page:
final_doc = wd_page.content.strip()
except Exception as e:
logger.warning(f"Working Document 읽기 실패: {e}")
# Discord에 요약 게시
parts = []
@@ -518,11 +533,16 @@ class DebateHandler:
# ═══════════════════════════════════════════
async def _update_working_document(self, speaker: str, response: str):
"""Flash가 AG 의견을 Working Document에 통합 편집 + Wiki.js 업로드."""
wd_path = AGENT_PATHS["gemini"] / "wiki" / "working_document.md"
"""Flash가 AG 의견을 Working Document에 통합 편집 (Wiki.js)."""
# 현재 문서 읽기 (Wiki.js)
wiki = self._get_wiki()
current_doc = ""
if wd_path.exists():
current_doc = wd_path.read_text(encoding="utf-8").strip()
try:
wd_page = await wiki.find_page(self._wiki_path("working-document"))
if wd_page:
current_doc = wd_page.content.strip()
except Exception:
pass
merge_prompt = f"""당신은 문서 편집자입니다.
@@ -554,12 +574,9 @@ class DebateHandler:
caller = GeminiCaller()
updated = await caller.call_simple(merge_prompt, timeout=120)
if updated and len(updated) > 50:
wd_path.parent.mkdir(parents=True, exist_ok=True)
wd_path.write_text(updated, encoding="utf-8")
logger.info(f"Working Document 업데이트: {len(updated)}")
# Wiki.js에 업로드
await self._wiki_upsert(
f"debates/{self.session.topic_slug}/working-document",
self._wiki_path("working-document"),
f"{self.session.wiki_title} — Working Document",
updated,
)
@@ -569,12 +586,16 @@ class DebateHandler:
logger.error(f"Working Document 업데이트 오류: {e}")
async def _append_round_log(self, speaker: str, response: str):
"""Round Log에 대화 전문 append + Wiki.js 업로드."""
log_path = AGENT_PATHS["gemini"] / "wiki" / "round_log.md"
# 기존 내용 읽기
"""Round Log에 대화 전문 append (Wiki.js)."""
# 기존 내용 읽기 (Wiki.js)
wiki = self._get_wiki()
existing = ""
if log_path.exists():
existing = log_path.read_text(encoding="utf-8")
try:
log_page = await wiki.find_page(self._wiki_path("round-log"))
if log_page:
existing = log_page.content
except Exception:
pass
emoji = AGENT_EMOJI.get(speaker, "👤")
entry = (
@@ -583,12 +604,9 @@ class DebateHandler:
f"{response}\n"
)
updated_log = existing + entry
log_path.parent.mkdir(parents=True, exist_ok=True)
log_path.write_text(updated_log, encoding="utf-8")
# Wiki.js에 업로드
await self._wiki_upsert(
f"debates/{self.session.topic_slug}/round-log",
self._wiki_path("round-log"),
f"{self.session.wiki_title} — Round Log",
updated_log,
)
@@ -608,18 +626,6 @@ class DebateHandler:
except Exception as e:
logger.warning(f"Wiki.js 업로드 실패 ({path}): {e}")
def _sync_wiki(self):
"""gemini wiki/ 폴더 내용을 opus wiki/에 동기화."""
src = AGENT_PATHS["gemini"] / "wiki"
dst = AGENT_PATHS["opus"] / "wiki"
dst.mkdir(parents=True, exist_ok=True)
for f in src.iterdir():
if f.is_file():
(dst / f.name).write_text(
f.read_text(encoding="utf-8"), encoding="utf-8",
)
def _build_working_document(self) -> str:
"""초기 working document 생성 (첫 라운드용)."""
return f"# {self.session.topic}\n\n*(토론 진행 중 — 내용이 채워집니다)*\n"