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:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user