refactor: GPU 서버 재구성 + ChromaDB→Qdrant 마이그레이션

- embed_to_chroma.py → embed_to_qdrant.py 리라이트 (bge-m3 + Qdrant REST API)
- auto_classify.scpt: embed_to_qdrant.py 경로 변경 + sourceChannel 덮어쓰기 버그 수정
- requirements.txt: chromadb/schedule 제거, qdrant-client/flask/gunicorn 추가
- credentials.env.example: GPU_SERVER_IP 항목 추가
- GPU 서버 재구성 계획서 (docs/gpu-restructure.md) + dev-roadmap/commands 통합
- CLAUDE.md, README.md, deploy.md 현행화

GPU 서버 변경사항 (이미 적용됨):
  - Ollama: qwen3.5:9b, id-9b 제거 → bge-m3 + bge-reranker-v2-m3
  - Surya OCR 서비스 (:8400, systemd)
  - Docker + NFS + Komga 이전 (:25600)
  - tk-ai-service: Ollama API → OpenAI API 전환 (MLX 35B)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hyungi
2026-03-30 13:19:31 +09:00
parent 35062145ed
commit 45cabc9aea
11 changed files with 2068 additions and 277 deletions

165
CLAUDE.md
View File

@@ -1,21 +1,68 @@
# DEVONThink PKM 시스템 — Claude Code 작업 가이드
> 마지막 업데이트: 2026-03-29
> 개발 현황: Phase 1 초기 구축 완료 → Phase 1.5 GPU 서버 재구성 + Phase 2 인프라 수정 병행 중
## 프로젝트 개요
Mac mini M4 Pro(64GB, 4TB) 기반 개인 지식관리(PKM) 시스템.
DEVONthink 4를 중앙 허브로, Ollama AI 자동 분류 + 법령 모니터링 + 일일 다이제스트를 자동화한다.
DEVONthink 4를 중앙 허브로, MLX AI 자동 분류 + 법령 모니터링 + 일일 다이제스트를 자동화한다.
## 핵심 문서 (반드시 먼저 읽을 것)
1. `docs/architecture.md` — 전체 시스템 아키텍처 (DB 구조, 태그, AI, 자동화 전체)
2. `docs/industrial-safety-blueprint.md` — 04_Industrial Safety DB 상세 설계
3. `docs/claude-code-commands.md` — 단계별 작업 지시서
3. `docs/claude-code-commands.md` — 단계별 작업 지시서 (현재 진행 상황 포함)
4. `docs/dev-roadmap.md` — 개발 로드맵 (Phase 1.5~6)
5. `docs/deploy.md` — Mac mini 배포 가이드 + 트러블슈팅
6. `docs/gpu-restructure.md` — GPU 서버 재구성 상세 계획 (Phase 1.5)
## 개발 현황 요약
```
[Phase 1: 초기 구축] ██████████████████░░ 90% — 코드 작성 완료, 인프라 일부 미해결
✅ 1단계: 프로젝트 구조 — 완료
✅ 2단계: AI 분류 프롬프트 — MLX Qwen3.5 OpenAI 호환 전환 완료
✅ 3단계: AppleScript — auto_classify + omnifocus_sync 완료
⚠️ 4단계: 법령 모니터링 — 외국(US/JP/EU) OK, 한국 API IP 미등록
❌ 5단계: MailPlus 수집 — IMAP Connection refused
⚠️ 6단계: Daily Digest — 코드 완성, 실행 테스트 미진행
✅ 7단계: DEVONagent 가이드 — 완료
❌ 8단계: 전체 테스트 — 미진행
✅ 추가: PKM API 서버 — 기본 동작, 개선 필요
[Phase 1.5: GPU 서버 재구성] ░░░░░░░░░░░░░░░░░░░░ 0% — 계획 완료, 실행 대기
→ docs/gpu-restructure.md 참조
→ GPU 모델 교체, Surya OCR, Komga 이전, Qdrant 통합, RAG 파이프라인
[Phase 2: 인프라 + 버그 픽스] ░░░░░░░░░░░░░░░░░░░░ 0% — Phase 1.5와 병행 착수 대기
→ docs/dev-roadmap.md 참조
```
## 알려진 이슈 (현재)
```
[P1 — 인프라]
- 한국 법령 API: open.law.go.kr에 Mac mini 공인IP 등록 필요
- MailPlus IMAP: 993 포트 Connection refused — Synology DSM에서 IMAP 활성화 확인
- requirements.txt: flask 누락, chromadb→qdrant-client 교체, schedule 미사용
- launchd: plist 파일은 있으나 실제 등록 여부 미확인
- GPU 서버: 중복 LLM 모델 제거 + Surya OCR/Komga 이전 필요 → gpu-restructure.md
[P2 — 코드 버그]
- JP 번역: MLX thinking 출력이 번역 결과에 오염 ("Wait, I'll check...")
- API 서버: /devonthink/stats → 500 에러, 한글 쿼리 인코딩 400 에러
- AppleScript: 경로 하드코딩 + sourceChannel 이중 설정 버그 (73행)
- embed_to_chroma.py: GPU_SERVER_IP 미설정으로 미작동 → Qdrant 리라이트 예정
```
## 네트워크 환경
```
Mac mini (운영 서버):
- Ollama: http://localhost:11434
- MLX 서버: http://localhost:8800/v1/chat/completions (Qwen3.5-35B-A3B)
- PKM API: http://127.0.0.1:9900 (Flask, GUI 세션 필수)
- DEVONthink: 로컬 실행 중
- OmniFocus: 로컬 실행 중
@@ -24,11 +71,12 @@ Synology NAS (DS1525+):
- Tailscale IP: 100.101.79.37
- 포트: 15001
- WebDAV: webdav.hyungi.net/Document_Server/DEVONThink/
- MailPlus IMAP: mailplus.hyungi.net:993 (SSL)
- MailPlus IMAP: mailplus.hyungi.net:993 (SSL) ← 현재 연결 불가
GPU 서버 (RTX 4070 Ti Super):
- 역할: 임베딩(nomic-embed-text), 비전(Qwen2.5-VL-7B), 리랭킹(bge-reranker)
- Tailscale IP: 별도 확인 필요
GPU 서버 (RTX 4070 Ti Super, 192.168.1.186):
- 현재: Ollama(11434) + qwen3.5:9b + id-9b, Plex(32400)
- 계획: bge-m3(임베딩) + bge-reranker(리랭킹) + Surya OCR(:8400) + Komga(:25600)
- → docs/gpu-restructure.md 참조
TKSafety: tksafety.technicalkorea.net (설정만, 나중에 활성화)
```
@@ -38,6 +86,7 @@ TKSafety: tksafety.technicalkorea.net (설정만, 나중에 활성화)
- 위치: `~/.config/pkm/credentials.env`
- 템플릿: `./credentials.env.example`
- 스크립트에서 python-dotenv로 로딩
- 필수 키: LAW_OC, MAILPLUS_HOST/PORT/USER/PASS, NAS_DOMAIN, GPU_SERVER_IP
## DEVONthink DB 구조 (13개)
@@ -80,46 +129,112 @@ manual — 직접 추가 → dataOrigin = work (기본
```
Tier 1 (Mac mini, 상시):
mlx-community/Qwen3.5-35B-A3B-4bit — 태그 생성, 문서 분류, 요약
mlx-community/Qwen3.5-35B-A3B-4bit — 태그 생성, 문서 분류, 요약, JP 번역
→ http://localhost:8800/v1/chat/completions (OpenAI 호환 API)
→ MLX 서버로 실행 중 (Ollama 아님)
※ thinking 모드 주의: /nothink 명시 또는 JSON 추출 후처리 필요
Tier 2 (Claude API, 필요시):
claude-sonnet — 복잡한 분석, 장문 처리
→ CLAUDE_API_KEY 사용
→ CLAUDE_API_KEY 사용 (아직 미연동)
Tier 3 (GPU 서버, 특수):
nomic-embed-text — 벡터 임베딩
Qwen2.5-VL-7B — 이미지/도면 OCR
bge-reranker-v2-m3 — RAG 리랭킹
Tier 3 (GPU 서버, 특수) — ※ 재구성 예정 (gpu-restructure.md 참조):
현재: qwen3.5:9b-q8_0, id-9b (제거 예정)
변경 후:
bge-m3 — 벡터 임베딩 (1024차원, Ollama)
bge-reranker-v2-m3 — RAG 리랭킹 (Ollama)
Surya OCR — 이미지/스캔 문서 OCR (FastAPI, 포트 8400)
```
## 파일 구조 (현재)
```
./
├── CLAUDE.md ← 이 파일 (Claude Code 작업 가이드)
├── README.md ← 프로젝트 설명
├── requirements.txt ← Python 패키지 (flask 추가 필요!)
├── .gitignore
├── credentials.env.example ← 인증 정보 템플릿
├── scripts/
│ ├── pkm_utils.py ← 공통 유틸 (로깅, 인증, LLM, AppleScript)
│ ├── law_monitor.py ← 법령 모니터링 (한국+US/JP/EU)
│ ├── mailplus_archive.py ← MailPlus 이메일 수집
│ ├── pkm_daily_digest.py ← 일일 다이제스트 생성
│ ├── pkm_api_server.py ← REST API 서버 (Flask, 포트 9900)
│ ├── embed_to_chroma.py ← ChromaDB 벡터 임베딩 (→ embed_to_qdrant.py로 교체 예정)
│ └── prompts/
│ └── classify_document.txt ← AI 분류 프롬프트 템플릿
├── applescript/
│ ├── auto_classify.scpt ← Inbox 자동 분류 Smart Rule
│ └── omnifocus_sync.scpt ← OmniFocus 연동 Smart Rule
├── launchd/
│ ├── net.hyungi.pkm.law-monitor.plist
│ ├── net.hyungi.pkm.mailplus.plist
│ └── net.hyungi.pkm.daily-digest.plist
├── data/
│ ├── law_last_check.json ← 법령 마지막 확인 시점
│ └── laws/ ← 수집된 법령 문서 (16건 수집 완료)
├── logs/ ← 실행 로그
├── docs/
│ ├── architecture.md ← 시스템 아키텍처
│ ├── industrial-safety-blueprint.md
│ ├── claude-code-commands.md ← 단계별 작업 지시서
│ ├── deploy.md ← Mac mini 배포 가이드
│ ├── devonagent-setup.md ← DEVONagent 검색 세트 가이드
│ ├── dev-roadmap.md ← 개발 로드맵 (Phase 1.5~6)
│ └── gpu-restructure.md ← GPU 서버 재구성 상세 계획
├── tests/
│ └── test_classify.py ← AI 분류 테스트 (5종 문서)
└── venv/ ← Python 가상환경
```
## 작업 순서
docs/claude-code-commands.md의 단계를 순서대로 진행:
### Phase 1 (완료): 초기 구축
docs/claude-code-commands.md의 1~7단계 → 코드 작성 완료
1. **프로젝트 구조** — README.md, deploy.md 작성 (구조는 이미 생성됨)
2. **Ollama 테스트** — 분류 프롬프트 최적화 → scripts/prompts/에 저장
3. **AppleScript** — auto_classify.scpt, omnifocus_sync.scpt
4. **법령 모니터링** — scripts/law_monitor.py + launchd plist
5. **이메일 수집** — scripts/mailplus_archive.py + launchd plist
6. **Daily Digest** — scripts/pkm_daily_digest.py + launchd plist
7. **DEVONagent 가이드** — docs/devonagent-setup.md (수동 설정 가이드)
8. **테스트** — tests/ + docs/test-report.md
### Phase 1.5 (계획 완료): GPU 서버 재구성
docs/gpu-restructure.md 참조:
1. GPU 모델 교체 (LLM 제거, bge-m3/reranker 설치)
2. Docker + NFS + Komga 이전
3. Surya OCR 설치
4. PKM 코드 갱신 (Qdrant 통합, embed 스크립트, AppleScript)
5. RAG 파이프라인 구축 (후순위)
### Phase 2 (진행 중): 인프라 수정 + 버그 픽스
docs/dev-roadmap.md 참조 (Phase 1.5와 병행):
1. requirements.txt 수정 ← Phase 1.5와 합산 (qdrant-client, flask)
2. 한국 법령 API IP 등록
3. MailPlus IMAP 연결 수정
4. JP 번역 thinking 오염 필터링
5. API 서버 한글 인코딩 + stats 500 에러 수정
6. AppleScript 하드코딩 경로 변수화 ← Phase 1.5와 합산
7. launchd 등록 및 확인
### Phase 3~4: API 서버 개선 + 테스트
- gunicorn 전환 + launchd plist 추가
- 엔드포인트 추가 (/law-monitor/status, /digest/latest)
- 모듈별 + E2E 통합 테스트 → docs/test-report.md
### Phase 5~6: 운영 안정화
- 로그 로테이션, Synology Chat 알림, 문서 보완
## 코딩 규칙
- Python 3.11+ (Mac mini 기본)
- Python 3.11+ (Mac mini 기본, 현재 3.14 확인됨)
- 인증 정보는 반드시 credentials.env에서 로딩 (하드코딩 금지)
- AppleScript는 DEVONthink/OmniFocus와 연동 (osascript로 호출)
- 로그는 ~/Documents/code/DEVONThink_my\ server/logs/에 저장
- launchd plist는 launchd/ 디렉토리에 생성, Mac mini에서 심볼릭 링크로 등록
- LLM 호출 시 pkm_utils.llm_generate() 사용 (thinking 후처리 포함)
- 한글 주석 사용
## 배포 방법
```
MacBook Pro (개발) → Gitea push → Mac mini에서 git pull
또는 Cowork 모드에서 직접 파일 수정 → git push
Mac mini에서:
cd ~/Documents/code/DEVONThink_my\ server/
git pull
@@ -132,6 +247,8 @@ Mac mini에서:
- credentials.env는 git에 올리지 않음 (.gitignore에 포함)
- DEVONthink, OmniFocus는 Mac mini에서 GUI로 실행 중이어야 AppleScript 작동
- 법령 API (LAW_OC)는 승인 대기 중 — 스크립트만 만들고 실제 호출은 승인 후
- PKM API 서버도 GUI 세션에서 실행 필수 (AppleScript 중계)
- 법령 API (LAW_OC): 키 발급 완료, Mac mini 공인IP 등록 필요
- TKSafety 연동은 설계만 완료, 구현은 나중에
- GPU 서버 Tailscale IP는 별도 확인 후 credentials.env에 추가
- MLX 서버 thinking 모드: 번역/분류 시 /nothink 프리픽스 또는 후처리 필수

114
README.md
View File

@@ -1,23 +1,91 @@
# DEVONThink PKM System
Mac mini M4 Pro 기반 개인 지식관리 자동화 시스템
Mac mini M4 Pro 기반 개인 지식관리(PKM) 자동화 시스템
## 구성 요소
## 시스템 구성
- **DEVONthink 4** — 중앙 지식 허브 (13개 DB)
- **Ollama** — AI 자동 분류/태깅 (Qwen3.5-35B-A3B)
- **법령 모니터링** — 산업안전보건법 등 변경 추적
- **일일 다이제스트** — PKM 전체 변화 요약
- **OmniFocus 연동** — 액션 아이템 자동 생성
```
┌─────────────────── Mac mini M4 Pro (허브) ───────────────────┐
│ │
│ DEVONthink 4 ◄── DEVONagent Pro │
│ (13개 DB) (자동 검색) │
│ │ │
│ ┌────┴─────── 자동화 레이어 ────────────────────────┐ │
│ │ auto_classify.scpt 법령 모니터링 이메일 수집 │ │
│ │ omnifocus_sync.scpt 일일 다이제스트 PKM API │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ OmniFocus 4 MLX Qwen3.5-35B-A3B (AI 분류/번역) │
│ (작업 관리) localhost:8800 │
│ │
└──────────────────────────┬─────────────────────────────────────┘
│ Tailscale VPN
┌────────────────┼────────────────┐
┌─────────▼──────────┐ ┌────────────▼─────────────┐
│ Synology DS1525+ │ │ GPU 서버 (RTX 4070 Ti S) │
│ Gitea · MailPlus │ │ 임베딩 · OCR · 리랭킹 │
│ WebDAV 동기화 │ │ Plex │
└────────────────────┘ └──────────────────────────┘
```
## 설치
## 핵심 기능
**AI 자동 분류** — DEVONthink Inbox에 들어온 문서를 MLX Qwen3.5가 분석하여 13개 DB 중 적합한 곳으로 자동 이동, 태그와 메타데이터를 자동 부여
**법령 모니터링** — 산업안전보건법, 중대재해처벌법 등 7개 한국 법령 + US OSHA, JP 厚労省, EU-OSHA 해외 법령 변경을 매일 자동 추적
**이메일 아카이브** — Synology MailPlus에서 IMAP으로 이메일을 수집하여 DEVONthink Archive DB에 자동 보관
**일일 다이제스트** — DEVONthink 변화, OmniFocus 진행 상황, 법령 변경 등을 종합한 일일 보고서 자동 생성
**OmniFocus 연동** — Projects DB의 TODO 패턴을 감지하여 OmniFocus에 작업 자동 생성, DEVONthink 역링크 포함
**REST API** — DEVONthink/OmniFocus 상태를 HTTP로 조회 (내부 모니터링용)
## 디렉토리 구조
```
scripts/ Python 스크립트
pkm_utils.py 공통 유틸 (로깅, 인증, LLM 호출)
law_monitor.py 법령 변경 모니터링 (한국+US/JP/EU)
mailplus_archive.py MailPlus 이메일 수집
pkm_daily_digest.py 일일 다이제스트 생성
pkm_api_server.py REST API 서버 (Flask, 포트 9900)
embed_to_chroma.py ChromaDB 벡터 임베딩
prompts/ AI 프롬프트 템플릿
applescript/ DEVONthink/OmniFocus 연동
auto_classify.scpt Inbox 자동 분류 Smart Rule
omnifocus_sync.scpt OmniFocus 작업 생성 Smart Rule
launchd/ macOS 스케줄 실행
net.hyungi.pkm.law-monitor.plist 매일 07:00
net.hyungi.pkm.mailplus.plist 매일 07:00, 18:00
net.hyungi.pkm.daily-digest.plist 매일 20:00
docs/ 문서
architecture.md 시스템 아키텍처
deploy.md 배포 가이드 + 트러블슈팅
claude-code-commands.md 개발 작업 지시서
dev-roadmap.md 개발 로드맵
devonagent-setup.md DEVONagent 검색 세트 가이드
industrial-safety-blueprint.md 산업안전 DB 설계
data/ 데이터
laws/ 수집된 법령 문서
law_last_check.json 마지막 확인 시점
tests/ 테스트
test_classify.py AI 분류 정확도 테스트
```
## 빠른 시작
```bash
# Mac mini에서
git clone [gitea-repo-url]
cd DEVONThink_my\ server
python3 -m venv venv
source venv/bin/activate
git clone https://git.hyungi.net/hyungi/devonthink_home.git "DEVONThink_my server"
cd "DEVONThink_my server"
python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txt
# 인증 정보 설정
@@ -29,12 +97,22 @@ chmod 600 ~/.config/pkm/credentials.env
자세한 배포 방법은 `docs/deploy.md` 참조
## 디렉토리 구조
## 실행 환경
| 구성 요소 | 요구사항 |
|-----------|---------|
| macOS | 14+ (Sonoma) |
| Python | 3.11+ |
| DEVONthink | 4.x, GUI 실행 중 |
| OmniFocus | 4.x, GUI 실행 중 |
| MLX 서버 | Qwen3.5-35B-A3B, localhost:8800 |
| Tailscale | NAS/GPU 서버 접근용 |
## 개발
```
scripts/ Python 스크립트 (법령모니터, 메일수집, 다이제스트)
applescript/ DEVONthink/OmniFocus 연동 AppleScript
launchd/ macOS 스케줄 실행 plist
docs/ 설계 문서, 가이드
tests/ 테스트 코드
개발 흐름:
MacBook Pro (또는 Cowork) → git push → Gitea (NAS) → Mac mini에서 git pull
```
개발 현황과 다음 작업은 `docs/dev-roadmap.md` 참조

View File

@@ -70,7 +70,6 @@ except:
add custom meta data sourceChannel for "sourceChannel" to theRecord
add custom meta data dataOrigin for "dataOrigin" to theRecord
add custom meta data (current date) for "lastAIProcess" to theRecord
add custom meta data "inbox_route" for "sourceChannel" to theRecord
-- 7. 대상 도메인 DB로 이동
set targetDatabase to missing value
@@ -89,7 +88,7 @@ except:
-- 8. GPU 서버 벡터 임베딩 비동기 전송
set embedScript to (POSIX path of (path to home folder)) & "Documents/code/DEVONThink_my server/venv/bin/python3"
set embedPy to (POSIX path of (path to home folder)) & "Documents/code/DEVONThink_my server/scripts/embed_to_chroma.py"
set embedPy to (POSIX path of (path to home folder)) & "Documents/code/DEVONThink_my server/scripts/embed_to_qdrant.py"
do shell script embedScript & " " & quoted form of embedPy & " " & quoted form of docUUID & " &> /dev/null &"
on error errMsg

View File

@@ -24,6 +24,9 @@ MAILPLUS_PASS=
# ─── Synology Chat 웹훅 (나중에 추가) ───
#CHAT_WEBHOOK_URL=
# ─── GPU 서버 (임베딩/OCR) ───
GPU_SERVER_IP=192.168.1.xxx
# ─── TKSafety API (나중에 활성화) ───
#TKSAFETY_HOST=
#TKSAFETY_PORT=

View File

@@ -1,18 +1,19 @@
# Claude Code 실행 명령어 — PKM 시스템 구축
> 작업 위치: MacBook Pro ~/Documents/code/DEVONThink_my server/
> Claude Code를 이 디렉토리에서 실행
> 또는 Cowork 모드에서 마운트된 폴더
> 완성 후 Gitea에 push → Mac mini에서 pull
```
개발/배포 흐름:
MacBook Pro (Claude Code)
MacBook Pro (Claude Code / Cowork)
~/Documents/code/DEVONThink_my server/
→ 스크립트/설정 파일 작성
→ git commit & push
Gitea (Synology NAS)
https://git.hyungi.net/hyungi/devonthink_home.git
Mac mini (git pull → 실행)
@@ -20,272 +21,478 @@ MacBook Pro (Claude Code)
---
## 0단계: 프로젝트 구조 생성 + credentials.env 복사
# Phase 1: 초기 구축 (완료)
Claude Code 실행 전에 먼저:
> 2026-03-26 ~ 03-27 작업. 총 15 커밋.
## 0단계: 프로젝트 구조 생성 + credentials.env ✅ 완료
```bash
# MacBook Pro에서
cd ~/Documents/code/DEVONThink_my\ server/
# credentials.env를 프로젝트에 복사 (gitignore 필수!)
cp ~/.config/pkm/credentials.env ./credentials.env.example
# example은 값을 비운 템플릿용, 실제 파일은 Mac mini에서 직접 생성
# Mac mini에서 (SSH 접속 후)
# Mac mini에서
mkdir -p ~/.config/pkm
nano ~/.config/pkm/credentials.env
# → 실제 인증 정보 입력
chmod 600 ~/.config/pkm/credentials.env
```
## 1단계: 프로젝트 구조 + requirements.txt ✅ 완료
생성된 파일: README.md, requirements.txt, .gitignore, credentials.env.example, 전체 디렉토리 구조
## 2단계: AI 분류 프롬프트 ✅ 완료
- MLX 서버(localhost:8800) OpenAI 호환 API로 전환됨
- Qwen3.5 thinking 모드 대응 완료 (JSON 추출 후처리)
- 프롬프트: scripts/prompts/classify_document.txt
## 3단계: AppleScript ✅ 완료
- applescript/auto_classify.scpt — Inbox 자동 분류
- applescript/omnifocus_sync.scpt — OmniFocus 작업 생성
## 4단계: 법령 모니터링 ⚠️ 부분 완료
- scripts/law_monitor.py 작성 완료
- 외국 법령 (US OSHA 1건, JP 厚労省 10건, EU-OSHA 5건) 수집 성공
- ❌ 한국 법령 API: IP 등록 미완 → Phase 2에서 해결
## 5단계: MailPlus 이메일 수집 ❌ 연결 실패
- scripts/mailplus_archive.py 코드 완성
- ❌ IMAP 접속 실패 (Connection refused) → Phase 2에서 해결
## 6단계: Daily Digest ⚠️ 미테스트
- scripts/pkm_daily_digest.py 코드 완성
- 실행 테스트 미진행 → Phase 2 이후 테스트
## 7단계: DEVONagent 가이드 ✅ 완료
- docs/devonagent-setup.md — 9개 검색 세트 설정 가이드
## 8단계: 전체 테스트 ❌ 미진행
- tests/test_classify.py 작성 완료
- docs/test-report.md 미생성 → Phase 4에서 진행
## 추가: PKM API 서버 (계획 외 생성)
- scripts/pkm_api_server.py — Flask REST API (포트 9900)
- DEVONthink + OmniFocus 상태 조회용
- 기본 동작 확인됨, 버그 있음 → Phase 2에서 수정
---
## 1단계: 프로젝트 구조 + requirements.txt
# Phase 1.5: GPU 서버 재구성 (Phase 2와 병행)
> 상세 계획: docs/gpu-restructure.md 참조
> GPU 서버 SSH 작업 + PKM 프로젝트 코드 수정
## GPU-1단계: GPU 서버 Ollama 모델 교체
```bash
# GPU 서버에서 실행 (ssh 192.168.1.186)
# 1. 기존 LLM 모델 제거
ollama rm qwen3.5:9b-q8_0
ollama rm id-9b
# 2. 임베딩/리랭킹 모델 설치
ollama pull bge-m3
ollama pull bge-reranker-v2-m3
# 3. no-think proxy 비활성화
sudo systemctl disable --now ollama-proxy
# 4. Ollama systemd 환경 조정
sudo systemctl edit ollama
# Environment="OLLAMA_MAX_LOADED_MODELS=2"
# Environment="OLLAMA_KEEP_ALIVE=30m"
sudo systemctl daemon-reload && sudo systemctl restart ollama
# 5. 검증
ollama list # → bge-m3, bge-reranker만 존재
curl localhost:11434/api/embed -d '{"model":"bge-m3","input":["테스트"]}' # → 1024차원 벡터
```
## GPU-2단계: tk-ai-service 코드 수정 (Mac Mini)
```
이 프로젝트의 디렉토리 구조를 만들고 기본 설정 파일들을 생성해줘.
작업 디렉토리: 현재 디렉토리 (~/Documents/code/DEVONThink_my server/)
tk-ai-service의 ollama_client.py를 OpenAI API 호환으로 리팩터링해줘.
프로젝트 구조:
./
├── README.md ← 프로젝트 설명
├── requirements.txt ← Python 패키지 목록
├── .gitignore ← credentials.env, venv, logs, __pycache__ 등 제외
├── credentials.env.example ← 인증 정보 템플릿 (값은 비움)
├── scripts/
│ ├── law_monitor.py
│ ├── mailplus_archive.py
│ ├── pkm_daily_digest.py
│ ├── embed_to_chroma.py
│ └── prompts/
│ └── classify_document.txt
├── applescript/
│ ├── auto_classify.scpt
│ └── omnifocus_sync.scpt
├── launchd/
│ ├── net.hyungi.pkm.law-monitor.plist
│ ├── net.hyungi.pkm.mailplus.plist
│ └── net.hyungi.pkm.daily-digest.plist
├── docs/
│ ├── devonagent-setup.md
│ └── deploy.md ← Mac mini 배포 방법
└── tests/
└── test_classify.py
변경 대상:
~/docker/tk-factory-services/ai-service/services/ollama_client.py
→ generate_text(): /api/chat → /v1/chat/completions
→ check_health(): /api/tags → /v1/models
→ generate_embedding(): 변경 없음 (GPU Ollama 유지)
requirements.txt에 넣을 패키지:
- chromadb
- requests
- python-dotenv
- schedule
- markdown
docker-compose.yml 환경변수:
OLLAMA_BASE_URL=http://host.internal:8800 (MLX)
OLLAMA_TEXT_MODEL=mlx-community/Qwen3.5-35B-A3B-4bit
OLLAMA_EMBED_URL=http://192.168.1.186:11434 (GPU)
.gitignore에 반드시 포함:
- credentials.env
- venv/
- logs/
- __pycache__/
- *.pyc
- .DS_Store
검증: docker compose build && docker compose up -d
curl http://localhost:30400/health → 모두 connected
```
deploy.md에는 Mac mini에서의 설치 절차 작성:
1. git pull
2. python3 -m venv venv && source venv/bin/activate
3. pip install -r requirements.txt
4. credentials.env는 ~/.config/pkm/credentials.env에 별도 관리
5. launchd plist 심볼릭 링크 등록 방법
## GPU-3단계: Docker + NFS + Komga 이전
네트워크 환경:
- NAS 도메인: ds1525.hyungi.net (Tailscale: 100.101.79.37, 포트: 15001)
- MailPlus: mailplus.hyungi.net:993 (IMAP SSL)
- WebDAV: webdav.hyungi.net/Document_Server/DEVONThink/
- TKSafety: tksafety.technicalkorea.net (나중에 활성화)
```bash
# GPU 서버에서 실행 (ssh 192.168.1.186)
# 1. Docker 설치
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo usermod -aG docker hyungi
# 2. NFS 마운트
sudo apt install nfs-common
sudo mkdir -p /mnt/comic
echo '192.168.1.227:/volume1/Comic /mnt/comic nfs4 ro,nosuid,noexec,nodev,soft,timeo=15 0 0' | sudo tee -a /etc/fstab
sudo mount -a
ls /mnt/comic # 확인
# 3. Komga 컨테이너 시작
sudo mkdir -p /opt/komga && cd /opt/komga
# docker-compose.yml 생성 (gpu-restructure.md Phase 1.5-3 참조)
docker compose up -d
# Mac Mini에서:
docker stop komga && docker rm komga
# nginx-ssl.conf: komga_backend upstream → 192.168.1.186:25600
docker restart home-service-proxy
# 검증
curl https://komga.hyungi.net # → GPU 서버 경유 접근
```
## GPU-4단계: Surya OCR 설치
```bash
# GPU 서버에서 실행 (ssh 192.168.1.186)
# 1. PyTorch CUDA 확인
python3 -c "import torch; print(torch.cuda.is_available())"
# False면: pip install torch --index-url https://download.pytorch.org/whl/cu124
# 2. Surya OCR 설치
sudo mkdir -p /opt/surya-ocr && cd /opt/surya-ocr
python3 -m venv venv
source venv/bin/activate
pip install surya-ocr fastapi uvicorn python-multipart
# 3. server.py 작성 (FastAPI 래퍼, gpu-restructure.md Phase 2-2 참조)
# 4. systemd 등록
# /etc/systemd/system/surya-ocr.service (gpu-restructure.md Phase 2-3 참조)
sudo systemctl daemon-reload
sudo systemctl enable --now surya-ocr
# 검증
curl -F "file=@test.pdf" http://localhost:8400/ocr
```
## GPU-5단계: Qdrant 통합 + PKM 코드 갱신
```
PKM 프로젝트 코드를 GPU 서버 재구성에 맞게 갱신해줘.
1. scripts/embed_to_chroma.py → scripts/embed_to_qdrant.py 리라이트
- chromadb → qdrant-client
- nomic-embed-text → bge-m3 (GPU 서버 192.168.1.186:11434)
- /api/embed 사용 (배치 지원)
- 텍스트 청킹 (500토큰, 50오버랩)
- 기존 embed_to_chroma.py는 git rm
2. applescript/auto_classify.scpt 수정
- Step 0: OCR 감지 + Surya OCR 호출 추가
- Step 4: embed_to_qdrant.py 호출로 변경
- 버그 픽스: 73행 sourceChannel 이중 설정 삭제
- baseDir 변수 사용 (12단계와 합산)
3. requirements.txt 업데이트 (9단계와 합산)
- chromadb, schedule 제거
+ qdrant-client, flask, gunicorn 추가
4. credentials.env.example: GPU_SERVER_IP 추가
5. Qdrant에 pkm_documents 컬렉션 생성 (1024차원, cosine)
검증: python3 scripts/embed_to_qdrant.py <테스트UUID> → Qdrant 벡터 저장
```
## GPU-6단계: architecture.md 대규모 갱신
```
docs/architecture.md를 GPU 서버 재구성에 맞게 전체 갱신해줘.
변경 규모: ChromaDB 28건, nomic-embed 12건, VL-7B 5건 — 문맥별 수정 필요
주요 변경:
- Tier 3 모델: nomic-embed-text → bge-m3, Qwen2.5-VL-7B → Surya OCR
- 벡터 DB: ChromaDB → Qdrant (모든 언급)
- VRAM 다이어그램: ~11.3GB → ~7-8GB
- Smart Rule 설계: embed_to_chroma → embed_to_qdrant, OCR 단계 추가
- 3-Tier AI 라우팅 전략 표 갱신
- 코드 예시 내 경로/모델명
※ 단순 치환 불가, 전체 문서를 통독하며 문맥에 맞게 수정할 것
※ 별도 커밋으로 분리
```
## GPU-7단계: RAG 파이프라인 구축 (후순위)
```
pkm_api_server.py에 RAG 엔드포인트를 추가해줘.
추가 엔드포인트:
POST /rag/query — 질문 → bge-m3 임베딩 → Qdrant 검색 → 리랭킹 → MLX 답변
POST /devonthink/embed — 단일 문서 임베딩 트리거
POST /devonthink/embed-batch — 배치 임베딩
docstring 갱신: "범위: DEVONthink + OmniFocus + RAG 검색"
scripts/ocr_preprocess.py 신규 작성:
DEVONthink UUID → AppleScript로 파일 경로 추출 → Surya API(GPU:8400) 호출
검증: curl -X POST localhost:9900/rag/query -d '{"q":"산업안전 법령"}'
```
---
## 2단계: Ollama 모델 확인 + 분류 프롬프트 테스트
# Phase 2: 인프라 수정 + 버그 픽스 (Phase 1.5와 병행)
> dev-roadmap.md Phase 2~3 해당
> ※ requirements.txt, AppleScript 경로, credentials.env는 GPU-5단계와 합산 진행
## 9단계: requirements.txt 수정 ← GPU-5단계와 합산
```
Ollama가 정상 동작하는지 확인하고, PKM 문서 분류용 프롬프트를 테스트해줘.
requirements.txt에 flask가 빠져있어. pkm_api_server.py에서 사용 중이니까 추가해줘.
GPU 서버 재구성에 따라 chromadb→qdrant-client 교체도 함께 진행.
gunicorn도 추가해 (프로덕션 WSGI 서버용).
schedule 패키지는 현재 미사용 — 제거할지 유지할지 판단해줘.
anthropic 패키지는 향후 Tier 2 연동용이니 유지.
1. ollama list로 현재 모델 확인
2. qwen3.5:35b-a3b 계열 모델이 있는지 확인 (없으면 알려줘)
3. 테스트 프롬프트 실행 — 아래 내용으로 분류 테스트:
수정할 파일: requirements.txt
테스트 문서: "산업안전보건법 시행규칙 일부개정령안 입법예고 - 고용노동부는 위험성평가에 관한 지침을 개정하여..."
추가:
+ flask>=3.0.0
+ gunicorn>=21.2.0
기대 응답 JSON:
{
"tags": ["위험성평가", "법령개정", "고용노동부"],
"domain_db": "04_Industrial safety",
"sub_group": "10_Legislation/Notice",
"sourceChannel": "inbox_route",
"dataOrigin": "external"
}
확인:
- schedule>=1.2.0 → 사용처 없으면 제거
```
도메인 DB 선택지:
00_Note_BOX, 01_Philosophie, 02_Language, 03_Engineering,
04_Industrial safety, 05_Programming, 07_General Book,
97_Production drawing, 99_Reference Data, 99_Technicalkorea
## 10단계: JP 번역 thinking 오염 수정
sourceChannel 값: tksafety, devonagent, law_monitor, inbox_route, email, web_clip, manual
dataOrigin 값: work (자사 업무), external (외부 참고)
```
법령 모니터링에서 일본어 번역 시 MLX Qwen3.5의 thinking 출력이 결과에 섞이는 문제를 수정해줘.
프롬프트를 최적화해서 ~/Documents/code/DEVONThink_my server/scripts/prompts/ 디렉토리에 저장해줘.
현재 문제:
로그에서 "Wait, I'll check if..." 같은 thinking 텍스트가 번역 결과에 포함됨.
위치: scripts/law_monitor.py의 JP 번역 호출부
수정 방향:
1. 번역 프롬프트에 /nothink 모드 명시 강화
2. llm_generate() 응답에서 thinking 패턴 필터링 추가
- "Wait,", "Let me", "I'll check", "Hmm," 등으로 시작하는 줄 제거
- "Final Output:" 이후 텍스트만 추출하는 로직
3. 또는 pkm_utils.py의 llm_generate()에 strip_thinking=True 옵션 추가
테스트: JP RSS 항목 하나로 번역 테스트하여 깨끗한 한글 출력 확인
```
## 11단계: API 서버 버그 수정
```
PKM API 서버의 두 가지 버그를 수정해줘.
위치: scripts/pkm_api_server.py
버그 1: /devonthink/stats → 500 Internal Server Error
- AppleScript 쿼리가 실패하는 것으로 추정
- 에러 로그 확인: logs/pkm-api.error.log
- AppleScript에서 DB 이름이나 property 접근 방식 수정 필요
버그 2: 한글 쿼리 파라미터 인코딩 에러
- /devonthink/search?q=산업안전 → 400 Bad request syntax
- Flask의 request.args는 UTF-8을 지원하므로, 클라이언트 측 문제일 가능성
- 서버 측에서도 방어 코드 추가: URL 디코딩 처리
추가 개선:
- /omnifocus/overdue 엔드포인트가 404였다가 나중에 추가됨 — 코드 확인
- / (루트) 접근 시 404 → /health로 리다이렉트 또는 엔드포인트 목록 반환
```
## 12단계: AppleScript 경로 변수화 ← GPU-5단계와 합산
```
AppleScript 파일들의 하드코딩된 경로를 변수로 교체해줘.
GPU-5단계의 embed_to_qdrant.py 변경, OCR 단계 추가, sourceChannel 버그 픽스도 함께 진행.
현재 하드코딩:
~/Documents/code/DEVONThink_my server/scripts/prompts/classify_document.txt
~/Documents/code/DEVONThink_my server/venv/bin/python3
~/Documents/code/DEVONThink_my server/scripts/embed_to_chroma.py
수정 대상:
applescript/auto_classify.scpt
applescript/omnifocus_sync.scpt
수정 방향:
- 스크립트 상단에 property baseDir 정의
- 모든 경로를 baseDir 기반으로 조합
- 또는 환경변수 PKM_HOME을 읽어서 사용
※ AppleScript에서 환경변수 읽기:
set pkmHome to do shell script "echo $PKM_HOME"
또는 property로 직접 지정하는 게 안정적
```
---
## 3단계: DEVONthink Smart Rule + AppleScript 배포
# Phase 3: API 서버 개선
## 13단계: gunicorn 전환 + launchd 등록
```
DEVONthink 4 Smart Rule용 AppleScript 모듈들을 만들어줘.
Mac mini에서 DEVONthink가 실행 중이야.
PKM API 서버를 Flask development server에서 gunicorn으로 전환하고,
launchd plist를 만들어 Mac mini 로그인 시 자동 시작되도록 해줘.
모듈 A: Ollama 연동 자동 분류 (~/Documents/code/DEVONThink_my server/applescript/auto_classify.scpt)
- DEVONthink Inbox DB에 새 문서가 들어오면 실행
- Ollama qwen3.5 35B에 문서 텍스트 전송
- 응답에서 tags, domain_db, sub_group, sourceChannel, dataOrigin 파싱
- DEVONthink 태그 설정 + 커스텀 메타데이터(sourceChannel, dataOrigin, lastAIProcess) 설정
- 해당 도메인 DB의 하위 그룹으로 문서 이동
- GPU 서버(Tailscale IP)로 벡터 임베딩 비동기 전송
1. gunicorn 설정:
gunicorn -w 2 -b 127.0.0.1:9900 pkm_api_server:app
※ AppleScript 실행 때문에 GUI 세션에서 실행해야 함
※ launchd의 LimitLoadToSessionType = Aqua 또는 LoginwindowUI
모듈 B: OmniFocus 연동 (~/Documents/code/DEVONThink_my server/applescript/omnifocus_sync.scpt)
- Projects DB에 새 문서 추가 시 TODO 패턴 감지
- OmniFocus에 작업 생성 (DEVONthink 링크 포함)
- 커스텀 메타데이터에 omnifocusTaskID 저장
2. launchd plist 생성:
launchd/net.hyungi.pkm.api-server.plist
- 로그인 시 자동 시작
- KeepAlive: true (크래시 시 재시작)
- WorkingDirectory: ~/Documents/code/DEVONThink_my server/
- StandardOutPath/StandardErrorPath: logs/pkm-api.log, logs/pkm-api.error.log
프롬프트 파일 위치: ~/Documents/code/DEVONThink_my server/scripts/prompts/
인증 정보: ~/.config/pkm/credentials.env
GPU 서버 Tailscale IP는 별도 확인 필요 (나중에 추가)
3. deploy.md에 API 서버 관련 내용 추가
```
## 14단계: API 엔드포인트 추가
```
PKM API 서버에 모니터링/상태 확인용 엔드포인트를 추가해줘.
추가할 엔드포인트:
1. GET /law-monitor/status
- data/law_last_check.json 읽어서 마지막 확인 시간 반환
- logs/law_monitor.log 최근 에러 건수
2. GET /digest/latest
- DEVONthink 00_Note_BOX/Daily_Digest/에서 최신 다이제스트 조회
- 또는 data/ 아래에 최근 다이제스트 캐시
3. GET / → 전체 엔드포인트 목록 반환 (현재 404)
4. GET /health 확장
- MLX 서버 상태 (localhost:8800 연결 가능 여부)
- DEVONthink 실행 상태
- 각 launchd job 상태
```
---
## 4단계: 법령 모니터링 스크립
# Phase 4: 테스
## 15단계: 모듈별 테스트 실행
```
한국 법령 변경 모니터링 스크립트를 만들어줘.
Mac mini에서 각 모듈의 동작을 확인해줘.
스크립트: ~/Documents/code/DEVONThink_my server/scripts/law_monitor.py
인증: ~/.config/pkm/credentials.env의 LAW_OC 값 사용
API: open.law.go.kr OpenAPI
1. AI 분류 테스트 (tests/test_classify.py)
cd ~/Documents/code/DEVONThink_my\ server/
source venv/bin/activate
python tests/test_classify.py
→ 5종 문서 분류 정확도 확인
기능:
1. 산업안전보건법, 중대재해처벌법, 관련 시행령/시행규칙/고시 변경 추적
2. 변경 감지 시:
- 법령 본문(XML) 다운로드 → ~/Documents/code/DEVONThink_my server/data/laws/에 저장
- DEVONthink 04_Industrial Safety/10_Legislation/ 하위에 자동 임포트 (AppleScript 호출)
- 커스텀 메타데이터: sourceChannel=law_monitor, dataOrigin=external
- 로그: ~/Documents/code/DEVONThink_my server/logs/law_monitor.log
3. launchd plist 생성: 매일 07:00 실행
~/Documents/code/DEVONThink_my server/launchd/net.hyungi.pkm.law-monitor.plist
→ ~/Library/LaunchAgents/에 심볼릭 링크
2. 법령 모니터링 (Phase 2 인프라 수정 후)
python scripts/law_monitor.py
→ 한국 법령 API 정상 응답 확인
→ 외국 법령 수집 재확인
※ 법령 API 승인 대기중이라 스크립트만 만들고 실제 테스트는 승인
※ 해외 법령(US OSHA, JP, EU)은 나중에 추가 예정
3. MailPlus 이메일 수집 (Phase 2 연결 수정)
python scripts/mailplus_archive.py
→ IMAP 접속 + 이메일 가져오기 확인
4. Daily Digest
python scripts/pkm_daily_digest.py
→ 다이제스트 MD 파일 생성 확인
5. API 서버 (Phase 2 버그 수정 후)
python scripts/pkm_api_server.py &
curl http://localhost:9900/health
curl http://localhost:9900/devonthink/stats
curl http://localhost:9900/devonthink/inbox-count
curl "http://localhost:9900/devonthink/search?q=safety&limit=3"
curl http://localhost:9900/omnifocus/stats
```
## 16단계: E2E 통합 테스트
```
PKM 시스템 End-to-End 테스트를 진행해줘.
시나리오 1: Inbox → 자동분류 플로우
1. DEVONthink Inbox에 테스트 문서 추가
2. Smart Rule 트리거 → auto_classify.scpt 실행 확인
3. 태그, 커스텀 메타데이터(sourceChannel, dataOrigin, lastAIProcess) 확인
4. 올바른 도메인 DB + 하위 그룹으로 이동 확인
시나리오 2: 법령 → 다이제스트 플로우
1. law_monitor.py 수동 실행
2. data/laws/에 파일 생성 + DEVONthink 04_Industrial Safety 임포트 확인
3. pkm_daily_digest.py 실행 → 법령 변경 건 포함 확인
시나리오 3: OmniFocus 연동
1. Projects DB에 TODO 패턴 문서 추가
2. omnifocus_sync.scpt 트리거 확인
3. OmniFocus Inbox에 작업 생성 + DEVONthink 링크 확인
4. 커스텀 메타데이터 omnifocusTaskID 확인
시나리오 4: launchd 스케줄 확인
launchctl list | grep pkm
→ 3개(+API 서버) 등록 확인
각 항목 pass/fail 리포트 → docs/test-report.md
```
---
## 5단계: MailPlus → DEVONthink 이메일 수집
# Phase 5: 운영 안정화 (나중에)
## 17단계: 로그 로테이션 + 알림
```
MailPlus 이메일을 DEVONthink Archive DB로 자동 수집하는 스크립트를 만들어줘.
운영 안정성을 위한 설정을 추가해줘.
스크립트: ~/Documents/code/DEVONThink_my server/scripts/mailplus_archive.py
인증: ~/.config/pkm/credentials.env
1. Python 로그 로테이션
- pkm_utils.py의 setup_logger()에 RotatingFileHandler 적용
- maxBytes=10MB, backupCount=5
접속 정보:
- IMAP 서버: mailplus.hyungi.net:993 (SSL)
- 계정: hyungi
2. Synology Chat 알림 (CHAT_WEBHOOK_URL 설정 후)
- 법령 변경 감지 시 알림
- 에러 발생 시 알림
- Daily Digest 요약 알림
기능:
1. IMAP으로 MailPlus 접속
2. 마지막 동기화 이후 새 메일 가져오기
3. DEVONthink Archive DB에 임포트 (AppleScript 호출)
4. 커스텀 메타데이터: sourceChannel=email
5. 안전 관련 키워드 감지 시 dataOrigin 자동 판별
launchd: 매일 07:00 + 18:00 실행
~/Documents/code/DEVONThink_my server/launchd/net.hyungi.pkm.mailplus.plist
3. 에러 모니터링
- pkm_daily_digest.py에 이미 에러 카운트 로직 있음
- 임계값 초과 시 Chat 알림 추가
```
---
## 6단계: Daily Digest 시스템
## 18단계: 문서 보완
```
PKM 일일 다이제스트를 자동 생성하는 스크립트를 만들어줘.
프로젝트 문서를 보완해줘.
스크립트: ~/Documents/code/DEVONThink_my server/scripts/pkm_daily_digest.py
인증: ~/.config/pkm/credentials.env
기능:
1. DEVONthink에서 오늘 추가/수정된 문서 집계 (AppleScript로 쿼리)
- DB별 신규 건수
- sourceChannel별 구분
2. law_monitor 로그에서 법령 변경 건 파싱
3. OmniFocus 오늘 완료/추가/기한초과 집계 (AppleScript)
4. 상위 뉴스 3건 요약 (Ollama 35B 호출)
5. MD 파일 생성 → DEVONthink 00_Note_BOX/Daily_Digest/에 저장
파일명: YYYY-MM-DD_digest.md
6. OmniFocus 액션 자동 생성 (법령변경, overdue, Inbox 미처리 등)
7. 90일 지난 다이제스트 → 90_Archive 이동 (Smart Rule 대체)
launchd: 매일 20:00 실행
~/Documents/code/DEVONThink_my server/launchd/net.hyungi.pkm.daily-digest.plist
※ Synology Chat 웹훅 알림은 나중에 추가 (CHAT_WEBHOOK_URL 설정 후)
```
---
## 7단계: DEVONagent 검색 세트 (수동 가이드)
```
DEVONagent Pro에서 안전 분야 자동 검색 세트를 설정하는 가이드를 만들어줘.
총 9개 검색 세트 (7 안전 + 2 기술):
1. 국내 산업안전 뉴스 (kosha, moel, safetynews 등)
2. 국내 중대재해 뉴스
3. KOSHA 가이드/지침
4. 국내 산업안전 학술/논문
5. US OSHA / Safety+Health Magazine
6. JP 厚生労働省 / 安全衛生
7. EU-OSHA
8. 기술 뉴스 (AI/서버/네트워크)
9. 프로그래밍 기술 동향
각 세트별로:
- 검색 키워드/연산자
- 사이트 제한
- 스케줄 (매일/주간)
- 수량 제한 (주간 합계 50~85건 수준)
- 결과 → DEVONthink Inbox로 전송 설정 방법
이건 DEVONagent GUI에서 수동 설정해야 하니까,
단계별 가이드 문서를 ~/Documents/code/DEVONThink_my server/docs/devonagent-setup.md로 만들어줘.
```
---
## 8단계: 전체 테스트
```
PKM 시스템 전체 End-to-End 테스트를 진행해줘.
테스트 항목:
1. Ollama 분류 테스트 — 5종 문서(법령, 뉴스, 논문, 메모, 이메일) 분류 정확도
2. DEVONthink Smart Rule — Inbox에 테스트 문서 추가 → 자동 분류 확인
3. sourceChannel/dataOrigin 메타데이터가 정상 설정되는지
4. OmniFocus 연동 — TODO 패턴 문서 → 작업 자동 생성
5. MailPlus IMAP 접속 테스트
6. launchd 스케줄 등록 확인 (launchctl list | grep pkm)
7. Daily Digest 수동 실행 테스트
각 항목 pass/fail 리포트를 ~/Documents/code/DEVONThink_my server/docs/test-report.md로 저장해줘.
1. README.md — 아키텍처 다이어그램, 기능 목록, 시작 가이드 확장
2. deploy.md — API 서버 배포, 트러블슈팅 섹션, macOS 요구사항 추가
3. docs/troubleshooting.md — 자주 발생하는 문제와 해결 방법
```
---
@@ -295,8 +502,29 @@ PKM 시스템 전체 End-to-End 테스트를 진행해줘.
```
Mac mini 접속: SSH (MacBook Pro → Mac mini)
NAS 도메인: ds1525.hyungi.net (Tailscale: 100.101.79.37, 포트: 15001)
MailPlus: mailplus.hyungi.net:993 (IMAP SSL)
MailPlus: mailplus.hyungi.net:993 (IMAP SSL) ← 현재 연결 불가, 확인 필요
WebDAV: webdav.hyungi.net/Document_Server/DEVONThink/
TKSafety: tksafety.technicalkorea.net (나중에 활성화)
내부 네트워크: Tailscale VPN 연결됨
Gitea: https://git.hyungi.net/hyungi/devonthink_home.git
```
## 참고: Git 커밋 히스토리
```
3506214 fix(law_monitor): US 타입 필터 제거 + JP RDF 네임스페이스 수정
c8e30b5 fix: AppleScript POSIX path 변수 방식 + 단일 -e 실행으로 따옴표 문제 해결
f13b998 fix: AppleScript 행별 -e 분할 실행 — 파일 방식 인코딩 문제 회피
735c072 fix: AppleScript를 임시 파일로 실행 — osascript -e 이스케이프 문제 해결
446963c fix(law_monitor): AppleScript f-string 제거 + EU 파일명 고유화
0b950a4 fix(law_monitor): AppleScript 따옴표 이스케이프 수정
6a44b10 fix(law_monitor): JP/EU RSS URL 수정 — news.rdf + rss.xml, RDF 네임스페이스 대응
9dc0694 feat(law_monitor): 외국 법령 지원 추가 — US OSHA, JP 厚労省(MLX 번역), EU-OSHA
ec6074d fix(law_monitor): API 에러 응답 로깅 추가 — 인증 실패 시 조용히 넘어가던 문제
aca4a02 fix: LLM thinking 허용 + 마지막 유효 JSON 추출 방식으로 변경
49c39a1 fix: LLM thinking 출력 대응 — max_tokens 증가 + JSON 추출 강화
948be16 fix: Qwen3.5 /nothink 모드 + json_mode 파라미터 추가
a774771 fix: MLX 서버(localhost:8800) 대응 — Ollama API → OpenAI 호환 변경
084d3a8 feat: 전체 PKM 스크립트 일괄 작성 — 분류/법령/메일/다이제스트/임베딩
bec9579 chore: 프로젝트 구조 + 설계 문서 초기 커밋
```

View File

@@ -1,5 +1,17 @@
# Mac mini 배포 가이드
> 마지막 업데이트: 2026-03-29
> 대상: Mac mini M4 Pro (macOS, Python 3.11+)
## 요구사항
- macOS 14+ (Sonoma 이상)
- Python 3.11+ (Homebrew 설치 권장)
- DEVONthink 4 — 실행 중이어야 AppleScript 동작
- OmniFocus 4 — 실행 중이어야 AppleScript 동작
- MLX 서버 — Qwen3.5-35B-A3B, localhost:8800에서 실행 중
- Tailscale — NAS 및 GPU 서버 접근용
## 1. 초기 설치
```bash
@@ -22,9 +34,44 @@ nano ~/.config/pkm/credentials.env
chmod 600 ~/.config/pkm/credentials.env
```
credentials.env.example을 참고하여 실제 값 입력.
credentials.env.example을 참고하여 실제 값 입력:
## 3. launchd 스케줄 등록
```
# 필수
LAW_OC=<법령API키>
MAILPLUS_HOST=mailplus.hyungi.net
MAILPLUS_PORT=993
MAILPLUS_USER=hyungi
MAILPLUS_PASS=<비밀번호>
NAS_DOMAIN=ds1525.hyungi.net
NAS_TAILSCALE_IP=100.101.79.37
NAS_PORT=15001
# 선택 (향후)
CLAUDE_API_KEY=<키>
#CHAT_WEBHOOK_URL=<Synology Chat 웹훅>
#GPU_SERVER_IP=<Tailscale IP>
```
## 3. 한국 법령 API IP 등록
법령 API 호출 전에 Mac mini의 공인 IP를 등록해야 합니다.
```bash
# Mac mini에서 공인 IP 확인
curl -s ifconfig.me
# open.law.go.kr → 로그인 → 마이페이지 → 인증키 관리
# 위에서 확인한 IP를 서버 IP로 등록
# ※ Tailscale IP(100.x.x.x)가 아니라 실제 공인 IP
# 등록 후 테스트
source venv/bin/activate
python scripts/law_monitor.py
# → "법령 API 에러" 없이 정상 동작 확인
```
## 4. launchd 스케줄 등록
```bash
# 심볼릭 링크 생성
@@ -41,7 +88,38 @@ launchctl load ~/Library/LaunchAgents/net.hyungi.pkm.daily-digest.plist
launchctl list | grep pkm
```
## 4. 수동 테스트
## 5. PKM API 서버 실행
```bash
# 개발 모드 (수동 실행)
cd ~/Documents/code/DEVONThink_my\ server/
source venv/bin/activate
python scripts/pkm_api_server.py
# 프로덕션 모드 (gunicorn, Phase 3 이후)
# gunicorn -w 2 -b 127.0.0.1:9900 scripts.pkm_api_server:app
# 동작 확인
curl http://localhost:9900/health
curl http://localhost:9900/devonthink/inbox-count
```
API 서버는 GUI 세션에서 실행해야 합니다 (AppleScript가 DEVONthink/OmniFocus GUI에 접근).
## 6. DEVONthink Smart Rule 설정
1. DEVONthink → Preferences → Smart Rules
2. 새 Rule: **"AI Auto Classify"**
- Event: On Import
- Database: Inbox
- Condition: Tags is empty
- Action: Execute Script → External → `applescript/auto_classify.scpt`
3. 새 Rule: **"OmniFocus Sync"**
- Event: On Import
- Database: Projects
- Action: Execute Script → External → `applescript/omnifocus_sync.scpt`
## 7. 수동 테스트
```bash
cd ~/Documents/code/DEVONThink_my\ server/
@@ -51,31 +129,26 @@ source venv/bin/activate
python3 scripts/law_monitor.py
python3 scripts/mailplus_archive.py
python3 scripts/pkm_daily_digest.py
# AI 분류 테스트
python3 tests/test_classify.py
```
## 5. DEVONthink Smart Rule 설정
1. DEVONthink → Preferences → Smart Rules
2. 새 Rule: "AI Auto Classify"
- Event: On Import
- Database: Inbox
- Condition: Tags is empty
- Action: Execute Script → External → `applescript/auto_classify.scpt`
3. 새 Rule: "OmniFocus Sync"
- Event: On Import
- Database: Projects
- Action: Execute Script → External → `applescript/omnifocus_sync.scpt`
## 6. 업데이트
## 8. 업데이트
```bash
cd ~/Documents/code/DEVONThink_my\ server/
git pull
source venv/bin/activate
pip install -r requirements.txt
# launchd 재로드 (plist가 변경된 경우만)
launchctl unload ~/Library/LaunchAgents/net.hyungi.pkm.law-monitor.plist
launchctl load ~/Library/LaunchAgents/net.hyungi.pkm.law-monitor.plist
# (나머지도 동일)
```
## 7. 로그 확인
## 9. 로그 확인
```bash
# 스크립트 로그
@@ -83,14 +156,94 @@ tail -f logs/law_monitor.log
tail -f logs/mailplus.log
tail -f logs/digest.log
# launchd 로그
tail -f logs/law_monitor_launchd.log
# API 서버 로그
tail -f logs/pkm-api.log
tail -f logs/pkm-api.error.log
# launchd 시스템 로그
log show --predicate 'process == "python3"' --last 1h
```
## 10. 일일 운영 점검
```bash
# 1. launchd 작업 상태
launchctl list | grep pkm
# 2. 오늘의 로그 에러 확인
grep -c ERROR logs/law_monitor.log
grep -c ERROR logs/mailplus.log
# 3. 법령 마지막 확인 시간
cat data/law_last_check.json | python3 -m json.tool
# 4. DEVONthink Inbox 미처리 건수 (API 서버 실행 중이면)
curl -s http://localhost:9900/devonthink/inbox-count
# 5. MLX 서버 상태
curl -s http://localhost:8800/v1/models | python3 -m json.tool
```
## 실행 스케줄
| 스크립트 | 시간 | 용도 |
|---------|------|------|
| law_monitor.py | 매일 07:00 | 법령 변경 모니터링 |
| mailplus_archive.py | 매일 07:00, 18:00 | 이메일 수집 |
| pkm_daily_digest.py | 매일 20:00 | 일일 다이제스트 |
| law_monitor.py | 매일 07:00 | 법령 변경 모니터링 (한국+US/JP/EU) |
| mailplus_archive.py | 매일 07:00, 18:00 | MailPlus 이메일 수집 |
| pkm_daily_digest.py | 매일 20:00 | 일일 다이제스트 생성 |
| pkm_api_server.py | 상시 (수동/launchd) | REST API (포트 9900) |
## 트러블슈팅
### 법령 API "사용자 정보 검증 실패"
```
원인: Mac mini 공인 IP가 open.law.go.kr에 등록되지 않음
해결:
1. curl -s ifconfig.me 로 현재 공인 IP 확인
2. open.law.go.kr → 마이페이지 → 인증키 관리 → IP 등록
3. IP가 변경되면 다시 등록 필요 (고정 IP 아닌 경우)
```
### MailPlus IMAP Connection refused
```
확인 순서:
1. Synology DSM → MailPlus Server → 서비스 상태 확인
2. IMAP 활성화: DSM → MailPlus Server → 메일 전송 → IMAP 탭
3. 포트: 993(SSL) vs 143(STARTTLS)
4. 방화벽: Synology 방화벽에서 993 포트 확인
5. Tailscale 직접:
python3 -c "import imaplib; m=imaplib.IMAP4_SSL('100.101.79.37', 993); print('OK')"
```
### AppleScript 실행 오류
```
확인:
1. DEVONthink, OmniFocus가 GUI로 실행 중인지 확인
2. 접근성 권한: 시스템 설정 → 개인정보 보호 → 접근성 → python/osascript 허용
3. 수동 테스트:
osascript -e 'tell application "DEVONthink 3" to get name of databases'
```
### MLX 서버 응답 없음
```
확인:
1. MLX 서버 프로세스 확인: ps aux | grep mlx
2. 포트 확인: lsof -i :8800
3. 모델 로드 확인: curl http://localhost:8800/v1/models
4. 재시작 필요 시: (MLX 서버 시작 명령어 실행)
```
### Daily Digest에 데이터가 비어있음
```
확인:
1. DEVONthink이 실행 중인지 확인
2. OmniFocus가 실행 중인지 확인
3. 로그 확인: tail -20 logs/digest.log
4. AppleScript 직접 테스트:
osascript -e 'tell application "DEVONthink 3" to get count of databases'
```

399
docs/dev-roadmap.md Normal file
View File

@@ -0,0 +1,399 @@
# PKM 시스템 개발 로드맵
> 작성일: 2026-03-29 (GPU 서버 재구성 계획 통합: 2026-03-29)
> 현재 상태: Phase 1 코드 작성 완료(90%), 인프라 일부 미해결 → Phase 1.5(GPU 재구성) + Phase 2 진행 중
---
## 현재 완료된 것
| 단계 | 항목 | 상태 | 비고 |
|------|------|------|------|
| 1 | 프로젝트 구조 | ✅ 완료 | README, deploy.md, .gitignore |
| 2 | AI 분류 프롬프트 | ✅ 완료 | MLX(Qwen3.5) OpenAI 호환 API 전환 완료 |
| 3 | AppleScript | ✅ 완료 | auto_classify + omnifocus_sync |
| 4 | 법령 모니터링 | ⚠️ 부분 | 외국(US/JP/EU) OK, 한국 API 인증 실패 |
| 5 | MailPlus 수집 | ❌ 연결 실패 | IMAP Connection refused |
| 6 | Daily Digest | ⚠️ 미테스트 | 코드 완성, 실행 기록 없음 |
| 7 | DEVONagent 가이드 | ✅ 완료 | docs/devonagent-setup.md |
| 8 | 전체 테스트 | ❌ 미진행 | test_classify.py만 존재 |
| 추가 | PKM API 서버 | ⚠️ 부분 | 한글 인코딩, stats 500 에러 |
---
## Phase 1.5: GPU 서버 재구성 (Phase 2와 병행)
> 상세 계획: docs/gpu-restructure.md 참조
GPU 서버(RTX 4070Ti Super)의 역할을 LLM 추론에서 임베딩/OCR 특화로 전환한다.
Mac Mini와 중복되는 LLM 모델을 제거하고, Surya OCR + bge-m3를 배치한다.
### 1.5-A. GPU 서버 정리 (SSH 작업)
```
작업 내용:
1. Ollama 모델 제거: qwen3.5:9b-q8_0, id-9b
2. 새 모델 설치: bge-m3 (임베딩), bge-reranker-v2-m3 (리랭킹)
3. no-think proxy(11435) 비활성화
4. paperless-gpt 처리 방침 결정
5. tk-ai-service 코드 수정 (Ollama API → OpenAI API 전환)
검증: ollama list → bge-m3, bge-reranker만 존재
검증: tk-ai-service /health → MLX(text), GPU(embed) 모두 connected
```
### 1.5-B. Docker + NFS + Komga 이전
```
작업 내용:
1. GPU 서버에 Docker Engine 설치
2. NAS NFS 마운트 설정 (192.168.1.227:/volume1/Comic → /mnt/comic, ro)
3. Komga Docker 컨테이너를 GPU 서버로 이전 (포트 25600 유지)
4. Mac Mini nginx upstream 변경 → GPU 서버
5. Mac Mini Komga 제거 (Docker VM 메모리 1.23GB 회수)
검증: curl https://komga.hyungi.net → GPU 서버 경유 접근 확인
```
### 1.5-C. Surya OCR 설치
```
작업 내용:
1. PyTorch CUDA 런타임 확인/설치
2. Surya OCR FastAPI 서버 구성 (/opt/surya-ocr/, 포트 8400)
3. systemd 서비스 등록
검증: curl -F "file=@test.pdf" http://192.168.1.186:8400/ocr → OCR 텍스트 반환
```
### 1.5-D. PKM 코드 갱신 (Phase 2와 겹치는 항목 포함)
```
작업 내용:
1. embed_to_chroma.py → embed_to_qdrant.py 리라이트 (Qdrant + bge-m3)
2. auto_classify.scpt: Step 0(OCR) 추가 + Step 4 Qdrant + sourceChannel 버그 픽스
3. requirements.txt: chromadb→qdrant-client, flask/gunicorn 추가 ← Phase 2 1-1과 합산
4. credentials.env: GPU_SERVER_IP=192.168.1.186 추가
5. architecture.md 대규모 갱신 (ChromaDB 28건, nomic 12건, VL-7B 5건)
검증: python3 scripts/embed_to_qdrant.py <테스트UUID> → Qdrant 벡터 저장 확인
```
### 1.5-E. RAG 파이프라인 + OCR 연동 (후순위)
```
작업 내용:
1. pkm_api_server.py에 RAG 엔드포인트 추가 (/rag/query, /devonthink/embed)
2. DEVONthink Smart Rule에 OCR 전처리 단계 추가
3. ocr_preprocess.py 신규 작성
검증: curl -X POST localhost:9900/rag/query -d '{"q":"산업안전 법령"}' → 답변 반환
```
---
## Phase 2: 인프라 수정 (Phase 1.5와 병행)
Mac mini에서 직접 확인/수정이 필요한 항목들.
※ Phase 1.5(GPU 재구성)과 병행하여 진행. 겹치는 항목(requirements.txt, credentials.env, AppleScript)은 합산.
### 1-1. requirements.txt 수정 ← Phase 1.5-D와 합산 진행
```
현재 문제:
- flask 누락 (pkm_api_server.py에서 사용 중)
- schedule 패키지 미사용 (제거 고려)
- chromadb → qdrant-client 교체 (GPU 재구성에 따라)
수정 내용:
+ flask>=3.0.0
+ gunicorn>=21.2.0 (프로덕션 WSGI)
+ qdrant-client>=1.7.0
- chromadb>=0.4.0
- schedule>=1.2.0 (미사용 확인 후 제거)
```
### 1-2. 한국 법령 API 인증 해결
```
현재 에러:
"사용자 정보 검증에 실패하였습니다.
OPEN API 호출 시 사용자 검증을 위하여 정확한 서버장비의 IP주소 및 도메인주소를 등록해 주세요."
조치:
1. open.law.go.kr 접속 → 마이페이지 → 인증키 관리
2. Mac mini의 외부 IP 확인: curl ifconfig.me
3. 해당 IP를 API 호출 서버 IP로 등록
4. Tailscale IP(100.x.x.x)가 아니라 실제 공인 IP여야 함
5. 등록 후 law_monitor.py 재실행하여 확인
```
### 1-3. MailPlus IMAP 연결 수정
```
현재 에러: [Errno 61] Connection refused (mailplus.hyungi.net:993)
확인 순서:
1. Synology DSM → MailPlus Server → 서비스 상태 확인
2. IMAP 활성화 여부: DSM → MailPlus Server → 메일 전송 → IMAP 탭
3. 포트 확인: 993(SSL) vs 143(STARTTLS)
4. 방화벽: Synology 방화벽에서 993 포트 개방 확인
5. DNS 확인: nslookup mailplus.hyungi.net → 올바른 IP?
6. Tailscale 경유 시: 100.101.79.37:993으로 직접 테스트
python3 -c "import imaplib; m=imaplib.IMAP4_SSL('100.101.79.37', 993); print('OK')"
7. credentials.env에 MAILPLUS_HOST 값 확인
```
### 1-4. launchd 등록 상태 확인 및 등록
```bash
# Mac mini에서 확인
launchctl list | grep pkm
# 미등록 시 심볼릭 링크 생성
ln -sf ~/Documents/code/DEVONThink_my\ server/launchd/net.hyungi.pkm.law-monitor.plist ~/Library/LaunchAgents/
ln -sf ~/Documents/code/DEVONThink_my\ server/launchd/net.hyungi.pkm.mailplus.plist ~/Library/LaunchAgents/
ln -sf ~/Documents/code/DEVONThink_my\ server/launchd/net.hyungi.pkm.daily-digest.plist ~/Library/LaunchAgents/
# 로드
launchctl load ~/Library/LaunchAgents/net.hyungi.pkm.law-monitor.plist
launchctl load ~/Library/LaunchAgents/net.hyungi.pkm.mailplus.plist
launchctl load ~/Library/LaunchAgents/net.hyungi.pkm.daily-digest.plist
```
---
## Phase 2: 버그 픽스 (코드 수정)
### 2-1. JP 번역 thinking 오염 문제
```
현재: MLX Qwen3.5가 번역 시 "Wait, I'll check if..." 같은 thinking을 출력에 포함
위치: scripts/law_monitor.py의 JP 번역 호출부
수정 방향:
- 번역 프롬프트에 /nothink 모드 명시 강화
- llm_generate() 호출 시 thinking 출력 후처리 추가
- "Wait,", "Let me", "I'll check" 등 패턴 필터링
- 또는 번역 결과에서 첫 번째 유효 문장만 추출
```
### 2-2. PKM API 서버 한글 인코딩
```
현재: /devonthink/search?q=산업안전 → 400 Bad request syntax
위치: scripts/pkm_api_server.py
수정:
- Flask 자체는 UTF-8 지원하므로, 클라이언트 측 URL 인코딩 문제일 가능성
- curl 호출 시 --data-urlencode 사용 또는 퍼센트 인코딩 필요
- 서버 측에서도 request.args.get('q', '') 기본 인코딩 확인
```
### 2-3. /devonthink/stats 500 에러 수정
```
현재: GET /devonthink/stats → 500 Internal Server Error
위치: scripts/pkm_api_server.py
원인 추정: AppleScript 실행 시 DB 이름이나 경로 문제
수정: 에러 로그 확인 후 AppleScript 쿼리 수정
```
### 2-4. AppleScript 하드코딩 경로 개선
```
현재:
- ~/Documents/code/DEVONThink_my server/scripts/prompts/ 하드코딩
- venv 경로 하드코딩
수정:
- 스크립트 상단에 BASE_DIR 변수 정의
- 또는 환경변수 PKM_HOME으로 통일
```
### 2-5. requirements.txt 정리
```
추가: flask>=3.0.0, gunicorn>=21.2.0
유지: anthropic (향후 Tier 2용)
검토: schedule (미사용이면 제거)
```
---
## Phase 3: API 서버 개선
### 3-1. 프로덕션 서빙
```
현재: Flask development server
변경: gunicorn + launchd로 안정 운영
launchd plist 추가:
net.hyungi.pkm.api-server.plist
→ gunicorn -w 2 -b 127.0.0.1:9900 pkm_api_server:app
```
### 3-2. 엔드포인트 보완
```
현재 엔드포인트:
GET /health
GET /devonthink/stats ← 500 에러 수정 필요
GET /devonthink/search
GET /devonthink/inbox-count
GET /omnifocus/stats
GET /omnifocus/today
GET /omnifocus/overdue
추가 고려:
GET /law-monitor/status ← 마지막 실행 결과, 다음 실행 시간
GET /digest/latest ← 최근 다이제스트 조회
POST /classify ← 수동 분류 요청 (테스트용)
```
### 3-3. 간단한 인증 추가 (선택)
```
localhost 전용이면 불필요하지만, Tailscale 내부에서 접근할 경우:
- Bearer token 방식 (credentials.env에 API_TOKEN 추가)
- 또는 IP 화이트리스트 (127.0.0.1 + Tailscale 대역)
```
---
## Phase 4: 테스트 & 검증
### 4-1. 개별 모듈 테스트
```bash
# Mac mini에서 실행
# 1. AI 분류 테스트 (5종 문서)
cd ~/Documents/code/DEVONThink_my\ server/
source venv/bin/activate
python tests/test_classify.py
# 2. 법령 모니터링 (한국 API 인증 후)
python scripts/law_monitor.py
# 3. MailPlus (IMAP 수정 후)
python scripts/mailplus_archive.py
# 4. Daily Digest
python scripts/pkm_daily_digest.py
# 5. API 서버
python scripts/pkm_api_server.py &
curl http://localhost:9900/health
curl http://localhost:9900/devonthink/stats
curl http://localhost:9900/devonthink/inbox-count
curl "http://localhost:9900/devonthink/search?q=safety&limit=3"
```
### 4-2. E2E 통합 테스트
```
시나리오 1: Inbox → 자동분류 플로우
1. DEVONthink Inbox에 테스트 문서 추가
2. Smart Rule 트리거 → auto_classify.scpt 실행 확인
3. 태그, 메타데이터, DB 이동 확인
시나리오 2: 법령 → 다이제스트 플로우
1. law_monitor.py 수동 실행
2. data/laws/에 파일 생성 확인
3. DEVONthink 04_Industrial Safety 확인
4. pkm_daily_digest.py 실행 → 법령 변경 건 포함 확인
시나리오 3: OmniFocus 연동
1. Projects DB에 TODO 패턴 문서 추가
2. omnifocus_sync.scpt 트리거 확인
3. OmniFocus Inbox에 작업 생성 확인
```
### 4-3. 테스트 리포트 작성
```
→ docs/test-report.md
각 항목별 pass/fail + 스크린샷/로그 첨부
```
---
## Phase 5: 운영 안정화 (선택)
### 5-1. 모니터링
```
- 로그 로테이션 (logrotate 또는 Python RotatingFileHandler)
- Synology Chat 웹훅 알림 연동 (CHAT_WEBHOOK_URL 설정 후)
- 에러 발생 시 즉시 알림
```
### 5-2. 백업
```
- Gitea 리포지토리 자동 백업 (이미 NAS에 있으므로 OK)
- credentials.env 백업 (Vaultwarden에 보관?)
- Qdrant 데이터 백업 (pkm_documents + tk_qc_issues 컬렉션)
```
### 5-3. 문서 보완
```
- README.md 상세화 (아키텍처 다이어그램, 기능 목록)
- 트러블슈팅 가이드 추가
- deploy.md에 API 서버 + 업데이트 절차 추가
```
---
## 작업 순서 요약
```
[GPU] Phase 1.5: GPU 서버 재구성 (Phase 2와 병행)
1.5-A. GPU 서버 정리 (모델 교체, proxy 제거) ← SSH 작업
1.5-B. Docker + NFS + Komga 이전 ← SSH 작업
1.5-C. Surya OCR 설치 ← SSH 작업
1.5-D. PKM 코드 갱신 (Qdrant, 임베딩) ← 코드 수정 후 push
1.5-E. RAG + OCR 연동 ← 후순위
[즉시] Phase 2: 인프라 수정
1-1. requirements.txt 수정 ← Phase 1.5-D와 합산
1-2. 한국 법령 API IP 등록 ← Mac mini에서 공인IP 확인
1-3. MailPlus IMAP 확인 ← Synology DSM 확인
1-4. launchd 등록 ← Mac mini에서 실행
[코드] Phase 3: 버그 픽스
2-1. JP 번역 thinking 필터링 ← 코드 수정 후 push
2-2~3. API 서버 수정 ← 코드 수정 후 push
2-4. AppleScript 경로 변수화 ← Phase 1.5-D와 합산
2-5. requirements.txt 정리 ← Phase 1.5-D와 합산
[개선] Phase 4: API 서버 개선
3-1. gunicorn 전환 + launchd plist ← 코드 작성 후 push
3-2~3. 엔드포인트 추가 ← 필요시
[검증] Phase 5: 테스트
4-1~2. 모듈별 + E2E 테스트 ← Mac mini에서 실행
4-3. 테스트 리포트 ← 결과 기반 작성
[안정] Phase 6: 운영 안정화 ← 여유 있을 때
```
---
## 예상 소요 시간
| Phase | 예상 시간 | 비고 |
|-------|-----------|------|
| Phase 1.5-A~C | 3~4시간 | GPU 서버 SSH 작업 (모델 교체, Docker, Surya) |
| Phase 1.5-D | 3~4시간 | PKM 코드 갱신 (Qdrant, architecture.md 대규모 수정) |
| Phase 1.5-E | 2~3시간 | RAG + OCR 연동 (후순위) |
| Phase 2 | 1~2시간 | 인프라 설정 확인 작업 |
| Phase 3 | 2~3시간 | 버그 픽스 코드 수정 |
| Phase 4 | 1~2시간 | gunicorn 전환 중심 |
| Phase 5 | 2~3시간 | Mac mini에서 테스트 실행 |
| Phase 6 | 필요시 | 운영하면서 점진적 |
| **합계** | **~18시간** | 4~5일 분량 |

460
docs/gpu-restructure.md Normal file
View File

@@ -0,0 +1,460 @@
# GPU 서버 재구성 + PKM 프로젝트 연계 계획
## Context
GPU 서버(RTX 4070Ti Super)에서 Mac Mini와 중복되는 LLM 모델(qwen3.5:9b, id-9b)을 제거하고,
대신 Surya OCR + bge-m3 임베딩 서비스를 배치하여 역할을 명확히 분리한다.
추가로 Komga(만화 서버)를 Mac Mini에서 GPU 서버로 이전하여 Surya OCR과 로컬 연동 가능하게 한다.
기존 PKM 프로젝트(`~/Documents/code/DEVONThink_my server/`)와 연계하여:
- ChromaDB → Qdrant 마이그레이션
- nomic-embed-text → bge-m3 통일
- Qwen2.5-VL-7B 비전 OCR → Surya OCR 전용 대체
- architecture.md, embed 스크립트, AppleScript 등 관련 코드/문서 일괄 갱신
PKM 프로젝트는 현재 Phase 1 완료(90%), Phase 2(인프라 수정+버그 픽스) 착수 대기 상태.
이번 GPU 서버 재구성은 Phase 2와 병행하여 인프라 변경을 반영한다.
---
## 현재 상태 요약
### GPU 서버 (192.168.1.186)
- Ryzen 7 7800X3D / 30GB RAM / RTX 4070Ti Super 16GB VRAM
- **서비스**: Ollama(11434) + no-think proxy(11435), Plex(32400)
- **Ollama 모델**: qwen3.5:9b-q8_0(10GB), id-9b(10GB) ← **제거 확정**
- CUDA 드라이버 580.x 설치됨, nvcc(CUDA toolkit) 미설치
- Docker 미설치, Python 3.12
- NAS SMB/NFS 마운트 미설정
### Mac Mini (192.168.1.122)
- M4 Pro / 64GB / MLX Qwen3.5-35B(8800), Ollama bge-m3(11434)
- **Docker**: Qdrant(6333), Komga(25600), NanoClaw(9801), tk-ai-service(30400) 등
- **PKM 프로젝트**: `~/Documents/code/DEVONThink_my server/`
- `embed_to_chroma.py` → GPU 서버 nomic-embed-text + ChromaDB ← **Qdrant + bge-m3로 변경**
- `auto_classify.scpt` → MLX localhost:8800으로 분류, Step 4에서 embed_to_chroma.py 호출
- `pkm_api_server.py` → Flask 9900번 포트 (stats 500 에러, 한글 인코딩 버그 있음)
- `architecture.md` → GPU Tier 3에 nomic-embed + VL-7B + reranker 계획 ← **갱신 필요**
- **DEVONthink 4**: 13개 DB, Smart Rule 3개 설계 완료
### 영향받는 외부 서비스
- `tk-ai-service` docker-compose.yml: `OLLAMA_TEXT_MODEL=qwen3.5:9b-q8_0`**변경 필요**
- `paperless-gpt`: `qwen3:8b` 참조 → docker-compose 존재하나 **현재 미실행** (docker ps에 없음), Phase 1에서 처리 방침 결정 필요
### NAS IP 참고
- NAS LAN IP: `192.168.1.227` (nginx-ssl.conf upstream에서 확인됨)
- NAS Tailscale IP: `100.101.79.37`
- NFS 마운트는 **LAN 직결 (192.168.1.227)** 사용 (성능상 최적)
---
## 변경 계획
### Phase 1: GPU 서버 정리 (선행)
**1-1. Ollama 모델 제거**
```bash
ssh 192.168.1.186
ollama rm qwen3.5:9b-q8_0
ollama rm id-9b
```
**1-2. 새 모델 설치**
```bash
ollama pull bge-m3 # 임베딩 (1024차원, 한국어 우수)
ollama pull bge-reranker-v2-m3 # RAG 리랭킹
```
- 임베딩 모델: `nomic-embed-text`(768차원) 대신 `bge-m3`(1024차원)으로 통일
- 이유: Mac Mini Ollama에서 이미 bge-m3 사용 중, 한국어 성능 우수, Qdrant tk_qc_issues 컬렉션도 1024차원
**1-3. Ollama no-think proxy(11435) 비활성화**
- LLM 모델 제거 후 think:false 주입이 불필요
- `sudo systemctl disable --now ollama-proxy`
**1-4. Ollama systemd 환경 조정**
```ini
# /etc/systemd/system/ollama.service [Service] 섹션에 추가
Environment="OLLAMA_MAX_LOADED_MODELS=2"
Environment="OLLAMA_KEEP_ALIVE=30m"
```
**1-5. paperless-gpt 처리**
- 현재 미실행 상태 (docker ps에 없음)
- docker-compose.yml에 `qwen3:8b` 참조 → GPU 서버 모델 제거 시 사용 불가
- 옵션: (a) MLX 35B로 전환 (b) 당분간 비활성 유지 (c) 폐기
- Paperless-ngx 자체가 활발히 사용 중인지 확인 후 결정
**1-6. tk-ai-service 코드 + 설정 변경** (Mac Mini 유지, 코드 수정 필수)
tk-ai-service는 Ollama 네이티브 API(`/api/chat`, `/api/embeddings`)를 사용 중.
MLX 서버는 OpenAI API(`/v1/chat/completions`)만 지원하므로 코드 수정 필요.
**a) `ollama_client.py` → `llm_client.py` 리팩터링**
- `generate_text()`: `/api/chat``/v1/chat/completions` (OpenAI 형식)
- 요청: `{"model":"...","messages":[...],"stream":false}`
- 응답: `response.json()["choices"][0]["message"]["content"]`
- `generate_embedding()`: 변경 없음 (GPU Ollama `/api/embeddings` 그대로)
- `check_health()`: text URL `/api/tags``/v1/models` 또는 단순 GET 체크
- 클래스명/파일명 변경 고려 (OllamaClient → LLMClient)
**b) docker-compose.yml 환경변수 변경**
```yaml
# ~/docker/tk-ai-service/docker-compose.yml
- OLLAMA_BASE_URL=http://host.internal:8800 # Mac Mini MLX 서버 (OpenAI API)
- OLLAMA_TEXT_MODEL=mlx-community/Qwen3.5-35B-A3B-4bit
- OLLAMA_EMBED_URL=http://192.168.1.186:11434 # GPU 서버 Ollama (임베딩)
- OLLAMA_EMBED_MODEL=bge-m3 # 변경 없음
```
**c) config.py 주석 갱신**
```python
# Mac Mini MLX (텍스트 생성) — OpenAI 호환 API
OLLAMA_BASE_URL: str = "http://host.internal:8800"
# GPU 서버 Ollama (임베딩)
OLLAMA_EMBED_URL: str = "http://192.168.1.186:11434"
```
### Phase 1.5: GPU 서버 Docker + NFS + Komga 이전
**1.5-1. Docker 설치** (GPU 서버)
```bash
# Docker Engine (Ubuntu)
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo usermod -aG docker hyungi
# nvidia-container-toolkit (Surya OCR Docker化 시 필요, 당장은 불필요)
```
**1.5-2. NAS NFS 마운트 설정**
Synology NAS 측:
- DSM → 제어판 → 파일 서비스 → NFS 활성화 (v4.1)
- 공유 폴더(Comic) → NFS 권한: `192.168.1.186` 단일 IP, 읽기 전용, root_squash
GPU 서버 측:
```bash
sudo apt install nfs-common
sudo mkdir -p /mnt/comic
# /etc/fstab 추가:
192.168.1.227:/volume1/Comic /mnt/comic nfs4 ro,nosuid,noexec,nodev,soft,timeo=15 0 0
sudo mount -a
```
**1.5-3. Komga Docker 이전**
GPU 서버에 docker-compose.yml 생성:
```yaml
# /opt/komga/docker-compose.yml
services:
komga:
image: gotson/komga
container_name: komga
ports:
- "25600:25600"
volumes:
- /mnt/comic:/data/comics:ro # NFS 마운트 (읽기 전용)
- ./config:/config # Komga 설정/DB
environment:
- TZ=Asia/Seoul
restart: unless-stopped
```
**1.5-4. Mac Mini 측 변경**
- Mac Mini Komga 컨테이너 중지: `docker stop komga && docker rm komga`
- nginx upstream 변경:
```
# nginx-ssl.conf
upstream komga_backend {
server 192.168.1.186:25600; # GPU 서버로 변경
}
```
- nginx 재시작: `docker restart home-service-proxy`
- Mac Mini Docker VM 메모리 **1.23GB 회수**
**1.5-5. Komga 설정 마이그레이션**
- Mac Mini의 Komga config/DB를 GPU 서버로 복사 (라이브러리 메타데이터, 사용자 설정 유지)
- 경로: Mac Mini `~/docker/Komga/` → GPU `scp`로 전송
- **주의**: Komga 내부 DB(H2)에 라이브러리 절대경로가 저장되어 있음
- Mac Mini: `/data/comics` (Docker 내부 마운트 경로)
- GPU 서버: `/data/comics` (동일하게 Docker 마운트하면 경로 변경 불필요)
- Docker 내부 경로를 동일하게 맞추면 DB 마이그레이션 문제 없음
- 만약 경로가 달라지면 Komga UI에서 라이브러리 경로 재설정 또는 전체 재스캔 필요
### Phase 2: Surya OCR 설치
**2-1. PyTorch CUDA 런타임 확인** (GPU 서버)
- Surya OCR은 PyTorch에 의존 → PyTorch 설치 시 CUDA 런타임이 번들됨
- 별도 `nvidia-cuda-toolkit` 설치가 **불필요할 수 있음** (nvcc는 직접 CUDA 코드 컴파일 시만 필요)
- GPU 서버에서 확인:
```bash
# PyTorch CUDA 지원 확인
python3 -c "import torch; print(torch.cuda.is_available())"
# 안 되면 CUDA 번들 포함 PyTorch 설치
pip install torch --index-url https://download.pytorch.org/whl/cu124
```
**2-2. Surya OCR 서비스 구성**
```
/opt/surya-ocr/
venv/ # Python venv (surya-ocr, fastapi, uvicorn, python-multipart)
server.py # FastAPI 래퍼
```
서버 엔드포인트:
- `POST /ocr` — 파일 업로드 → OCR 텍스트 + 바운딩박스 반환
- `POST /ocr/layout` — 레이아웃 분석 포함
- `GET /health` — 상태 확인
**2-3. systemd 서비스 등록**
```ini
# /etc/systemd/system/surya-ocr.service
[Unit]
Description=Surya OCR Service
After=network-online.target
[Service]
ExecStart=/opt/surya-ocr/venv/bin/uvicorn server:app --host 0.0.0.0 --port 8400
WorkingDirectory=/opt/surya-ocr
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
```
**VRAM 예산 (변경 후)**
| 컴포넌트 | VRAM | 비고 |
|----------|------|------|
| Plex HW 트랜스코드 | ~1-2GB | 활성 시 |
| Surya OCR | ~2-3GB | 활성 시 |
| bge-m3 임베딩 | ~1.5GB | 상시 |
| bge-reranker | ~1GB | 온디맨드 |
| **합계 (피크)** | **~7-8GB / 16GB** | 여유 충분 |
### Phase 3: 벡터 DB 통합 (ChromaDB → Qdrant) + PKM 코드 갱신
**결정: Qdrant로 통일, ChromaDB 폐기**
- Qdrant가 이미 Mac Mini Docker에서 운영 중 (tk_qc_issues 컬렉션)
- ChromaDB는 embed_to_chroma.py에서만 계획/사용
- 2개 벡터 DB 운영은 불필요한 복잡성
**3-1. Qdrant에 pkm_documents 컬렉션 생성**
```
dimension: 1024 (bge-m3)
distance: cosine
payload 필드: uuid, title, database, tags, source_channel
```
**3-2. `scripts/embed_to_chroma.py` → `scripts/embed_to_qdrant.py` 리라이트**
- `chromadb` → `qdrant-client` 교체
- GPU 서버 임베딩 모델: `nomic-embed-text` → `bge-m3`
- Ollama API: `/api/embed` 사용 (배치 지원, `{"model":"bge-m3","input":["텍스트"]}`)
- 현재 auto_classify.scpt에서 문서 단건 호출이므로 단일/배치 모두 가능
- 향후 embed-batch 엔드포인트에서 배치 활용 가능
- 텍스트 청킹 추가 (500토큰, 50토큰 오버랩)
- `pkm_utils.load_credentials()`에서 GPU_SERVER_IP 로드
- **기존 `embed_to_chroma.py`는 `git rm`으로 삭제** (GPU_SERVER_IP 미설정으로 실행된 적 없음, 실 데이터 없음)
**3-3. `applescript/auto_classify.scpt` Step 4 수정 + 버그 픽스**
- 현재: `do shell script "python3 ~/scripts/embed_to_chroma.py " & docUUID & " &"`
- 변경: `embed_to_qdrant.py` 호출로 교체 + baseDir 변수 사용
- **버그 픽스**: 73행 `add custom meta data "inbox_route" for "sourceChannel"` 삭제
- 70행에서 AI 분류 결과로 설정한 sourceChannel을 "inbox_route"로 덮어쓰는 버그
**3-4. requirements.txt 업데이트** (dev-roadmap 9단계와 합산, 단일 커밋)
```
- chromadb>=0.4.0
- schedule>=1.2.0 # 미사용 확인 후 제거
+ qdrant-client>=1.7.0
+ flask>=3.0.0 # dev-roadmap 9단계
+ gunicorn>=21.2.0 # dev-roadmap 9단계
anthropic>=0.40.0 # 유지 (향후 Tier 2용)
```
**3-5. credentials.env + credentials.env.example 업데이트**
```
# credentials.env (Mac mini)
GPU_SERVER_IP=192.168.1.186
# credentials.env.example (git 추적)
GPU_SERVER_IP=192.168.1.xxx
```
**3-6. `docs/architecture.md` 대규모 갱신** (별도 커밋)
- **변경 규모**: ChromaDB 28건, nomic-embed 12건, VL-7B 5건 — 문맥별 수정 필요 (단순 치환 불가)
- 주요 변경 영역:
- AI 통합 아키텍처 다이어그램 (Tier 3 모델 목록: nomic→bge-m3, VL-7B→Surya OCR)
- VRAM 배분 다이어그램 (~11.3GB → ~7-8GB)
- 자동화 파이프라인 / Smart Rule 설계 (ChromaDB→Qdrant, embed 스크립트 경로)
- AI 결과물 저장 전략 표 (ChromaDB→Qdrant)
- 임베딩 이전 근거 테이블 (nomic→bge-m3 반영)
- 3-Tier AI 라우팅 전략 표
- 코드 예시 내 경로/모델명
- 이 작업은 **별도 시간을 잡아서** 전체 문서를 통독하며 진행
### Phase 4: DEVONthink OCR 연동
**4-1. `scripts/ocr_preprocess.py` 신규 작성**
- DEVONthink UUID → AppleScript로 파일 경로 추출 → Surya API(GPU:8400) 호출 → OCR 텍스트 반환
- `pkm_utils.run_applescript_inline()` 재사용
- 반환값: OCR 텍스트 (plain text) — DEVONthink 본문에 병합용
**4-2. `applescript/auto_classify.scpt` Smart Rule 수정**
- architecture.md의 Rule 1 설계에 따라 Step 0(OCR) 추가:
```
현재: Step 1(MLX 분류) → Step 2(태그 파싱) → Step 3(DB 이동) → Step 4(임베딩) → Step 5(메타)
변경: Step 0(OCR 감지+처리) → Step 1(MLX 분류) → ... → Step 4(Qdrant 임베딩)
```
- OCR 대상 감지 조건 (단순 텍스트 길이 < 50 대신 정교한 판별):
- `type of theRecord` = PDF **AND** `plain text of theRecord` = "" (텍스트 레이어 없음)
- 또는 `type of theRecord` ∈ {JPEG, PNG, TIFF} (이미지 파일)
- DEVONthink 자체가 PDF 텍스트 레이어를 읽으므로, OCR이 필요한 건 **텍스트가 완전히 비어있는 경우**뿐
- Surya OCR 호출 → `set plain text of theRecord to ocrText` 로 본문 병합
- 기존 Qwen2.5-VL-7B 비전 OCR 계획 → Surya 전용 OCR로 대체 (정확도 + ABBYY 대체)
**4-3. `docs/architecture.md` Rule 1 갱신**
- "이미지/스캔 문서 → GPU 서버 VL-7B로 OCR" → "Surya OCR(:8400)으로 OCR"
### Phase 5: RAG 파이프라인 (PKM API 확장)
**5-1. pkm_api_server.py에 RAG 엔드포인트 추가**
- 현재 docstring(7행): "범위: DEVONthink + OmniFocus 전용. 이 이상 확장하지 않을 것."
- RAG는 DEVONthink 문서 검색 기반이므로 동일 범위의 확장으로 간주
- docstring을 "범위: DEVONthink + OmniFocus + RAG 검색" 으로 갱신
```
POST /rag/query # 질문 → 임베딩 → Qdrant 검색 → 리랭킹 → LLM 답변
POST /devonthink/embed # 단일 문서 임베딩 트리거
POST /devonthink/embed-batch # 배치 임베딩
```
**5-2. RAG 쿼리 플로우**
```
질문 텍스트
→ GPU 서버 bge-m3로 쿼리 임베딩 (192.168.1.186:11434)
→ Mac Mini Qdrant에서 유사도 검색 (localhost:6333, top-20)
→ GPU 서버 bge-reranker로 리랭킹 (top-5)
→ Mac Mini MLX Qwen3.5-35B로 답변 생성 (localhost:8800)
→ DEVONthink 링크(x-devonthink-item://UUID) 포함 응답
```
### Phase 6: NanoClaw + Komga 연동 (후순위, 별도 계획)
Phase 5 완료 후 별도 문서로 상세 계획 수립. 현재는 방향만 기록:
- **NanoClaw RAG**: PKM API `/rag/query` 엔드포인트 호출 → 시놀로지 Chat에서 "@이드 [질문]" → 문서 기반 답변
- **Komga OCR**: Komga REST API → Surya OCR → Qdrant `komga_manga` 컬렉션
- dev-roadmap.md에는 "향후 계획" 수준으로만 언급
---
## 변경 후 아키텍처
```
┌─ Mac Mini M4 Pro ─────────────────────┐ ┌─ GPU 서버 (4070Ti) ─────────┐
│ │ │ │
│ MLX Qwen3.5-35B (:8800) — LLM 추론 │ │ Ollama (:11434) │
│ MLX Proxy (:8801) — Synology 연동 │◄───►│ ├─ bge-m3 (임베딩) │
│ Ollama (:11434) — bge-m3 로컬 폴백 │ │ └─ bge-reranker (리랭킹) │
│ Qdrant (:6333) — 벡터 검색 │ │ │
│ PKM API (:9900) — RAG 오케스트레이션 │ │ Surya OCR (:8400) │
│ NanoClaw (:9801) — AI 어시스턴트 │ │ Plex (:32400) — HW 트랜스코드│
│ DEVONthink — 문서 허브 │ │ Komga (:25600) — 만화 서버 │
│ nginx proxy (:443) — 리버스 프록시 │ │ └─ NFS → NAS /Comic (ro) │
│ │ │ │
└────────────────────────────────────────┘ │ [제거됨] │
▲ │ ├─ qwen3.5:9b-q8_0 │
│ ┌─ NAS ──────┐ │ ├─ id-9b │
└──────────────│ 문서/미디어 │────────│ └─ no-think proxy (:11435)│
└────────────┘ └──────────────────────────────┘
```
## 수정 대상 파일 목록
### GPU 서버
| 파일 | 변경 |
|------|------|
| Ollama 모델 | `rm qwen3.5:9b-q8_0`, `rm id-9b` / `pull bge-m3`, `pull bge-reranker-v2-m3` |
| `/etc/systemd/system/ollama.service` | 환경변수 추가 (MAX_LOADED_MODELS, KEEP_ALIVE) |
| `/etc/systemd/system/ollama-proxy.service` | **비활성화** (disable --now) |
| `/opt/surya-ocr/server.py` | **신규** — FastAPI OCR 서버 |
| `/etc/systemd/system/surya-ocr.service` | **신규** — systemd 유닛 |
| Docker Engine | **신규 설치** |
| `/etc/fstab` | NFS 마운트 추가 (NAS Comic → /mnt/comic, ro) |
| `/opt/komga/docker-compose.yml` | **신규** — Komga 컨테이너 |
### PKM 프로젝트 (`~/Documents/code/DEVONThink_my server/`)
| 파일 | 변경 |
|------|------|
| `scripts/embed_to_chroma.py` | → `scripts/embed_to_qdrant.py` 리라이트 (chromadb→qdrant-client, nomic→bge-m3) |
| `scripts/ocr_preprocess.py` | **신규** — Surya OCR 호출 헬퍼 |
| `scripts/pkm_api_server.py` | RAG 엔드포인트 추가 (/rag/query, /devonthink/embed) |
| `scripts/pkm_utils.py` | 변경 없음 (`load_credentials()`에 이미 GPU_SERVER_IP 지원, 74행) |
| `applescript/auto_classify.scpt` | Step 0(OCR 감지) 추가 + Step 4 embed_to_qdrant.py로 변경 |
| `requirements.txt` | `chromadb` → `qdrant-client`, `flask` 추가 (dev-roadmap 9단계 합산) |
| `docs/architecture.md` | Tier 3 모델, VRAM 다이어그램, ChromaDB→Qdrant, Smart Rule 전체 갱신 |
| `docs/dev-roadmap.md` | GPU 서버 재구성 Phase 반영 |
| `docs/claude-code-commands.md` | GPU 서버 관련 단계 추가 |
| `credentials.env` (Mac mini) | `GPU_SERVER_IP=192.168.1.186` 추가 |
| `credentials.env.example` | GPU_SERVER_IP 항목 추가 |
### Mac Mini 기타
| 파일 | 변경 |
|------|------|
| `~/docker/tk-ai-service/docker-compose.yml` | BASE_URL → MLX :8800, EMBED_URL → GPU :11434 |
| `~/docker/tk-factory-services/ai-service/services/ollama_client.py` | Ollama API → OpenAI API 전환 (generate_text, check_health) |
| `~/docker/tk-factory-services/ai-service/config.py` | 주석 갱신 |
| `~/docker/home-service-proxy/nginx-ssl.conf` | `komga_backend` upstream → `192.168.1.186:25600` |
| Mac Mini Komga 컨테이너 | **중지 및 제거** (1.23GB Docker VM 메모리 회수) |
## 검증 방법
1. **Phase 1.5 검증**:
- `ssh GPU "docker ps"` → komga 컨테이너 실행 중
- `ssh GPU "ls /mnt/comic"` → NAS 만화 파일 목록 확인
- `curl http://192.168.1.186:25600` → Komga 웹 UI 접근
- `curl https://komga.hyungi.net` → nginx 프록시 경유 접근 확인
- Mac Mini: `docker ps | grep komga` → 없음 (제거 완료)
2. **Phase 1 검증**:
- `ssh GPU "ollama list"` → bge-m3, bge-reranker만 존재
- `ssh GPU "systemctl status ollama-proxy"` → inactive
- `ssh GPU "curl localhost:11434/api/embed -d '{\"model\":\"bge-m3\",\"input\":[\"test\"]}'` → 1024차원 벡터 반환
- Qdrant 차원 확인: `curl localhost:6333/collections/tk_qc_issues` → vector size=1024 확인 (bge-m3와 일치)
- tk-ai-service 코드 수정 후: `docker compose build && docker compose up -d`
- `curl http://localhost:30400/health` → ollama_text(MLX), ollama_embed(GPU) 모두 connected
- `curl -X POST http://localhost:30400/api/chat -d '{"message":"테스트"}'` → MLX 35B 응답 확인
2. **Phase 2 검증**: `curl -F "file=@test.pdf" http://192.168.1.186:8400/ocr` → OCR 텍스트 반환
3. **Phase 3 검증**:
- `curl http://localhost:6333/collections/pkm_documents` → 컬렉션 존재
- `python3 scripts/embed_to_qdrant.py <테스트UUID>` → Qdrant에 벡터 저장 확인
- `git diff` → embed_to_chroma.py 삭제, embed_to_qdrant.py 생성 확인
4. **Phase 4 검증**: DEVONthink Inbox에 스캔 PDF(텍스트 없는) 추가 → Smart Rule → OCR 텍스트 병합 → 분류 완료
5. **Phase 5 검증**: `curl -X POST localhost:9900/rag/query -d '{"q":"산업안전 법령"}'` → 관련 문서 + 답변 반환
6. **Phase 6**: 후순위, 별도 계획 시 검증 방법 수립
## 실행 순서
```
Phase 1 (GPU 정리 + 모델 교체) ──┐
Phase 1.5(Docker + NFS + Komga) ──┼── GPU 서버 작업 (SSH)
Phase 2 (Surya OCR 설치) ────────┘
Phase 3 (Qdrant 통합 + PKM 코드) ──┐── PKM 프로젝트 코드 수정
Phase 4 (DEVONthink OCR 연동) ─────┘ → git commit & push
→ Mac mini에서 git pull
Phase 5 (RAG 파이프라인) ──────── PKM API 확장
Phase 6 (NanoClaw/Komga OCR) ─── 후순위
```
Phase 1~2는 GPU 서버 SSH 작업, Phase 3~4는 PKM 프로젝트 코드 수정.
Phase 1 완료 시점에 tk-ai-service docker-compose도 함께 변경.
각 Phase는 독립적으로 검증 가능.
**PKM dev-roadmap과의 관계:**
- 이 계획의 Phase 3~4 = dev-roadmap Phase 2의 일부 (인프라 수정)
- requirements.txt, AppleScript 경로, credentials.env 변경이 겹침 → 합쳐서 진행
**문서 통합 전략:**
- `docs/dev-roadmap.md`: GPU 서버 재구성 Phase를 기존 Phase 2 앞에 "Phase 1.5: GPU 서버 재구성" 으로 삽입, Phase 2 항목에서 겹치는 변경(requirements.txt, credentials.env) 연결
- `docs/claude-code-commands.md`: Phase 2 섹션에 GPU 서버 관련 실행 단계 추가 (SSH 명령, Surya 설치, Qdrant 컬렉션 생성 등)
- 이 계획서는 `docs/gpu-restructure.md`로 정식 문서화 (아키텍처 결정 근거 기록으로 보존)

View File

@@ -1,6 +1,7 @@
chromadb>=0.4.0
qdrant-client>=1.7.0
requests>=2.31.0
python-dotenv>=1.0.0
schedule>=1.2.0
markdown>=3.5.0
anthropic>=0.40.0
flask>=3.0.0
gunicorn>=21.2.0

114
scripts/embed_to_qdrant.py Normal file
View File

@@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""
벡터 임베딩 스크립트
- DEVONthink 문서 UUID로 텍스트 추출
- GPU 서버(bge-m3)로 임베딩 생성
- Qdrant에 저장
"""
import sys
import uuid as uuid_mod
import requests
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from pkm_utils import setup_logger, load_credentials, run_applescript_inline
logger = setup_logger("embed")
QDRANT_URL = "http://localhost:6333"
COLLECTION = "pkm_documents"
def get_document_text(doc_uuid: str) -> tuple[str, str]:
"""DEVONthink에서 UUID로 문서 텍스트 + 제목 추출"""
script = f'''
tell application id "DNtp"
set theRecord to get record with uuid "{doc_uuid}"
set docText to plain text of theRecord
set docTitle to name of theRecord
return docTitle & "|||" & docText
end tell
'''
result = run_applescript_inline(script)
parts = result.split("|||", 1)
title = parts[0] if len(parts) > 0 else ""
text = parts[1] if len(parts) > 1 else ""
return title, text
def get_embedding(text: str, gpu_server_ip: str) -> list[float] | None:
"""GPU 서버의 bge-m3로 임베딩 생성"""
url = f"http://{gpu_server_ip}:11434/api/embed"
try:
resp = requests.post(url, json={
"model": "bge-m3",
"input": [text[:8000]]
}, timeout=60)
resp.raise_for_status()
embeddings = resp.json().get("embeddings")
return embeddings[0] if embeddings else None
except Exception as e:
logger.error(f"임베딩 생성 실패: {e}")
return None
def store_in_qdrant(doc_uuid: str, title: str, text: str, embedding: list[float]):
"""Qdrant에 저장"""
# UUID 문자열을 정수 ID로 변환 (Qdrant point ID)
point_id = uuid_mod.uuid5(uuid_mod.NAMESPACE_URL, doc_uuid).int >> 64
payload = {
"uuid": doc_uuid,
"title": title,
"text_preview": text[:500],
"source": "devonthink",
}
resp = requests.put(
f"{QDRANT_URL}/collections/{COLLECTION}/points",
json={
"points": [{
"id": point_id,
"vector": embedding,
"payload": payload,
}]
},
timeout=30,
)
resp.raise_for_status()
logger.info(f"Qdrant 저장: {doc_uuid} ({title[:30]})")
def run(doc_uuid: str):
"""단일 문서 임베딩 처리"""
logger.info(f"임베딩 처리 시작: {doc_uuid}")
creds = load_credentials()
gpu_ip = creds.get("GPU_SERVER_IP")
if not gpu_ip:
logger.warning("GPU_SERVER_IP 미설정 — 임베딩 건너뜀")
return
try:
title, text = get_document_text(doc_uuid)
if not text or len(text) < 10:
logger.warning(f"텍스트 부족 [{doc_uuid}]: {len(text)}")
return
embedding = get_embedding(text, gpu_ip)
if embedding:
store_in_qdrant(doc_uuid, title, text, embedding)
logger.info(f"임베딩 완료: {doc_uuid}")
else:
logger.error(f"임베딩 실패: {doc_uuid}")
except Exception as e:
logger.error(f"임베딩 처리 에러 [{doc_uuid}]: {e}", exc_info=True)
if __name__ == "__main__":
if len(sys.argv) < 2:
print("사용법: python3 embed_to_qdrant.py <DEVONthink_UUID>")
sys.exit(1)
run(sys.argv[1])

239
scripts/pkm_api_server.py Normal file
View File

@@ -0,0 +1,239 @@
#!/usr/bin/env python3
"""
PKM Host API Server
DEVONthink + OmniFocus AppleScript 중계용 경량 HTTP 서버.
NanoClaw 컨테이너에서 호출. LaunchAgent(GUI 세션)로 실행 필수.
범위: DEVONthink + OmniFocus 전용. 이 이상 확장하지 않을 것.
"""
import json
import subprocess
import sys
from flask import Flask, request, jsonify
app = Flask(__name__)
def run_applescript(script: str, timeout: int = 120) -> str:
result = subprocess.run(
['osascript', '-e', script],
capture_output=True, text=True, timeout=timeout
)
if result.returncode != 0:
raise RuntimeError(result.stderr.strip())
return result.stdout.strip()
# --- DEVONthink ---
@app.route('/devonthink/stats')
def devonthink_stats():
try:
script = (
'tell application id "DNtp"\n'
' set today to current date\n'
' set time of today to 0\n'
' set stats to {}\n'
' repeat with db in databases\n'
' set dbName to name of db\n'
' set addedCount to 0\n'
' set modifiedCount to 0\n'
' repeat with rec in children of root of db\n'
' try\n'
' if creation date of rec >= today then set addedCount to addedCount + 1\n'
' if modification date of rec >= today then set modifiedCount to modifiedCount + 1\n'
' end try\n'
' end repeat\n'
' if addedCount > 0 or modifiedCount > 0 then\n'
' set end of stats to dbName & ":" & addedCount & ":" & modifiedCount\n'
' end if\n'
' end repeat\n'
' set AppleScript\'s text item delimiters to "|"\n'
' return stats as text\n'
'end tell'
)
result = run_applescript(script)
stats = {}
if result:
for item in result.split('|'):
parts = item.split(':')
if len(parts) == 3:
stats[parts[0]] = {'added': int(parts[1]), 'modified': int(parts[2])}
total_added = sum(s['added'] for s in stats.values())
total_modified = sum(s['modified'] for s in stats.values())
return jsonify(success=True, data={
'databases': stats,
'total_added': total_added,
'total_modified': total_modified
})
except Exception as e:
return jsonify(success=False, error=str(e)), 500
@app.route('/devonthink/search')
def devonthink_search():
q = request.args.get('q', '')
limit = int(request.args.get('limit', '10'))
if not q:
return jsonify(success=False, error='q parameter required'), 400
try:
script = (
'tell application id "DNtp"\n'
f' set results to search "{q}"\n'
' set output to {}\n'
f' set maxCount to {limit}\n'
' set i to 0\n'
' repeat with rec in results\n'
' if i >= maxCount then exit repeat\n'
' set recName to name of rec\n'
' set recDB to name of database of rec\n'
' set recDate to modification date of rec as text\n'
' set end of output to recName & "||" & recDB & "||" & recDate\n'
' set i to i + 1\n'
' end repeat\n'
' set AppleScript\'s text item delimiters to linefeed\n'
' return output as text\n'
'end tell'
)
result = run_applescript(script)
items = []
if result:
for line in result.split('\n'):
parts = line.split('||')
if len(parts) == 3:
items.append({'name': parts[0], 'database': parts[1], 'modified': parts[2]})
return jsonify(success=True, data=items, count=len(items))
except Exception as e:
return jsonify(success=False, error=str(e)), 500
@app.route('/devonthink/inbox-count')
def devonthink_inbox_count():
try:
script = (
'tell application id "DNtp"\n'
' set inboxDB to database "Inbox"\n'
' return count of children of root of inboxDB\n'
'end tell'
)
count = int(run_applescript(script))
return jsonify(success=True, data={'inbox_count': count})
except Exception as e:
return jsonify(success=False, error=str(e)), 500
# --- OmniFocus ---
@app.route('/omnifocus/stats')
def omnifocus_stats():
try:
script = (
'tell application "OmniFocus"\n'
' tell default document\n'
' set today to current date\n'
' set time of today to 0\n'
' set completedCount to count of (every flattened task whose completed is true and completion date >= today)\n'
' set addedCount to count of (every flattened task whose creation date >= today)\n'
' set overdueCount to count of (every flattened task whose completed is false and due date < today and due date is not missing value)\n'
' return (completedCount as text) & "|" & (addedCount as text) & "|" & (overdueCount as text)\n'
' end tell\n'
'end tell'
)
result = run_applescript(script)
parts = result.split('|')
return jsonify(success=True, data={
'completed': int(parts[0]) if len(parts) > 0 else 0,
'added': int(parts[1]) if len(parts) > 1 else 0,
'overdue': int(parts[2]) if len(parts) > 2 else 0
})
except Exception as e:
return jsonify(success=False, error=str(e)), 500
@app.route('/omnifocus/overdue')
def omnifocus_overdue():
try:
script = (
'tell application "OmniFocus"\n'
' tell default document\n'
' set today to current date\n'
' set time of today to 0\n'
' set overdueTasks to every flattened task whose completed is false and due date < today and due date is not missing value\n'
' set output to {}\n'
' repeat with t in overdueTasks\n'
' set taskName to name of t\n'
' set dueDate to due date of t as text\n'
' set projName to ""\n'
' try\n'
' set projName to name of containing project of t\n'
' end try\n'
' set end of output to taskName & "||" & projName & "||" & dueDate\n'
' end repeat\n'
' set AppleScript\'s text item delimiters to linefeed\n'
' return output as text\n'
' end tell\n'
'end tell'
)
result = run_applescript(script)
tasks = []
if result:
for line in result.split('\n'):
parts = line.split('||')
tasks.append({
'name': parts[0],
'project': parts[1] if len(parts) > 1 else '',
'due_date': parts[2] if len(parts) > 2 else ''
})
return jsonify(success=True, data=tasks, count=len(tasks))
except Exception as e:
return jsonify(success=False, error=str(e)), 500
@app.route('/omnifocus/today')
def omnifocus_today():
try:
script = (
'tell application "OmniFocus"\n'
' tell default document\n'
' set today to current date\n'
' set time of today to 0\n'
' set tomorrow to today + 1 * days\n'
' set todayTasks to every flattened task whose completed is false and ((due date >= today and due date < tomorrow) or (defer date >= today and defer date < tomorrow))\n'
' set output to {}\n'
' repeat with t in todayTasks\n'
' set taskName to name of t\n'
' set projName to ""\n'
' try\n'
' set projName to name of containing project of t\n'
' end try\n'
' set end of output to taskName & "||" & projName\n'
' end repeat\n'
' set AppleScript\'s text item delimiters to linefeed\n'
' return output as text\n'
' end tell\n'
'end tell'
)
result = run_applescript(script)
tasks = []
if result:
for line in result.split('\n'):
parts = line.split('||')
tasks.append({'name': parts[0], 'project': parts[1] if len(parts) > 1 else ''})
return jsonify(success=True, data=tasks, count=len(tasks))
except Exception as e:
return jsonify(success=False, error=str(e)), 500
@app.route('/health')
def health():
return jsonify(success=True, service='pkm-api', endpoints=[
'/devonthink/stats', '/devonthink/search?q=',
'/devonthink/inbox-count', '/omnifocus/stats', '/omnifocus/overdue', '/omnifocus/today'
])
if __name__ == '__main__':
port = int(sys.argv[1]) if len(sys.argv) > 1 else 9900
print(f'PKM API Server starting on port {port}')
app.run(host='127.0.0.1', port=port)