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:
165
CLAUDE.md
165
CLAUDE.md
@@ -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
114
README.md
@@ -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` 참조
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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: 프로젝트 구조 + 설계 문서 초기 커밋
|
||||
```
|
||||
|
||||
199
docs/deploy.md
199
docs/deploy.md
@@ -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
399
docs/dev-roadmap.md
Normal 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
460
docs/gpu-restructure.md
Normal 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`로 정식 문서화 (아키텍처 결정 근거 기록으로 보존)
|
||||
@@ -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
114
scripts/embed_to_qdrant.py
Normal 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
239
scripts/pkm_api_server.py
Normal 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)
|
||||
Reference in New Issue
Block a user