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:
@@ -68,11 +68,13 @@ Discord 메시지
|
|||||||
| NC 4모듈 | `tools/nc_*.py` | ✅ | Files/Calendar/Mail/Contacts |
|
| NC 4모듈 | `tools/nc_*.py` | ✅ | Files/Calendar/Mail/Contacts |
|
||||||
| AI Foreman | `core/foreman.py` | ✅ | 목표 분해 + 상담 + Vikunja 등록 |
|
| AI Foreman | `core/foreman.py` | ✅ | 목표 분해 + 상담 + Vikunja 등록 |
|
||||||
| 에이전트 워크플로우 | `.agent/` | ✅ | STATUS.md + 수칙 업데이트 |
|
| 에이전트 워크플로우 | `.agent/` | ✅ | STATUS.md + 수칙 업데이트 |
|
||||||
| Wiki.js | `tools/wiki_client.py` | ✅ | GraphQL CRUD + 대시보드 |
|
| Wiki.js | `tools/wiki_client.py` | ✅ | GraphQL CRUD + 대시보드 + singleByPath |
|
||||||
| AI Debate Room | `handlers/debate_handler.py` | 🚧 | 파일 기반 자동 토론 + Wiki.js 동기화 |
|
| AI Debate Room | `handlers/debate_handler.py` | 🚧 | Wiki.js 기반 분산 토론 (AG 분리) |
|
||||||
|
| Debate AG 프로젝트 | `debate-agent/` | 🚧 | gemini/opus AG 스캐폴딩 + wiki_debate.py |
|
||||||
|
|
||||||
## 최근 마일스톤
|
## 최근 마일스톤
|
||||||
|
|
||||||
|
- **2026-03-21**: debate-agent 분산 토론 시스템 — Wiki.js 기반 통신, AG 프로젝트 스캐폴딩, handler 리팩토링
|
||||||
- **2026-03-20**: AI Debate Room v2 — 파일 기반 자동 토론 + Flash 합의 판정 + Wiki.js 동기화
|
- **2026-03-20**: AI Debate Room v2 — 파일 기반 자동 토론 + Flash 합의 판정 + Wiki.js 동기화
|
||||||
- **2026-03-18**: Nextcloud 4모듈 + NC핸들러 + AI Foreman v0.1 + unified 분류→라우팅 구현
|
- **2026-03-18**: Nextcloud 4모듈 + NC핸들러 + AI Foreman v0.1 + unified 분류→라우팅 구현
|
||||||
- **2026-03-17**: 커뮤니티 트렌드 스크래퍼 구현 (DCInside 인기글 + Gemini Skill)
|
- **2026-03-17**: 커뮤니티 트렌드 스크래퍼 구현 (DCInside 인기글 + Gemini Skill)
|
||||||
@@ -85,5 +87,5 @@ Discord 메시지
|
|||||||
## 현재 미해결
|
## 현재 미해결
|
||||||
|
|
||||||
- **핫딜 통합 수집기** (#393) — 뽐뿌/퀘이사존/딜봄/클리앙 핫딜 통합 크롤링 + 트렌드 분석
|
- **핫딜 통합 수집기** (#393) — 뽐뿌/퀘이사존/딜봄/클리앙 핫딜 통합 크롤링 + 트렌드 분석
|
||||||
- **AI Debate Room** (#387) — 파일 기반 자동 토론 MVP 테스트 중 (합의 판정, response.md 규정 준수 검증 필요)
|
- **AI Debate Room** (#387) — Wiki.js 기반 분산 토론 구현 완료, E2E 테스트 필요 (봇 `!debate` 명령 + AG `/start`)
|
||||||
- **CLI Bridge PoC** (#203) — Windows PTY 미지원으로 보류. pywinpty 또는 Docker 환경 필요
|
- **CLI Bridge PoC** (#203) — Windows PTY 미지원으로 보류. pywinpty 또는 Docker 환경 필요
|
||||||
|
|||||||
47
debate-agent/debate-gemini/.agent/workflows/start.md
Normal file
47
debate-agent/debate-gemini/.agent/workflows/start.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
description: 토론 세션을 시작합니다. 사회자의 input을 읽고 response를 작성합니다.
|
||||||
|
---
|
||||||
|
|
||||||
|
# 토론 참여 시작
|
||||||
|
|
||||||
|
// turbo-all
|
||||||
|
|
||||||
|
## 절차
|
||||||
|
|
||||||
|
### 1. GEMINI.md 행동 규칙 확인
|
||||||
|
|
||||||
|
`GEMINI.md`를 읽어 토론 참여자 행동 규칙을 확인합니다.
|
||||||
|
|
||||||
|
### 2. 사회자 지시 읽기
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python tools/wiki_debate.py read debates/{slug}/input-gemini
|
||||||
|
```
|
||||||
|
|
||||||
|
> `{slug}`는 현재 진행 중인 토론의 slug입니다. `input-gemini` 페이지가 비어있으면 아직 사회자가 input을 작성하지 않은 것이니 대기하세요.
|
||||||
|
|
||||||
|
### 3. 합의 상태 확인
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python tools/wiki_debate.py read debates/{slug}/working-document
|
||||||
|
```
|
||||||
|
|
||||||
|
사회자가 정리한 현재 합의 상태를 확인합니다.
|
||||||
|
|
||||||
|
### 4. 답변 작성 및 업로드
|
||||||
|
|
||||||
|
답변을 작성한 후 response 페이지에 업로드:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python tools/wiki_debate.py write debates/{slug}/response-gemini "답변 전문"
|
||||||
|
```
|
||||||
|
|
||||||
|
또는 긴 답변은 파일로:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python tools/wiki_debate.py write-file debates/{slug}/response-gemini response_draft.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Discord 완료 시그널
|
||||||
|
|
||||||
|
Discord에 "response 작성 완료"를 게시하여 사회자에게 알립니다.
|
||||||
42
debate-agent/debate-gemini/GEMINI.md
Normal file
42
debate-agent/debate-gemini/GEMINI.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Debate Participant — Gemini
|
||||||
|
|
||||||
|
당신은 AI 토론 참여자 (Gemini)입니다. 사회자의 지시에 따라 주어진 주제에 대해 의견을 제시하고, 상대방의 주장을 검증합니다.
|
||||||
|
|
||||||
|
## ⚠️ Wiki.js 기반 답변 (필수)
|
||||||
|
|
||||||
|
### 도구
|
||||||
|
- `python tools/wiki_debate.py read <path>` — Wiki.js 페이지 읽기
|
||||||
|
- `python tools/wiki_debate.py write <path> <content>` — Wiki.js 페이지 쓰기
|
||||||
|
- `python tools/wiki_debate.py write-file <path> <file>` — 파일 내용을 Wiki.js에 업로드
|
||||||
|
|
||||||
|
### 읽을 페이지 (순서대로)
|
||||||
|
1. **`debates/{slug}/input-gemini`** — 사회자가 작성한 지시 + 상대 의견 전문. **반드시 먼저 읽으세요.**
|
||||||
|
2. **`debates/{slug}/working-document`** — 현재까지의 합의 사항 (참고 + 취합 검증용)
|
||||||
|
3. **`debates/{slug}/round-log`** — 지금까지의 전체 토론 기록 (필요 시)
|
||||||
|
|
||||||
|
### 쓸 페이지
|
||||||
|
- **`debates/{slug}/response-gemini`** — 여기에만 전문 답변을 작성하세요.
|
||||||
|
|
||||||
|
### Discord 완료 시그널
|
||||||
|
- Wiki.js에 response 작성이 끝나면 Discord에 "response 작성 완료"를 게시하세요.
|
||||||
|
|
||||||
|
## 행동 규칙
|
||||||
|
|
||||||
|
1. **input 먼저 읽기** — 사회자의 지시, 상대 의견 전문, 방향이 담겨 있음
|
||||||
|
2. **전문으로 답변** — 요약하지 마세요. 논거를 구체적으로 전개하세요
|
||||||
|
3. **상대 의견 검증** — 오류·누락·논리적 허점을 확인하세요
|
||||||
|
4. **사회자 취합 검증** — `working-document`에 사회자가 양측 의견을 올바르게 반영했는지 확인하고, 누락/오류가 있으면 지적하세요
|
||||||
|
5. **개선안 제시** — 단순 반론이 아니라 대안/보강을 함께 제시하세요
|
||||||
|
6. **근거 명시** — 기술적 근거, 사례, 레퍼런스를 포함하세요
|
||||||
|
7. **합의 가능 시 인정** — 상대 의견이 맞으면 솔직히 인정하고 발전시키세요
|
||||||
|
|
||||||
|
## 금지 사항
|
||||||
|
|
||||||
|
- ❌ 상대방의 `input-opus` 또는 `response-opus` 페이지 접근
|
||||||
|
- ❌ `working-document` 수정 (읽기만 — 사회자만 편집)
|
||||||
|
- ❌ 소스 코드 작성 (문서 수준 논의만)
|
||||||
|
- ❌ 사회자 지시 무시
|
||||||
|
- ❌ 주제에서 벗어난 발언
|
||||||
|
- ❌ 근거 없는 주장
|
||||||
|
- ❌ "이하생략" 또는 답변 축약
|
||||||
|
- ❌ Discord에 긴 답변 직접 게시 (반드시 Wiki.js response 페이지에)
|
||||||
172
debate-agent/debate-gemini/tools/wiki_debate.py
Normal file
172
debate-agent/debate-gemini/tools/wiki_debate.py
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
"""Wiki.js Debate Tool — AG용 Wiki.js 읽기/쓰기 CLI.
|
||||||
|
|
||||||
|
사용법:
|
||||||
|
python tools/wiki_debate.py read <page-path>
|
||||||
|
python tools/wiki_debate.py write <page-path> <content>
|
||||||
|
python tools/wiki_debate.py write-file <page-path> <file-path>
|
||||||
|
|
||||||
|
예시:
|
||||||
|
python tools/wiki_debate.py read debates/frtb/input-gemini
|
||||||
|
python tools/wiki_debate.py write debates/frtb/response-gemini "SA 방식을 제안합니다..."
|
||||||
|
python tools/wiki_debate.py write-file debates/frtb/response-gemini response_draft.md
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import httpx
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# .env 로드 (여러 후보 경로에서 탐색)
|
||||||
|
_candidates = [
|
||||||
|
Path(__file__).parent.parent / ".env", # tools/../.env (배포 위치)
|
||||||
|
Path.cwd() / ".env", # CWD/.env
|
||||||
|
Path.cwd().parent / ".env", # CWD/../.env
|
||||||
|
]
|
||||||
|
for _env_path in _candidates:
|
||||||
|
if _env_path.exists():
|
||||||
|
for line in _env_path.read_text(encoding="utf-8").splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
if "=" in line:
|
||||||
|
key, _, value = line.partition("=")
|
||||||
|
value = value.strip()
|
||||||
|
if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"):
|
||||||
|
value = value[1:-1]
|
||||||
|
os.environ.setdefault(key.strip(), value)
|
||||||
|
break # 첫 번째 .env만 사용
|
||||||
|
|
||||||
|
WIKI_URL = os.getenv("WIKI_URL", "https://wiki.variet.net")
|
||||||
|
WIKI_API_KEY = os.getenv("WIKI_API_KEY", "")
|
||||||
|
GRAPHQL_ENDPOINT = f"{WIKI_URL}/graphql"
|
||||||
|
|
||||||
|
|
||||||
|
async def _query(query: str, variables: dict = None) -> dict:
|
||||||
|
"""GraphQL 쿼리 실행."""
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {WIKI_API_KEY}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
payload = {"query": query}
|
||||||
|
if variables:
|
||||||
|
payload["variables"] = variables
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||||
|
resp = await client.post(GRAPHQL_ENDPOINT, json=payload, headers=headers)
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
|
||||||
|
if "errors" in data and data["errors"]:
|
||||||
|
raise RuntimeError(f"Wiki.js 오류: {data['errors'][0].get('message', data['errors'])}")
|
||||||
|
return data.get("data", {})
|
||||||
|
|
||||||
|
|
||||||
|
async def read_page(path: str) -> str:
|
||||||
|
"""경로로 페이지 읽기 (singleByPath)."""
|
||||||
|
query = """
|
||||||
|
query ($path: String!) {
|
||||||
|
pages { singleByPath(path: $path, locale: "ko") {
|
||||||
|
id, path, title, content
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
data = await _query(query, {"path": path})
|
||||||
|
page = data.get("pages", {}).get("singleByPath")
|
||||||
|
if not page:
|
||||||
|
return f"[오류] 페이지를 찾을 수 없습니다: {path}"
|
||||||
|
return page.get("content", "")
|
||||||
|
|
||||||
|
|
||||||
|
async def write_page(path: str, content: str) -> str:
|
||||||
|
"""경로에 페이지 쓰기 (upsert)."""
|
||||||
|
# 기존 페이지 확인
|
||||||
|
find_query = """
|
||||||
|
query ($path: String!) {
|
||||||
|
pages { singleByPath(path: $path, locale: "ko") {
|
||||||
|
id, tags { tag }
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
data = await _query(find_query, {"path": path})
|
||||||
|
existing = data.get("pages", {}).get("singleByPath")
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
# 수정
|
||||||
|
tags = [t["tag"] for t in existing.get("tags", [])]
|
||||||
|
update_query = """
|
||||||
|
mutation ($id: Int!, $content: String!, $tags: [String!]) {
|
||||||
|
pages { update(id: $id, content: $content, tags: $tags) {
|
||||||
|
responseResult { succeeded, message }
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
result = await _query(update_query, {
|
||||||
|
"id": existing["id"], "content": content, "tags": tags,
|
||||||
|
})
|
||||||
|
ok = result["pages"]["update"]["responseResult"]["succeeded"]
|
||||||
|
msg = result["pages"]["update"]["responseResult"]["message"]
|
||||||
|
return f"✅ 수정 완료 (id: {existing['id']})" if ok else f"❌ 수정 실패: {msg}"
|
||||||
|
else:
|
||||||
|
# 생성
|
||||||
|
title = path.split("/")[-1].replace("-", " ").title()
|
||||||
|
create_query = """
|
||||||
|
mutation ($content: String!, $path: String!, $title: String!,
|
||||||
|
$description: String!, $tags: [String!]!) {
|
||||||
|
pages { create(
|
||||||
|
content: $content, description: $description,
|
||||||
|
editor: "markdown", isPublished: true, isPrivate: false,
|
||||||
|
locale: "ko", path: $path, tags: $tags, title: $title
|
||||||
|
) {
|
||||||
|
responseResult { succeeded, message }
|
||||||
|
page { id }
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
result = await _query(create_query, {
|
||||||
|
"content": content, "path": path, "title": title,
|
||||||
|
"description": title, "tags": ["debate"],
|
||||||
|
})
|
||||||
|
ok = result["pages"]["create"]["responseResult"]["succeeded"]
|
||||||
|
if ok:
|
||||||
|
pid = result["pages"]["create"]["page"]["id"]
|
||||||
|
return f"✅ 생성 완료 (id: {pid})"
|
||||||
|
return f"❌ 생성 실패: {result['pages']['create']['responseResult']['message']}"
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
if sys.stdout.encoding != "utf-8":
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
args = sys.argv[1:]
|
||||||
|
if len(args) < 2:
|
||||||
|
print(__doc__)
|
||||||
|
return
|
||||||
|
|
||||||
|
cmd, path = args[0], args[1]
|
||||||
|
|
||||||
|
if cmd == "read":
|
||||||
|
content = await read_page(path)
|
||||||
|
print(content)
|
||||||
|
|
||||||
|
elif cmd == "write" and len(args) > 2:
|
||||||
|
content = " ".join(args[2:])
|
||||||
|
result = await write_page(path, content)
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
elif cmd == "write-file" and len(args) > 2:
|
||||||
|
file_path = Path(args[2])
|
||||||
|
if not file_path.exists():
|
||||||
|
print(f"❌ 파일을 찾을 수 없습니다: {file_path}")
|
||||||
|
return
|
||||||
|
content = file_path.read_text(encoding="utf-8")
|
||||||
|
result = await write_page(path, content)
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(__doc__)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
47
debate-agent/debate-opus/.agent/workflows/start.md
Normal file
47
debate-agent/debate-opus/.agent/workflows/start.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
description: 토론 세션을 시작합니다. 사회자의 input을 읽고 response를 작성합니다.
|
||||||
|
---
|
||||||
|
|
||||||
|
# 토론 참여 시작
|
||||||
|
|
||||||
|
// turbo-all
|
||||||
|
|
||||||
|
## 절차
|
||||||
|
|
||||||
|
### 1. GEMINI.md 행동 규칙 확인
|
||||||
|
|
||||||
|
`GEMINI.md`를 읽어 토론 참여자 행동 규칙을 확인합니다.
|
||||||
|
|
||||||
|
### 2. 사회자 지시 읽기
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python tools/wiki_debate.py read debates/{slug}/input-opus
|
||||||
|
```
|
||||||
|
|
||||||
|
> `{slug}`는 현재 진행 중인 토론의 slug입니다. `input-opus` 페이지가 비어있으면 아직 사회자가 input을 작성하지 않은 것이니 대기하세요.
|
||||||
|
|
||||||
|
### 3. 합의 상태 확인
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python tools/wiki_debate.py read debates/{slug}/working-document
|
||||||
|
```
|
||||||
|
|
||||||
|
사회자가 정리한 현재 합의 상태를 확인합니다.
|
||||||
|
|
||||||
|
### 4. 답변 작성 및 업로드
|
||||||
|
|
||||||
|
답변을 작성한 후 response 페이지에 업로드:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python tools/wiki_debate.py write debates/{slug}/response-opus "답변 전문"
|
||||||
|
```
|
||||||
|
|
||||||
|
또는 긴 답변은 파일로:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python tools/wiki_debate.py write-file debates/{slug}/response-opus response_draft.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Discord 완료 시그널
|
||||||
|
|
||||||
|
Discord에 "response 작성 완료"를 게시하여 사회자에게 알립니다.
|
||||||
42
debate-agent/debate-opus/GEMINI.md
Normal file
42
debate-agent/debate-opus/GEMINI.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Debate Participant — Opus
|
||||||
|
|
||||||
|
당신은 AI 토론 참여자 (Opus)입니다. 사회자의 지시에 따라 주어진 주제에 대해 의견을 제시하고, 상대방의 주장을 검증합니다.
|
||||||
|
|
||||||
|
## ⚠️ Wiki.js 기반 답변 (필수)
|
||||||
|
|
||||||
|
### 도구
|
||||||
|
- `python tools/wiki_debate.py read <path>` — Wiki.js 페이지 읽기
|
||||||
|
- `python tools/wiki_debate.py write <path> <content>` — Wiki.js 페이지 쓰기
|
||||||
|
- `python tools/wiki_debate.py write-file <path> <file>` — 파일 내용을 Wiki.js에 업로드
|
||||||
|
|
||||||
|
### 읽을 페이지 (순서대로)
|
||||||
|
1. **`debates/{slug}/input-opus`** — 사회자가 작성한 지시 + 상대 의견 전문. **반드시 먼저 읽으세요.**
|
||||||
|
2. **`debates/{slug}/working-document`** — 현재까지의 합의 사항 (참고 + 취합 검증용)
|
||||||
|
3. **`debates/{slug}/round-log`** — 지금까지의 전체 토론 기록 (필요 시)
|
||||||
|
|
||||||
|
### 쓸 페이지
|
||||||
|
- **`debates/{slug}/response-opus`** — 여기에만 전문 답변을 작성하세요.
|
||||||
|
|
||||||
|
### Discord 완료 시그널
|
||||||
|
- Wiki.js에 response 작성이 끝나면 Discord에 "response 작성 완료"를 게시하세요.
|
||||||
|
|
||||||
|
## 행동 규칙
|
||||||
|
|
||||||
|
1. **input 먼저 읽기** — 사회자의 지시, 상대 의견 전문, 방향이 담겨 있음
|
||||||
|
2. **전문으로 답변** — 요약하지 마세요. 논거를 구체적으로 전개하세요
|
||||||
|
3. **상대 의견 검증** — 오류·누락·논리적 허점을 확인하세요
|
||||||
|
4. **사회자 취합 검증** — `working-document`에 사회자가 양측 의견을 올바르게 반영했는지 확인하고, 누락/오류가 있으면 지적하세요
|
||||||
|
5. **개선안 제시** — 단순 반론이 아니라 대안/보강을 함께 제시하세요
|
||||||
|
6. **근거 명시** — 기술적 근거, 사례, 레퍼런스를 포함하세요
|
||||||
|
7. **합의 가능 시 인정** — 상대 의견이 맞으면 솔직히 인정하고 발전시키세요
|
||||||
|
|
||||||
|
## 금지 사항
|
||||||
|
|
||||||
|
- ❌ 상대방의 `input-gemini` 또는 `response-gemini` 페이지 접근
|
||||||
|
- ❌ `working-document` 수정 (읽기만 — 사회자만 편집)
|
||||||
|
- ❌ 소스 코드 작성 (문서 수준 논의만)
|
||||||
|
- ❌ 사회자 지시 무시
|
||||||
|
- ❌ 주제에서 벗어난 발언
|
||||||
|
- ❌ 근거 없는 주장
|
||||||
|
- ❌ "이하생략" 또는 답변 축약
|
||||||
|
- ❌ Discord에 긴 답변 직접 게시 (반드시 Wiki.js response 페이지에)
|
||||||
172
debate-agent/debate-opus/tools/wiki_debate.py
Normal file
172
debate-agent/debate-opus/tools/wiki_debate.py
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
"""Wiki.js Debate Tool — AG용 Wiki.js 읽기/쓰기 CLI.
|
||||||
|
|
||||||
|
사용법:
|
||||||
|
python tools/wiki_debate.py read <page-path>
|
||||||
|
python tools/wiki_debate.py write <page-path> <content>
|
||||||
|
python tools/wiki_debate.py write-file <page-path> <file-path>
|
||||||
|
|
||||||
|
예시:
|
||||||
|
python tools/wiki_debate.py read debates/frtb/input-gemini
|
||||||
|
python tools/wiki_debate.py write debates/frtb/response-gemini "SA 방식을 제안합니다..."
|
||||||
|
python tools/wiki_debate.py write-file debates/frtb/response-gemini response_draft.md
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import httpx
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# .env 로드 (여러 후보 경로에서 탐색)
|
||||||
|
_candidates = [
|
||||||
|
Path(__file__).parent.parent / ".env", # tools/../.env (배포 위치)
|
||||||
|
Path.cwd() / ".env", # CWD/.env
|
||||||
|
Path.cwd().parent / ".env", # CWD/../.env
|
||||||
|
]
|
||||||
|
for _env_path in _candidates:
|
||||||
|
if _env_path.exists():
|
||||||
|
for line in _env_path.read_text(encoding="utf-8").splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
if "=" in line:
|
||||||
|
key, _, value = line.partition("=")
|
||||||
|
value = value.strip()
|
||||||
|
if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"):
|
||||||
|
value = value[1:-1]
|
||||||
|
os.environ.setdefault(key.strip(), value)
|
||||||
|
break # 첫 번째 .env만 사용
|
||||||
|
|
||||||
|
WIKI_URL = os.getenv("WIKI_URL", "https://wiki.variet.net")
|
||||||
|
WIKI_API_KEY = os.getenv("WIKI_API_KEY", "")
|
||||||
|
GRAPHQL_ENDPOINT = f"{WIKI_URL}/graphql"
|
||||||
|
|
||||||
|
|
||||||
|
async def _query(query: str, variables: dict = None) -> dict:
|
||||||
|
"""GraphQL 쿼리 실행."""
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {WIKI_API_KEY}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
payload = {"query": query}
|
||||||
|
if variables:
|
||||||
|
payload["variables"] = variables
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||||
|
resp = await client.post(GRAPHQL_ENDPOINT, json=payload, headers=headers)
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
|
||||||
|
if "errors" in data and data["errors"]:
|
||||||
|
raise RuntimeError(f"Wiki.js 오류: {data['errors'][0].get('message', data['errors'])}")
|
||||||
|
return data.get("data", {})
|
||||||
|
|
||||||
|
|
||||||
|
async def read_page(path: str) -> str:
|
||||||
|
"""경로로 페이지 읽기 (singleByPath)."""
|
||||||
|
query = """
|
||||||
|
query ($path: String!) {
|
||||||
|
pages { singleByPath(path: $path, locale: "ko") {
|
||||||
|
id, path, title, content
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
data = await _query(query, {"path": path})
|
||||||
|
page = data.get("pages", {}).get("singleByPath")
|
||||||
|
if not page:
|
||||||
|
return f"[오류] 페이지를 찾을 수 없습니다: {path}"
|
||||||
|
return page.get("content", "")
|
||||||
|
|
||||||
|
|
||||||
|
async def write_page(path: str, content: str) -> str:
|
||||||
|
"""경로에 페이지 쓰기 (upsert)."""
|
||||||
|
# 기존 페이지 확인
|
||||||
|
find_query = """
|
||||||
|
query ($path: String!) {
|
||||||
|
pages { singleByPath(path: $path, locale: "ko") {
|
||||||
|
id, tags { tag }
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
data = await _query(find_query, {"path": path})
|
||||||
|
existing = data.get("pages", {}).get("singleByPath")
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
# 수정
|
||||||
|
tags = [t["tag"] for t in existing.get("tags", [])]
|
||||||
|
update_query = """
|
||||||
|
mutation ($id: Int!, $content: String!, $tags: [String!]) {
|
||||||
|
pages { update(id: $id, content: $content, tags: $tags) {
|
||||||
|
responseResult { succeeded, message }
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
result = await _query(update_query, {
|
||||||
|
"id": existing["id"], "content": content, "tags": tags,
|
||||||
|
})
|
||||||
|
ok = result["pages"]["update"]["responseResult"]["succeeded"]
|
||||||
|
msg = result["pages"]["update"]["responseResult"]["message"]
|
||||||
|
return f"✅ 수정 완료 (id: {existing['id']})" if ok else f"❌ 수정 실패: {msg}"
|
||||||
|
else:
|
||||||
|
# 생성
|
||||||
|
title = path.split("/")[-1].replace("-", " ").title()
|
||||||
|
create_query = """
|
||||||
|
mutation ($content: String!, $path: String!, $title: String!,
|
||||||
|
$description: String!, $tags: [String!]!) {
|
||||||
|
pages { create(
|
||||||
|
content: $content, description: $description,
|
||||||
|
editor: "markdown", isPublished: true, isPrivate: false,
|
||||||
|
locale: "ko", path: $path, tags: $tags, title: $title
|
||||||
|
) {
|
||||||
|
responseResult { succeeded, message }
|
||||||
|
page { id }
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
result = await _query(create_query, {
|
||||||
|
"content": content, "path": path, "title": title,
|
||||||
|
"description": title, "tags": ["debate"],
|
||||||
|
})
|
||||||
|
ok = result["pages"]["create"]["responseResult"]["succeeded"]
|
||||||
|
if ok:
|
||||||
|
pid = result["pages"]["create"]["page"]["id"]
|
||||||
|
return f"✅ 생성 완료 (id: {pid})"
|
||||||
|
return f"❌ 생성 실패: {result['pages']['create']['responseResult']['message']}"
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
if sys.stdout.encoding != "utf-8":
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
args = sys.argv[1:]
|
||||||
|
if len(args) < 2:
|
||||||
|
print(__doc__)
|
||||||
|
return
|
||||||
|
|
||||||
|
cmd, path = args[0], args[1]
|
||||||
|
|
||||||
|
if cmd == "read":
|
||||||
|
content = await read_page(path)
|
||||||
|
print(content)
|
||||||
|
|
||||||
|
elif cmd == "write" and len(args) > 2:
|
||||||
|
content = " ".join(args[2:])
|
||||||
|
result = await write_page(path, content)
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
elif cmd == "write-file" and len(args) > 2:
|
||||||
|
file_path = Path(args[2])
|
||||||
|
if not file_path.exists():
|
||||||
|
print(f"❌ 파일을 찾을 수 없습니다: {file_path}")
|
||||||
|
return
|
||||||
|
content = file_path.read_text(encoding="utf-8")
|
||||||
|
result = await write_page(path, content)
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(__doc__)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
| # | 시간 | 작업 | 커밋 | 상태 |
|
| # | 시간 | 작업 | 커밋 | 상태 |
|
||||||
|----|------|------|------|------|
|
|----|------|------|------|------|
|
||||||
| 001 | 12:00 | 자막 다운로드 Flow 정밀검증 — 과다 다운로드/리네임 미실행/오류 표현 5건 수정 | `94fb4e6` | ✅ |
|
| 001 | 12:00 | 자막 다운로드 Flow 정밀검증 — 과다 다운로드/리네임 미실행/오류 표현 5건 수정 | `94fb4e6` | ✅ |
|
||||||
|
| 002 | 20:50 | debate-agent 분산 토론 시스템 — Wiki.js 기반 통신, AG 프로젝트 스캐폴딩, handler 리팩토링 | `9fedba3` | ✅ |
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import logging
|
|||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
@@ -29,12 +28,6 @@ DEBATE_AGENTS = {
|
|||||||
"opus": 1484156521209401476,
|
"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_COLORS = {"gemini": 0x2ECC71, "opus": 0x3498DB}
|
||||||
AGENT_EMOJI = {"gemini": "🟢", "opus": "🔵"}
|
AGENT_EMOJI = {"gemini": "🟢", "opus": "🔵"}
|
||||||
|
|
||||||
@@ -64,6 +57,18 @@ class DebateHandler:
|
|||||||
self.session = DebateSession()
|
self.session = DebateSession()
|
||||||
self._debate_task: Optional[asyncio.Task] = None
|
self._debate_task: Optional[asyncio.Task] = None
|
||||||
self._response_event = asyncio.Event()
|
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
|
# 공개 API
|
||||||
@@ -101,12 +106,21 @@ class DebateHandler:
|
|||||||
active=True, max_rounds=MAX_ROUNDS,
|
active=True, max_rounds=MAX_ROUNDS,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 폴더 초기화
|
# Wiki.js 초기 페이지 생성
|
||||||
for name, path in AGENT_PATHS.items():
|
wiki = self._get_wiki()
|
||||||
(path / "response.md").write_text("", encoding="utf-8")
|
for page in ["working-document", "round-log", "control",
|
||||||
(path / "input.md").write_text("", encoding="utf-8")
|
"input-gemini", "input-opus",
|
||||||
wiki_dir = path / "wiki"
|
"response-gemini", "response-opus"]:
|
||||||
wiki_dir.mkdir(exist_ok=True)
|
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)
|
ctrl = self.bot.get_channel(DEBATE_CONTROL_CH)
|
||||||
@@ -222,8 +236,6 @@ class DebateHandler:
|
|||||||
# ⑥ Working Document 통합 편집 (Flash + Wiki.js)
|
# ⑥ Working Document 통합 편집 (Flash + Wiki.js)
|
||||||
await self._update_working_document(speaker, response)
|
await self._update_working_document(speaker, response)
|
||||||
|
|
||||||
# ⑦ 양쪽 wiki/ 동기화
|
|
||||||
self._sync_wiki()
|
|
||||||
|
|
||||||
# ⑧ #debate에 요약 게시
|
# ⑧ #debate에 요약 게시
|
||||||
await self._post_summary(speaker, response)
|
await self._post_summary(speaker, response)
|
||||||
@@ -273,11 +285,19 @@ class DebateHandler:
|
|||||||
return speaker
|
return speaker
|
||||||
|
|
||||||
async def _prepare_input(self, speaker: str):
|
async def _prepare_input(self, speaker: str):
|
||||||
"""사회자가 input.md + wiki/ 작성."""
|
"""사회자가 Wiki.js input 페이지 작성 + response 초기화."""
|
||||||
# 사회자(Flash) 해설 생성
|
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)
|
moderator_commentary = await self._build_moderator_prompt(speaker)
|
||||||
|
|
||||||
# 상대방 전문을 코드에서 직접 삽입 (Flash의 요약에 의존하지 않음)
|
# ③ 상대방 전문을 코드에서 직접 삽입
|
||||||
prev = [h for h in self.session.history if h["speaker"] != "user"]
|
prev = [h for h in self.session.history if h["speaker"] != "user"]
|
||||||
opponent_section = ""
|
opponent_section = ""
|
||||||
if prev:
|
if prev:
|
||||||
@@ -289,19 +309,12 @@ class DebateHandler:
|
|||||||
f"---\n"
|
f"---\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
# input.md = 사회자 해설 + 상대방 전문 (코드가 보장)
|
# ④ input = 사회자 해설 + 상대방 전문
|
||||||
full_input = moderator_commentary + opponent_section
|
full_input = moderator_commentary + opponent_section
|
||||||
input_path = AGENT_PATHS[speaker] / "input.md"
|
await self._wiki_upsert(
|
||||||
input_path.write_text(full_input, encoding="utf-8")
|
self._wiki_path(f"input-{speaker}"),
|
||||||
|
f"Input - {speaker} (Round {self.session.round})",
|
||||||
# wiki/ — 기존 working_document.md가 있으면 그대로 유지
|
full_input,
|
||||||
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",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _send_signal(self, speaker: str):
|
async def _send_signal(self, speaker: str):
|
||||||
@@ -315,11 +328,7 @@ class DebateHandler:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def _wait_for_response(self, speaker: str) -> str:
|
async def _wait_for_response(self, speaker: str) -> str:
|
||||||
"""AG의 Discord 완료 메시지 대기 후 response.md 읽기."""
|
"""AG의 Discord 완료 메시지 대기 후 Wiki.js response 읽기."""
|
||||||
resp_path = AGENT_PATHS[speaker] / "response.md"
|
|
||||||
# response.md 비우기
|
|
||||||
resp_path.write_text("", encoding="utf-8")
|
|
||||||
|
|
||||||
ctrl = self.bot.get_channel(DEBATE_CONTROL_CH)
|
ctrl = self.bot.get_channel(DEBATE_CONTROL_CH)
|
||||||
if ctrl:
|
if ctrl:
|
||||||
await ctrl.send(
|
await ctrl.send(
|
||||||
@@ -330,21 +339,23 @@ class DebateHandler:
|
|||||||
self._response_event.clear()
|
self._response_event.clear()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Discord에서 "작업 종료" 메시지가 올 때까지 대기
|
# Discord에서 "step XX 종료" 또는 "작성 완료" 메시지가 올 때까지 대기
|
||||||
await asyncio.wait_for(self._response_event.wait(), timeout=RESPONSE_TIMEOUT)
|
await asyncio.wait_for(self._response_event.wait(), timeout=RESPONSE_TIMEOUT)
|
||||||
|
|
||||||
if not self.session.active:
|
if not self.session.active:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
# 완료 시그널을 받으면 파일 읽기
|
# 완료 시그널을 받으면 Wiki.js에서 response 읽기
|
||||||
if resp_path.exists():
|
wiki = self._get_wiki()
|
||||||
content = resp_path.read_text(encoding="utf-8").strip()
|
page = await wiki.find_page(self._wiki_path(f"response-{speaker}"))
|
||||||
if content:
|
if page and page.content.strip():
|
||||||
return content
|
return page.content.strip()
|
||||||
else:
|
else:
|
||||||
if ctrl:
|
if ctrl:
|
||||||
await ctrl.send(f"⚠️ `{speaker}` 완료 신호를 받았으나 `response.md`가 비어있습니다.")
|
await ctrl.send(
|
||||||
|
f"⚠️ `{speaker}` 완료 신호를 받았으나 "
|
||||||
|
f"`response-{speaker}` 페이지가 비어있습니다."
|
||||||
|
)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
@@ -476,11 +487,15 @@ class DebateHandler:
|
|||||||
"""토론 종료 — Discord + Wiki.js에 결론 기록."""
|
"""토론 종료 — Discord + Wiki.js에 결론 기록."""
|
||||||
self.session.active = False
|
self.session.active = False
|
||||||
|
|
||||||
# Working Document 최종본 읽기
|
# Working Document 최종본 읽기 (Wiki.js)
|
||||||
wd_path = AGENT_PATHS["gemini"] / "wiki" / "working_document.md"
|
wiki = self._get_wiki()
|
||||||
final_doc = ""
|
final_doc = ""
|
||||||
if wd_path.exists():
|
try:
|
||||||
final_doc = wd_path.read_text(encoding="utf-8").strip()
|
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에 요약 게시
|
# Discord에 요약 게시
|
||||||
parts = []
|
parts = []
|
||||||
@@ -518,11 +533,16 @@ class DebateHandler:
|
|||||||
# ═══════════════════════════════════════════
|
# ═══════════════════════════════════════════
|
||||||
|
|
||||||
async def _update_working_document(self, speaker: str, response: str):
|
async def _update_working_document(self, speaker: str, response: str):
|
||||||
"""Flash가 AG 의견을 Working Document에 통합 편집 + Wiki.js 업로드."""
|
"""Flash가 AG 의견을 Working Document에 통합 편집 (Wiki.js)."""
|
||||||
wd_path = AGENT_PATHS["gemini"] / "wiki" / "working_document.md"
|
# 현재 문서 읽기 (Wiki.js)
|
||||||
|
wiki = self._get_wiki()
|
||||||
current_doc = ""
|
current_doc = ""
|
||||||
if wd_path.exists():
|
try:
|
||||||
current_doc = wd_path.read_text(encoding="utf-8").strip()
|
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"""당신은 문서 편집자입니다.
|
merge_prompt = f"""당신은 문서 편집자입니다.
|
||||||
|
|
||||||
@@ -554,12 +574,9 @@ class DebateHandler:
|
|||||||
caller = GeminiCaller()
|
caller = GeminiCaller()
|
||||||
updated = await caller.call_simple(merge_prompt, timeout=120)
|
updated = await caller.call_simple(merge_prompt, timeout=120)
|
||||||
if updated and len(updated) > 50:
|
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)}자")
|
logger.info(f"Working Document 업데이트: {len(updated)}자")
|
||||||
# Wiki.js에 업로드
|
|
||||||
await self._wiki_upsert(
|
await self._wiki_upsert(
|
||||||
f"debates/{self.session.topic_slug}/working-document",
|
self._wiki_path("working-document"),
|
||||||
f"{self.session.wiki_title} — Working Document",
|
f"{self.session.wiki_title} — Working Document",
|
||||||
updated,
|
updated,
|
||||||
)
|
)
|
||||||
@@ -569,12 +586,16 @@ class DebateHandler:
|
|||||||
logger.error(f"Working Document 업데이트 오류: {e}")
|
logger.error(f"Working Document 업데이트 오류: {e}")
|
||||||
|
|
||||||
async def _append_round_log(self, speaker: str, response: str):
|
async def _append_round_log(self, speaker: str, response: str):
|
||||||
"""Round Log에 대화 전문 append + Wiki.js 업로드."""
|
"""Round Log에 대화 전문 append (Wiki.js)."""
|
||||||
log_path = AGENT_PATHS["gemini"] / "wiki" / "round_log.md"
|
# 기존 내용 읽기 (Wiki.js)
|
||||||
# 기존 내용 읽기
|
wiki = self._get_wiki()
|
||||||
existing = ""
|
existing = ""
|
||||||
if log_path.exists():
|
try:
|
||||||
existing = log_path.read_text(encoding="utf-8")
|
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, "👤")
|
emoji = AGENT_EMOJI.get(speaker, "👤")
|
||||||
entry = (
|
entry = (
|
||||||
@@ -583,12 +604,9 @@ class DebateHandler:
|
|||||||
f"{response}\n"
|
f"{response}\n"
|
||||||
)
|
)
|
||||||
updated_log = existing + entry
|
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(
|
await self._wiki_upsert(
|
||||||
f"debates/{self.session.topic_slug}/round-log",
|
self._wiki_path("round-log"),
|
||||||
f"{self.session.wiki_title} — Round Log",
|
f"{self.session.wiki_title} — Round Log",
|
||||||
updated_log,
|
updated_log,
|
||||||
)
|
)
|
||||||
@@ -608,18 +626,6 @@ class DebateHandler:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Wiki.js 업로드 실패 ({path}): {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:
|
def _build_working_document(self) -> str:
|
||||||
"""초기 working document 생성 (첫 라운드용)."""
|
"""초기 working document 생성 (첫 라운드용)."""
|
||||||
return f"# {self.session.topic}\n\n*(토론 진행 중 — 내용이 채워집니다)*\n"
|
return f"# {self.session.topic}\n\n*(토론 진행 중 — 내용이 채워집니다)*\n"
|
||||||
|
|||||||
@@ -110,12 +110,25 @@ class WikiClient:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def find_page(self, path: str) -> Optional[WikiPage]:
|
async def find_page(self, path: str) -> Optional[WikiPage]:
|
||||||
"""경로로 페이지 찾기 (없으면 None)."""
|
"""경로로 페이지 찾기 (없으면 None). singleByPath 직접 조회."""
|
||||||
pages = await self.list_pages()
|
query = """
|
||||||
for p in pages:
|
query ($path: String!) {
|
||||||
if p.path == path:
|
pages { singleByPath(path: $path, locale: "ko") {
|
||||||
return await self.get_page(p.id)
|
id, path, title, content, updatedAt, description,
|
||||||
|
tags { tag }
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
data = await self._query(query, {"path": path})
|
||||||
|
p = data.get("pages", {}).get("singleByPath")
|
||||||
|
if not p:
|
||||||
return None
|
return None
|
||||||
|
tags = [t["tag"] for t in p.get("tags", [])]
|
||||||
|
return WikiPage(
|
||||||
|
id=p["id"], path=p["path"], title=p["title"],
|
||||||
|
content=p.get("content", ""), updated_at=p.get("updatedAt", ""),
|
||||||
|
description=p.get("description", ""), tags=tags,
|
||||||
|
)
|
||||||
|
|
||||||
# ──────────────────────────────────────
|
# ──────────────────────────────────────
|
||||||
# 생성 / 수정 / 삭제
|
# 생성 / 수정 / 삭제
|
||||||
|
|||||||
Reference in New Issue
Block a user