diff --git a/debate-agent/debate-gemini/tools/wiki_debate.py b/debate-agent/debate-gemini/tools/wiki_debate.py index 878953d..29b9c81 100644 --- a/debate-agent/debate-gemini/tools/wiki_debate.py +++ b/debate-agent/debate-gemini/tools/wiki_debate.py @@ -59,7 +59,10 @@ async def _query(query: str, variables: dict = None) -> dict: data = resp.json() if "errors" in data and data["errors"]: - raise RuntimeError(f"Wiki.js 오류: {data['errors'][0].get('message', data['errors'])}") + err_msg = data["errors"][0].get("message", str(data["errors"])) + # singleByPath는 페이지 미존재 시 에러 반환 — None 처리를 위해 통과 + if "does not exist" not in err_msg.lower(): + raise RuntimeError(f"Wiki.js 오류: {err_msg}") return data.get("data", {}) diff --git a/debate-agent/debate-opus/tools/wiki_debate.py b/debate-agent/debate-opus/tools/wiki_debate.py index 878953d..29b9c81 100644 --- a/debate-agent/debate-opus/tools/wiki_debate.py +++ b/debate-agent/debate-opus/tools/wiki_debate.py @@ -59,7 +59,10 @@ async def _query(query: str, variables: dict = None) -> dict: data = resp.json() if "errors" in data and data["errors"]: - raise RuntimeError(f"Wiki.js 오류: {data['errors'][0].get('message', data['errors'])}") + err_msg = data["errors"][0].get("message", str(data["errors"])) + # singleByPath는 페이지 미존재 시 에러 반환 — None 처리를 위해 통과 + if "does not exist" not in err_msg.lower(): + raise RuntimeError(f"Wiki.js 오류: {err_msg}") return data.get("data", {}) diff --git a/docs/design/debate-room-v2.md b/docs/design/debate-room-v2.md index 1c271b8..6f6fd0f 100644 --- a/docs/design/debate-room-v2.md +++ b/docs/design/debate-room-v2.md @@ -1,140 +1,118 @@ -# AI Debate Room — 설계서 v4.1 +# AI Debate Room — 설계서 v5 -> 확정 구조: 파일 기반 자동 토론 + Wiki.js 실시간 동기화 +> 확정 구조: **Wiki.js 기반 분산 토론** (로컬 파일 I/O 제거) ## 아키텍처 ``` 사용자 (관전/개입/방향지시) - ↕ Discord #variet-debate (펜딩 시에만 질문) -사회자 (Gemini CLI Flash) - ├→ AG 로컬 폴더에서 response.md 읽기 - ├→ 상대 AG 로컬 폴더에 input.md 쓰기 - ├→ 양쪽 wiki/ 폴더 동기화 - ├→ Wiki.js에 합의 기록 - ├→ 합의 감지 → Wiki 기록 + 사용자에게 검토 요청 - └→ 펜딩 감지 → 사용자에게 질문 + ↕ Discord #variet-debate +사회자 (variet-agent Bot 내 debate_handler.py + Gemini Flash) + ├→ Wiki.js에 input-{speaker} 작성 (해설 + 상대 전문) + ├→ Wiki.js에서 response-{speaker} 읽기 + ├→ Wiki.js working-document 통합 편집 (Flash) + ├→ Wiki.js round-log append + ├→ Discord에 발언권 시그널 전송 + ├→ Discord에서 "step XX 종료" 완료 감지 + └→ 합의 판정 → conclude / ask_user / continue ↕ ↕ -AG(Gemini) AG(Claude/Opus) -debate_gemini/ debate_opus/ +AG(Gemini) AG(Opus) +debate-gemini/ debate-opus/ ``` -## 각 AG 로컬 폴더 구조 +## 통신 채널 -``` -debate_gemini/ debate_opus/ - GEMINI.md (참여자 프롬프트) GEMINI.md (참여자 프롬프트) - response.md ← AG가 Output response.md - input.md ← 사회자가 Input input.md - wiki/ ← Wiki 미러링 wiki/ - agenda.md agenda.md - working_document.md working_document.md - ... ... -``` +| 채널 | 역할 | +|------|------| +| **Wiki.js** | 유일한 콘텐츠 전달 경로 (input, response, WD, round-log) | +| **Discord** | 양방향 시그널만 (발언권 시작 + "step XX 종료" 감지) | -| 파일 | 작성자 | 소비자 | 용도 | -|------|--------|--------|------| -| `response.md` | AG | 사회자 | AG의 전문 답변 (길이 무제한) | -| `input.md` | 사회자 | AG | 상대 의견 + 사회자 해설 + 방향 지시 | -| `wiki/` | 사회자 | AG | Wiki.js 페이지 미러 (현재 합의 상태) | -| `GEMINI.md` | 사전 세팅 | AG | 참여자 역할 + 행동 규칙 | - -## 턴 흐름 (자동 반복) - -```mermaid -flowchart TD - S["!debate-start 주제"] --> M1["사회자: 첫 발언자 선택"] - M1 --> W1["input.md 작성 + wiki/ 동기화"] - W1 --> D1["Discord 시그널 전송"] - D1 --> AG1["AG 발언: response.md 작성"] - AG1 --> M2["사회자: response.md 읽기"] - M2 --> LOG["Round Log append (로컬 + Wiki.js)"] - LOG --> MERGE["Flash: Working Document 통합 편집"] - MERGE --> WIKI["Wiki.js 업로드 (WD + Round Log)"] - WIKI --> SYNC["양쪽 wiki/ 폴더 동기화"] - SYNC --> CHECK{"합의 판정 (Flash)"} - CHECK -->|대립 중| W2["상대 input.md 작성 + wiki/ 동기화"] - W2 --> D2["Discord 시그널"] - D2 --> AG2["상대 AG 발언"] - AG2 --> M2 - CHECK -->|합의| DONE["Wiki 최종 기록 + 사용자 검토 요청"] - CHECK -->|펜딩| ASK["사용자에게 질문"] - ASK --> USER["사용자 응답"] - USER --> W2 -``` - -## 사용자 개입 시점 - -| 상황 | 트리거 | 사회자 행동 | -|------|--------|-----------| -| **합의 완료** | 양쪽 agree 2+ 연속 | Wiki 최종 기록 → Discord에 "검토 요청" | -| **의견 필요** | 양쪽 모두 user_input 요구 | Discord에 질문 → 응답 대기 | -| **중간 업데이트** | N 라운드마다 | Wiki Working-Document 업데이트 + Discord 요약 | -| **사용자 임의 개입** | `!debate-inject 의견` | 다음 턴 input.md에 반영 | -| **방향 전환** | 주제 이탈 감지 | 사회자가 redirect 지시 | - -## 합의 판정 기준 - -사회자(Flash)가 매 턴 후 판정: -```json -{ - "decision": "continue | ask_user | conclude", - "agreement_level": "disagree | partial | agree", - "reason": "판정 근거", - "wiki_update": true/false -} -``` - -- `continue`: 자동 다음 턴 -- `ask_user`: Discord에 질문 → 응답 올 때까지 대기 -- `conclude`: 합의 완료 → Wiki 최종 기록 - -## Wiki.js 구조 +## Wiki.js 페이지 구조 ``` /debates/{topic-slug}/ - working-document ← Flash가 통합 편집한 산출물 (라운드마다 업데이트) - round-log ← 대화 전문 (append) - conclusion ← 최종 합의 (종료 후) +├── control ← 턴 상태 JSON +├── input-gemini ← 사회자 → Gemini (해설 + 상대 전문) +├── response-gemini ← Gemini → 사회자 (AG 답변) +├── input-opus ← 사회자 → Opus +├── response-opus ← Opus → 사회자 +├── working-document ← 사회자만 편집 (AG 열람 가능) +├── round-log ← 전문 기록 append (AG 열람 가능) +└── conclusion ← 최종 합의 ``` -- 사회자만 Wiki 쓰기 — `WikiClient.upsert_page()` 사용 -- 매 라운드 후 Working Document + Round Log 자동 업로드 -- AG는 로컬 `wiki/` 폴더에서 읽기만 +### 페이지 접근 매트릭스 -## Discord 채널 역할 +| 페이지 | 사회자 (Bot) | AG-Gemini | AG-Opus | +|--------|:---:|:---:|:---:| +| `input-{자기}` | ✍️ Write | 👁️ Read | 👁️ Read | +| `response-{자기}` | 👁️ Read | ✍️ Write | ✍️ Write | +| `working-document` | ✍️ Write | 👁️ Read | 👁️ Read | +| `round-log` | ✍️ Write | 👁️ Read | 👁️ Read | -| 채널 | 용도 | -|------|------| -| `#variet-debate` | 사용자 커맨드 + 요약 게시 + 승인 요청 | -| `#ag-debate_gemini` | AG(Gemini)에게 시그널 전송 | -| `#ag-debate_opus` | AG(Opus)에게 시그널 전송 | +> **접근 권한은 GEMINI.md 지시문으로 제어** (Wiki.js 레벨 ACL 아님) -Discord 메시지는 **시그널 용도**만. 전문은 모두 로컬 파일로 전달. +## 턴 흐름 -## 과금 구조 (변경 없음) +``` +① response-{speaker} 초기화 (이전 답변 누적 방지) +② Flash: input-{speaker} 생성 (해설 + 상대 전문) +③ Discord 발언권 시그널 전송 ("📥 Round N — input 확인") +④ Discord "step XX 종료" 대기 +⑤ Wiki.js에서 response-{speaker} 읽기 +⑥ Flash: Working Document 통합 편집 +⑦ Round Log append +⑧ 합의 판정 (continue / ask_user / conclude) +``` -| 역할 | 모델 | 과금 | -|------|------|------| -| 사회자 | Gemini Flash (CLI) | 무료/구독 쿼터 | -| 토론자 A | AG (Gemini 3.1 Pro) | 구독 쿼터 | -| 토론자 B | AG (Claude Opus) | 구독 쿼터 | +## 프로젝트 구조 -## 구현 파일 +### 소스 관리 (variet-agent 리포) -| 파일 | 역할 | -|------|------| -| `handlers/debate_handler.py` | 세션 관리, 파일 I/O, 자동 루프, 합의 판정 | -| `prompts/debate/participant_base.md` | AG 참여자 프롬프트 (→ GEMINI.md로 복사) | -| `prompts/debate/moderator.md` | 사회자 프롬프트 | -| `api/discord_bot.py` | 커맨드 + AG 메시지 감지 | -| `tools/wiki_client.py` | Wiki.js GraphQL API 클라이언트 | +``` +variet-agent/ +├── handlers/debate_handler.py ← 사회자 (Wiki.js API 사용) +├── tools/wiki_client.py ← singleByPath 쿼리 지원 +└── debate-agent/ ← AG 프로젝트 소스 관리 + ├── debate-gemini/ + │ ├── GEMINI.md ← 참여자 행동 규칙 + │ ├── .agent/workflows/start.md ← /start 워크플로우 + │ └── tools/wiki_debate.py ← Wiki.js 읽기/쓰기 CLI + └── debate-opus/ + ├── GEMINI.md + ├── .agent/workflows/start.md + └── tools/wiki_debate.py +``` -## 미결 사항 +### 배포 위치 -1. ~~Wiki.js 연동~~ → ✅ 구현 완료 -2. ~~Working Document 통합 편집~~ → ✅ Flash merge 구현 -3. ~~Round Log~~ → ✅ 대화 전문 append 구현 -4. AG가 `response.md`에 쓰라는 지시를 **확실히 따르는지** 검증 필요 -5. 컨텍스트 창 관리 — 토론이 길어질 때 히스토리 압축 전략 -6. Nextcloud 백업 연동 (나중) +``` +C:\Users\Variet-Worker\Desktop\debate-agent\ +├── debate-gemini/ ← AG(Gemini)가 이 폴더를 프로젝트로 열기 +│ ├── GEMINI.md +│ ├── .agent/workflows/start.md +│ ├── .env ← Wiki.js API 키 +│ └── tools/wiki_debate.py +└── debate-opus/ + ├── GEMINI.md + ├── .agent/workflows/start.md + ├── .env + └── tools/wiki_debate.py +``` + +## AG 도구 + +`wiki_debate.py` CLI: + +``` +python tools/wiki_debate.py read +python tools/wiki_debate.py write +python tools/wiki_debate.py write-file +``` + +## 주요 변경 이력 + +| 버전 | 날짜 | 변경 내용 | +|------|------|-----------| +| v4.1 | 2026-03-20 | 로컬 파일 기반 자동 토론 + Wiki.js 동기화 | +| v5 | 2026-03-21 | **Wiki.js 기반 분산 토론** — 로컬 파일 I/O 전면 제거, AG 프로젝트 분리 | diff --git a/tools/wiki_client.py b/tools/wiki_client.py index 8305f71..41a00ad 100644 --- a/tools/wiki_client.py +++ b/tools/wiki_client.py @@ -119,7 +119,13 @@ class WikiClient: }} } """ - data = await self._query(query, {"path": path}) + try: + data = await self._query(query, {"path": path}) + except RuntimeError as e: + # singleByPath는 페이지 미존재 시 GraphQL error 반환 + if "does not exist" in str(e).lower(): + return None + raise p = data.get("pages", {}).get("singleByPath") if not p: return None