From e48b6a2bb4b70d206ceb64ae53dd48a3a6375ca0 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Thu, 2 Apr 2026 09:35:09 +0900 Subject: [PATCH] chore: remove v1 files from main branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v1 코드는 v1-archive 브랜치 + v1-final 태그로 보존. 필요시 git show v1-final:<파일경로>로 참조 가능. 삭제: applescript/, launchd/, v1 scripts, v1 docs, requirements.txt Co-Authored-By: Claude Opus 4.6 (1M context) --- applescript/auto_classify.scpt | 102 -- applescript/omnifocus_sync.scpt | 71 -- docs/architecture.md | 1368 --------------------- docs/devonagent-setup.md | 103 -- docs/industrial-safety-blueprint.md | 684 ----------- launchd/net.hyungi.pkm.daily-digest.plist | 28 - launchd/net.hyungi.pkm.law-monitor.plist | 28 - launchd/net.hyungi.pkm.mailplus.plist | 36 - requirements.txt | 6 - scripts/embed_to_chroma.py | 104 -- scripts/law_monitor.py | 400 ------ scripts/mailplus_archive.py | 209 ---- scripts/pkm_daily_digest.py | 284 ----- scripts/pkm_utils.py | 161 --- tests/test_classify.py | 129 -- 15 files changed, 3713 deletions(-) delete mode 100644 applescript/auto_classify.scpt delete mode 100644 applescript/omnifocus_sync.scpt delete mode 100644 docs/architecture.md delete mode 100644 docs/devonagent-setup.md delete mode 100644 docs/industrial-safety-blueprint.md delete mode 100644 launchd/net.hyungi.pkm.daily-digest.plist delete mode 100644 launchd/net.hyungi.pkm.law-monitor.plist delete mode 100644 launchd/net.hyungi.pkm.mailplus.plist delete mode 100644 requirements.txt delete mode 100644 scripts/embed_to_chroma.py delete mode 100644 scripts/law_monitor.py delete mode 100644 scripts/mailplus_archive.py delete mode 100644 scripts/pkm_daily_digest.py delete mode 100644 scripts/pkm_utils.py delete mode 100644 tests/test_classify.py diff --git a/applescript/auto_classify.scpt b/applescript/auto_classify.scpt deleted file mode 100644 index 66a6e6e..0000000 --- a/applescript/auto_classify.scpt +++ /dev/null @@ -1,102 +0,0 @@ --- DEVONthink 4 Smart Rule: AI 자동 분류 --- Inbox DB 새 문서 → Ollama 분류 → 태그 + 메타데이터 + 도메인 DB 이동 --- Smart Rule 설정: Event = On Import, 조건 = Tags is empty - -on performSmartRule(theRecords) - tell application id "DNtp" - repeat with theRecord in theRecords - try - -- 1. 문서 텍스트 추출 (최대 4000자) - set docText to plain text of theRecord - set docUUID to uuid of theRecord - - if length of docText > 4000 then - set docText to text 1 thru 4000 of docText - end if - - if length of docText < 10 then - -- 텍스트가 너무 짧으면 건너뜀 - set tags of theRecord to {"@상태/검토필요"} - continue repeat - end if - - -- 2. 분류 프롬프트 로딩 - set promptPath to (POSIX path of (path to home folder)) & "Documents/code/DEVONThink_my server/scripts/prompts/classify_document.txt" - set promptTemplate to do shell script "cat " & quoted form of promptPath - - -- 문서 텍스트를 프롬프트에 삽입 (특수문자 이스케이프) - set escapedText to do shell script "echo " & quoted form of docText & " | sed 's/\\\\/\\\\\\\\/g; s/\"/\\\\\"/g; s/\\n/\\\\n/g' | head -c 4000" - - -- 3. MLX 서버 API 호출 (OpenAI 호환) - set curlCmd to "curl -s --max-time 120 http://localhost:8800/v1/chat/completions -H 'Content-Type: application/json' -d '{\"model\": \"mlx-community/Qwen3.5-35B-A3B-4bit\", \"messages\": [{\"role\": \"user\", \"content\": " & quoted form of escapedText & "}], \"temperature\": 0.3, \"max_tokens\": 1024}'" - set jsonResult to do shell script curlCmd - - -- 4. JSON 파싱 (Python 사용) - set parseCmd to "echo " & quoted form of jsonResult & " | python3 -c \" -import sys, json -try: - r = json.loads(sys.stdin.read()) - content = r['choices'][0]['message']['content'] - d = json.loads(content) - tags = ','.join(d.get('tags', [])) - db = d.get('domain_db', '00_Note_BOX') - grp = d.get('sub_group', '00_Inbox') - ch = d.get('sourceChannel', 'inbox_route') - origin = d.get('dataOrigin', 'external') - print(f'{db}|{grp}|{tags}|{ch}|{origin}') -except: - print('00_Note_BOX|00_Inbox||inbox_route|external') -\"" - - set classResult to do shell script parseCmd - set AppleScript's text item delimiters to "|" - set resultParts to text items of classResult - set targetDB to item 1 of resultParts - set targetGroup to item 2 of resultParts - set tagString to item 3 of resultParts - set sourceChannel to item 4 of resultParts - set dataOrigin to item 5 of resultParts - set AppleScript's text item delimiters to "" - - -- 5. 태그 설정 - if tagString is not "" then - set AppleScript's text item delimiters to "," - set tagList to text items of tagString - set AppleScript's text item delimiters to "" - set tags of theRecord to tagList - end if - - -- 6. 커스텀 메타데이터 설정 - 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 - repeat with db in databases - if name of db is targetDB then - set targetDatabase to db - exit repeat - end if - end repeat - - if targetDatabase is not missing value then - set groupPath to "/" & targetGroup - set targetLocation to create location groupPath in targetDatabase - move record theRecord to targetLocation - end if - - -- 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" - do shell script embedScript & " " & quoted form of embedPy & " " & quoted form of docUUID & " &> /dev/null &" - - on error errMsg - -- 에러 시 로그 기록 + 검토필요 태그 - set tags of theRecord to {"@상태/검토필요", "AI분류실패"} - do shell script "echo '[" & (current date) & "] [auto_classify] [ERROR] " & errMsg & "' >> ~/Documents/code/DEVONThink_my\\ server/logs/auto_classify.log" - end try - end repeat - end tell -end performSmartRule diff --git a/applescript/omnifocus_sync.scpt b/applescript/omnifocus_sync.scpt deleted file mode 100644 index dfa32d6..0000000 --- a/applescript/omnifocus_sync.scpt +++ /dev/null @@ -1,71 +0,0 @@ --- DEVONthink 4 Smart Rule: OmniFocus 연동 --- Projects DB 새 문서에서 TODO 패턴 감지 → OmniFocus 작업 생성 --- Smart Rule 설정: Event = On Import, DB = Projects - -on performSmartRule(theRecords) - tell application id "DNtp" - repeat with theRecord in theRecords - try - set docText to plain text of theRecord - set docTitle to name of theRecord - set docUUID to uuid of theRecord - set docLink to reference URL of theRecord -- x-devonthink-item://UUID - - -- TODO 패턴 감지: "TODO", "할일", "□", "[ ]", "FIXME" - set hasAction to false - if docText contains "TODO" or docText contains "할일" or docText contains "□" or docText contains "[ ]" or docText contains "FIXME" then - set hasAction to true - end if - - if not hasAction then continue repeat - - -- 액션 아이템 추출 (Python으로 파싱) - set extractCmd to "echo " & quoted form of docText & " | python3 -c \" -import sys, re -text = sys.stdin.read() -patterns = [ - r'(?:TODO|FIXME|할일)[:\\s]*(.+?)(?:\\n|$)', - r'(?:□|\\[ \\])\\s*(.+?)(?:\\n|$)', -] -items = [] -for p in patterns: - items.extend(re.findall(p, text, re.MULTILINE)) -# 최대 5개, 중복 제거 -seen = set() -for item in items[:10]: - item = item.strip() - if item and item not in seen: - seen.add(item) - print(item) - if len(seen) >= 5: - break -\"" - - set actionItems to paragraphs of (do shell script extractCmd) - - if (count of actionItems) = 0 then continue repeat - - -- OmniFocus에 작업 생성 - tell application "OmniFocus" - tell default document - set taskIDs to {} - repeat with actionItem in actionItems - set taskName to docTitle & " — " & (contents of actionItem) - set newTask to make new inbox task with properties {name:taskName, note:"DEVONthink 문서: " & docLink} - set end of taskIDs to id of newTask - end repeat - end tell - end tell - - -- DEVONthink 메타데이터에 OmniFocus Task ID 저장 - set AppleScript's text item delimiters to "," - set taskIDString to taskIDs as text - set AppleScript's text item delimiters to "" - add custom meta data taskIDString for "omnifocusTaskID" to theRecord - - on error errMsg - do shell script "echo '[" & (current date) & "] [omnifocus_sync] [ERROR] " & errMsg & "' >> ~/Documents/code/DEVONThink_my\\ server/logs/omnifocus_sync.log" - end try - end repeat - end tell -end performSmartRule diff --git a/docs/architecture.md b/docs/architecture.md deleted file mode 100644 index 0417d16..0000000 --- a/docs/architecture.md +++ /dev/null @@ -1,1368 +0,0 @@ -# Mac mini M4 Pro — PKM 허브 아키텍처 설계 - -## 시스템 개요 - -Mac mini M4 Pro(64GB RAM, 4TB SSD)를 중심으로, DEVONthink를 **중앙 지식 허브**로 두고, Omni 제품군으로 **실행/계획**, Synology NAS로 **저장/백업**, AI(Claude API + MLX/Ollama)로 **지능화**하는 통합 PKM 시스템. - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Mac mini M4 Pro (허브) │ -│ │ -│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ │ -│ │ DEVONthink │◄──│ DEVONagent │ │ DEVONsphere │ │ -│ │ (지식 허브) │ │ (리서치 엔진) │ │ (시스템 검색) │ │ -│ └──────┬──────┘ └──────────────┘ └─────────────────┘ │ -│ │ │ -│ ┌──────┴──────────────────────────────────────────────┐ │ -│ │ 자동화 레이어 (AppleScript + Shortcuts) │ │ -│ └──────┬──────────────┬───────────────┬───────────────┘ │ -│ │ │ │ │ -│ ┌──────▼──────┐ ┌─────▼─────┐ ┌──────▼──────┐ │ -│ │ OmniFocus │ │ OmniPlan │ │ OmniGraffle │ │ -│ │ (작업 실행) │ │ (프로젝트) │ │ (시각화) │ │ -│ └─────────────┘ └───────────┘ └─────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────┐ │ -│ │ AI 레이어 (Ollama + Claude API) │ │ -│ │ 태깅 · 분류 · RAG · 요약 · 리서치 │ │ -│ └─────────────────────────────────────────────────────┘ │ -└───────────────────────────┬─────────────────────────────────┘ - │ WebDAV / SMB - ┌────────────────┼────────────────┐ -┌──────────▼──────────┐ ┌───────────▼───────────┐ -│ Synology DS1525+ │ │ GPU 서버 (리눅스) │ -│ 백업·메일·채팅 아카이브│ │ 비전·임베딩·리랭킹·Plex │ -└─────────────────────┘ └───────────────────────┘ -``` - ---- - -## DEVONthink 데이터베이스 구조 - -기존 도메인별 DB를 유지하면서, 운영용 DB만 추가하는 **하이브리드 구조**입니다. -64GB RAM이면 13개 DB를 동시에 열어도 문제없습니다. - -### DB 설계 - -**운영 레이어 (신규 추가)** — 자동화 파이프라인용 - -| DB 이름 | 용도 | 저장 위치 | 동기화 | -|---|---|---|---| -| **📥 Inbox** | 미분류 자료 임시 보관, AI 분류 후 도메인 DB로 이동 | 내장 SSD | CloudKit | -| **📧 Archive** | 이메일(MailPlus), 채팅 로그, 영수증 | 내장 SSD | WebDAV (NAS) | -| **🏗️ Projects** | 활성 프로젝트 문서 (OmniFocus 연동) | 내장 SSD | CloudKit | - -**지식 레이어 (기존 유지)** — 도메인별 전문 지식 저장소 - -| DB 이름 | 용도 | 저장 위치 | 동기화 | -|---|---|---|---| -| **00_Note_BOX** | 일반 메모, 스크랩, 잡다한 노트 | 내장 SSD | WebDAV (NAS) | -| **01_Philosophie** | 철학, 사상, 인문학 자료 | 내장 SSD | WebDAV (NAS) | -| **02_Language** | 어학 학습, 번역, 언어학 자료 | 내장 SSD | WebDAV (NAS) | -| **03_Engineering** | 공학 전반 기술 문서 | 내장 SSD | WebDAV (NAS) | -| **04_Industrial safety** | 산업안전, 규정, 인증 문서 | 내장 SSD | WebDAV (NAS) | -| **05_Programming** | 개발, 코드, IT 기술 자료 | 내장 SSD | WebDAV (NAS) | -| **07_General Book** | 일반 도서, 독서 노트 | 내장 SSD | WebDAV (NAS) | -| **97_Production drawing** | 생산 도면, CAD, 설계 자료 | 내장 SSD | WebDAV (NAS) | -| **99_Reference Data** | 범용 레퍼런스, 규격표 | 내장 SSD | WebDAV (NAS) | -| **99_Technicalkorea** | 한국 기술 규정, 국내 기술 자료 | 내장 SSD | WebDAV (NAS) | - -### 자동 분류 라우팅 — Inbox → 도메인 DB - -AI 태깅 후 Smart Rule이 태그 기반으로 적절한 도메인 DB로 자동 이동시킵니다: - -``` -Inbox 진입 → AI 태깅(Ollama) → Smart Rule 라우팅: - -태그 매핑: - #주제/기술/AI-ML, #주제/기술/서버관리 → 05_Programming - #주제/기술/네트워크 → 03_Engineering - $유형/이메일, $유형/채팅로그 → Archive - #주제/산업안전 → 04_Industrial safety - #주제/도면, #주제/설계 → 97_Production drawing - #주제/철학, #주제/인문 → 01_Philosophie - #주제/어학, #주제/번역 → 02_Language - #주제/독서 → 07_General Book - #주제/규정/한국 → 99_Technicalkorea - #주제/규격, #주제/레퍼런스 → 99_Reference Data - 미분류 / 복합 태그 → 00_Note_BOX (기본 폴백) -``` - -### 태그 체계 (Taxonomy) - -계층적 태그 시스템을 설계합니다: - -``` -태그 구조: -├── @상태/ -│ ├── 처리중 -│ ├── 검토필요 -│ ├── 완료 -│ └── 아카이브 -├── #주제/ -│ ├── 기술/ -│ │ ├── 서버관리 -│ │ ├── 네트워크 -│ │ └── AI-ML -│ ├── 산업안전/ -│ │ ├── 법령 ← 10_Legislation 라우팅 -│ │ ├── 위험성평가 ← 50_Practice/Risk_Assessment -│ │ ├── 순회점검 ← 50_Practice/Patrol_Inspection -│ │ ├── 안전교육 ← 50_Practice/Education -│ │ ├── 사고사례 ← 40_Cases -│ │ ├── 신고보고 ← 60_Compliance -│ │ ├── 안전관리자 ← 70_Safety_Manager -│ │ ├── 보건관리자 ← 75_Health_Manager -│ │ └── 규격기준 ← 80_Reference -│ ├── 업무/ -│ └── 개인/ -├── $유형/ -│ ├── 논문 ← 30_Papers 라우팅 (DB 불문) -│ ├── 법령 ← 10_Legislation 라우팅 -│ ├── 기사 -│ ├── 메모 -│ ├── 이메일 -│ ├── 채팅로그 -│ ├── 도면 -│ └── 체크리스트 -└── !우선순위/ - ├── 긴급 - ├── 중요 - └── 참고 -``` - -### DB별 그룹 구조 (Internal Group Structure) - -각 도메인 DB 내부에 통일된 넘버링 체계(10 단위)로 그룹을 구성합니다. -00_Inbox는 AI가 해당 DB로 라우팅했지만 하위 그룹까지 확정 못한 문서의 임시 보관소입니다. - -#### 04_Industrial Safety → [상세 설계서 참조](computer:///sessions/amazing-vigilant-hypatia/mnt/outputs/04-industrial-safety-blueprint.md) - -10_Legislation (법령/해외법령), 20_Theory, 30_Papers, 40_Cases, -50_Practice (9개 실무 하위그룹), 60_Compliance, 70_Safety_Manager, -75_Health_Manager, 80_Reference, 90_Archive -+ 법령 자동 수집 시스템 (한국/미국/일본/EU API 모니터링) -+ DEVONagent 안전 분야 검색 세트 7종 (주간 ~50~85건, 양 조절 전략 포함) - -※ 나머지 도메인 DB(03_Engineering, 05_Programming 등)도 동일한 넘버링 패턴으로 확장 예정 - ---- - -### 커스텀 메타데이터 - -DEVONthink 4의 커스텀 메타데이터 필드를 활용합니다. -단, AI 처리 결과의 저장은 **역할 분리 원칙**을 따릅니다: - -| 필드명 | 타입 | 용도 | -|---|---|---| -| `omnifocusTaskID` | 텍스트 | OmniFocus 작업 역링크 | -| `sourceURL` | URL | 원본 출처 | -| `synologyPath` | 텍스트 | NAS 원본 경로 | -| `lastAIProcess` | 날짜 | 마지막 AI 처리 일시 | -| `sourceChannel` | 텍스트 | 유입 경로 (tksafety/devonagent/law_monitor/inbox_route/email/web_clip/manual) | -| `dataOrigin` | 텍스트 | 데이터 출처 구분 (work = 자사 업무 / external = 외부 참고) | - -> `sourceChannel`과 `dataOrigin`은 04_Industrial Safety에서 우선 적용하고, -> 안정화 후 다른 도메인 DB로 확장합니다. -> 상세 규칙: [04-industrial-safety-blueprint.md § 8. 유입 경로 추적 체계](computer:///sessions/amazing-vigilant-hypatia/mnt/outputs/04-industrial-safety-blueprint.md) 참조 - -### AI 결과물 저장 전략 — 중복 저장 금지 - -GPU 서버에서 처리된 AI 결과물은 **각자 목적에 맞는 곳에만** 저장합니다. -DEVONthink와 ChromaDB에 같은 정보를 이중으로 넣지 않습니다. - -``` -처리 결과 저장 위치 이유 -─────────────────────────────────────────────────────── -벡터 임베딩 ChromaDB만 시맨틱 검색 전용, DEVONthink에선 쓸모없음 -비전 OCR 텍스트 DEVONthink 본문에 병합 검색 가능한 텍스트가 되어야 하므로 필수 -리랭킹 점수 저장 안 함 (휘발) 쿼리 시점에만 의미 있는 일회성 데이터 -태그/분류 DEVONthink 태그만 Smart Group, 브라우징에 활용 -요약 저장 안 함 RAG로 언제든 재생성 가능, 별도 보관 불필요 -OmniFocus 역링크 DEVONthink 메타데이터 양방향 참조에 필요 -``` - -**핵심 원칙:** -- ChromaDB = 벡터 검색 엔진. 여기엔 임베딩만 들어감 -- DEVONthink = 원본 문서 + 사람이 읽는 메타데이터(태그, 링크) -- 요약/분석은 RAG로 실시간 생성하면 되므로 별도 캐싱 불필요 -- 비전 모델의 OCR 결과만 DEVONthink 본문에 반드시 병합 (검색성 확보) - ---- - -## 법령 자동 수집 및 변경 알림 시스템 - -→ [산업안전 상세 설계서 §3](computer:///sessions/amazing-vigilant-hypatia/mnt/outputs/04-industrial-safety-blueprint.md) 참조 - -한국(매일), 미국(주1), 일본(주1), EU(월1) 법령 API를 `law_monitor.py`로 폴링. -변경 감지 시 DEVONthink 자동 저장 + Synology Chat 알림 + OmniFocus 작업 생성. -각국 법령은 저작권 보호 대상이 아니므로 수집에 법적 문제 없음. - ---- - -## 자동화 파이프라인 - -### 문서 수집 파이프라인 (Ingestion) - -``` -[입력 소스] [처리] [출력] - -웹 클리핑 ──────┐ -DEVONagent ────┤ ┌──────────────┐ -스캔 문서 ──────┼──► Inbox ──►│ Smart Rule │──► 자동 태깅 -이메일 ────────┤ │ + Ollama API │ + 적절한 DB로 이동 -파일 드롭 ──────┘ │ + GPU 서버 │ + 벡터 인덱싱 (ChromaDB) - └──────────────┘ + OCR 텍스트 병합 (스캔 시) - ▼ - OmniFocus 작업 생성 - (액션 아이템 감지 시) -``` - -### Smart Rules 설계 - -**Rule 1: AI 자동 태깅 + 도메인 DB 라우팅 (새 문서 도착 시)** -``` -트리거: Inbox DB에 새 문서 추가 -조건: 태그가 비어있음 -동작: - 1. 이미지/스캔 문서 → GPU 서버 VL-7B로 OCR → 본문에 병합 - 2. Mac mini 35B → 태그 + 분류 대상 DB 생성 → DEVONthink 태그에만 저장 - 3. GPU 서버 nomic-embed → 벡터화 → ChromaDB에만 저장 - 4. 태그 기반 도메인 DB 자동 이동: - #주제/프로그래밍, #주제/AI-ML → 05_Programming - #주제/공학, #주제/네트워크 → 03_Engineering - #주제/산업안전, #주제/규정 → 04_Industrial safety - #주제/도면, #주제/설계 → 97_Production drawing - #주제/철학, #주제/인문 → 01_Philosophie - #주제/어학, #주제/번역 → 02_Language - #주제/독서, #주제/서평 → 07_General Book - #주제/규격, #주제/레퍼런스 → 99_Reference Data - #주제/한국기술, #주제/국내규정 → 99_Technicalkorea - 미분류 / 복합 태그 → 00_Note_BOX (폴백) - ※ 요약은 별도 저장하지 않음 (RAG로 실시간 생성) -``` - -**Rule 2: 이메일 아카이브 자동 정리** -``` -트리거: Archive DB에 새 이메일 추가 -조건: MIME 타입이 이메일 -동작: - 1. 발신자 기준 그룹 자동 생성/분류 - 2. 첨부파일 추출 → 태그 기반 도메인 DB로 복제 (기술문서→03, 도면→97 등) - 3. GPU 서버에서 벡터 임베딩 → ChromaDB 인덱싱 - ※ 이메일 요약은 저장하지 않음 (RAG로 검색 시 생성) -``` - -**Rule 3: 프로젝트 문서 → OmniFocus 연동** -``` -트리거: Projects DB에 새 문서 추가 -조건: 텍스트에 "TODO", "할일", "□" 패턴 존재 -동작: - 1. AppleScript로 텍스트에서 액션 아이템 추출 - 2. OmniFocus에 작업 생성 (DEVONthink 링크 포함) - 3. 커스텀 메타데이터에 OmniFocus Task ID 저장 -``` - -### 핵심 AppleScript 모듈 - -#### 모듈 A: Ollama 연동 스크립트 - -```applescript --- DEVONthink 4 Smart Rule에서 호출 --- 문서를 Ollama API로 보내 태그 생성 + GPU 서버로 벡터 인덱싱 -on performSmartRule(theRecords) - repeat with theRecord in theRecords - set docText to plain text of theRecord - set docUUID to uuid of theRecord - - if length of docText > 4000 then - set docText to text 1 thru 4000 of docText - end if - - -- Step 1: Mac mini MLX 35B → 태그 + 분류 대상 DB/그룹 생성 - set shellCmd to "curl -s http://localhost:8800/v1/chat/completions -H 'Content-Type: application/json' -d '{" & ¬ - "\"model\": \"mlx-community/Qwen3.5-35B-A3B-4bit\"," & ¬ - "\"messages\": [{\"role\":\"user\",\"content\":\"다음 문서를 분석하고 JSON으로 응답해줘.\\n" & ¬ - "{\\\"tags\\\": [최대5개_한글태그],\\n" & ¬ - " \\\"domain_db\\\": \\\"DB이름\\\",\\n" & ¬ - " \\\"sub_group\\\": \\\"하위그룹경로\\\"}\\n\\n" & ¬ - "domain_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\\n" & ¬ - "sub_group 예시: 10_Legislation/Notice, 50_Practice/Risk_Assessment, " & ¬ - "40_Cases/Domestic 등 (해당 DB의 그룹 경로)\\n\\n" & ¬ - docText & "\"," & ¬ - "\"stream\": false" & ¬ - "}'" - - set jsonResult to do shell script shellCmd - - -- Step 2: 태그 파싱 → DEVONthink 태그 설정 - set parseCmd to "echo " & quoted form of jsonResult & ¬ - " | python3 -c \"import sys,json; " & ¬ - "r=json.loads(sys.stdin.read()); " & ¬ - "d=json.loads(r['response']); " & ¬ - "print(d.get('domain_db','00_Note_BOX') + '|' + " & ¬ - "d.get('sub_group','00_Inbox') + '|' + " & ¬ - "','.join(d.get('tags',[])))\"" - - try - set classResult to do shell script parseCmd - set AppleScript's text item delimiters to "|" - set resultParts to text items of classResult - set targetDB to item 1 of resultParts - set targetGroup to item 2 of resultParts - set tagResult to item 3 of resultParts - set AppleScript's text item delimiters to "" - - -- 태그 설정 - set tags of theRecord to tagResult - - -- Step 3: 도메인 DB로 이동 + 하위 그룹 라우팅 - set targetDatabase to missing value - repeat with db in databases - if name of db is targetDB then - set targetDatabase to db - exit repeat - end if - end repeat - - if targetDatabase is not missing value then - -- 하위 그룹 경로 확인/생성 후 이동 - set groupPath to "/" & targetGroup - set targetLocation to create location groupPath in targetDatabase - move record theRecord to targetLocation - end if - end try - - -- Step 4: GPU 서버 → 벡터 임베딩 → ChromaDB 인덱싱 (비동기) - do shell script "python3 ~/scripts/embed_to_chroma.py " & ¬ - quoted form of docUUID & " &" - - -- Step 5: 처리 완료 표시 - add custom meta data (current date) ¬ - for "lastAIProcess" to theRecord - - end repeat -end performSmartRule -``` - -#### 모듈 B: DEVONthink ↔ OmniFocus 양방향 연동 - -```applescript --- DEVONthink 4 문서에서 OmniFocus 작업 생성 -on createOmniFocusTask(theRecord) - tell application id "DNtp" - set docName to name of theRecord - set docLink to reference URL of theRecord - set docUUID to uuid of theRecord - end tell - - tell application "OmniFocus" - tell default document - set newTask to make new inbox task with properties { - name: "처리: " & docName, - note: "DEVONthink 문서: " & docLink & return & ¬ - "UUID: " & docUUID - } - set taskID to id of newTask - end tell - end tell - - -- DEVONthink에 역링크 저장 - tell application id "DNtp" - add custom meta data taskID ¬ - for "omnifocusTaskID" to theRecord - end tell - - return taskID -end createOmniFocusTask -``` - -#### 모듈 C: Claude API 심층 분석 - -```applescript --- 복잡한 문서 분석이 필요할 때 Claude API 호출 --- (Ollama 대비 고품질 분석이 필요한 경우) -on analyzeWithClaude(theRecord) - tell application id "DNtp" - set docText to plain text of theRecord - end tell - - -- Claude API 키는 macOS Keychain에 저장 - set apiKey to do shell script ¬ - "security find-generic-password -s 'claude-api-key' -w" - - set shellCmd to "curl -s https://api.anthropic.com/v1/messages " & ¬ - "-H 'x-api-key: " & apiKey & "' " & ¬ - "-H 'anthropic-version: 2023-06-01' " & ¬ - "-H 'content-type: application/json' " & ¬ - "-d '{\"model\":\"claude-sonnet-4-6\"," & ¬ - "\"max_tokens\":2048," & ¬ - "\"messages\":[{\"role\":\"user\"," & ¬ - "\"content\":\"다음 문서를 심층 분석해줘. " & ¬ - "핵심 개념, 관련 키워드, 액션 아이템, " & ¬ - "관련 문서 검색을 위한 시맨틱 태그를 JSON으로: " & ¬ - docText & "\"}]}'" - - set result to do shell script shellCmd - return result -end analyzeWithClaude -``` - ---- - -## Synology NAS 연동 - -### WebDAV 동기화 설정 - -Synology에서 WebDAV Server 패키지를 설치하고 DEVONthink 4 동기화를 설정합니다. - -``` -WebDAV 동기화 (구성 완료): -· URL: https://webdav.hyungi.net/Document_Server/DEVONThink/ -· 기존 도메인 DB 10개 — 이미 동기화 중 - -신규 DB 동기화 추가: -· Archive, Projects → WebDAV 동기화에 포함 -· Inbox → CloudKit 별도 동기화 (MacBook 연동용) -``` - -### MailPlus → DEVONthink 아카이브 - -``` -방법 1: IMAP 직접 연결 -- DEVONthink 4 → File → Import → Email -- MailPlus IMAP 서버: [NAS-IP]:993 (SSL) -- 주기적으로 가져오기 (Smart Rule로 자동화) - -방법 2: 자동화 스크립트 (권장) -- MailPlus API → Python 스크립트 → DEVONthink Apple Event -- Cron 또는 launchd로 주기 실행 -``` - -### Synology Chat → DEVONthink 아카이브 - -```python -#!/usr/bin/env python3 -""" -Synology Chat 메시지를 DEVONthink 4로 아카이브하는 스크립트 -launchd로 매일 1회 실행 -""" -import requests -import subprocess -import json -from datetime import datetime, timedelta - -SYNOLOGY_URL = "https://[NAS-IP]:5001" -CHAT_TOKEN = "YOUR_CHAT_BOT_TOKEN" # Keychain에서 가져오는 것 권장 - -def fetch_chat_messages(channel_id, since_days=1): - """Synology Chat API로 메시지 가져오기""" - since = datetime.now() - timedelta(days=since_days) - url = f"{SYNOLOGY_URL}/webapi/entry.cgi" - params = { - "api": "SYNO.Chat.Post", - "method": "list", - "version": 1, - "token": CHAT_TOKEN, - "channel_id": channel_id, - "limit": 100 - } - resp = requests.get(url, params=params, verify=False) - return resp.json().get("data", {}).get("posts", []) - -def send_to_devonthink(text, title, tags): - """AppleScript를 통해 DEVONthink 4에 문서 생성""" - script = f''' - tell application id "DNtp" - set theDB to open database "/path/to/Archive.dtBase2" - set theGroup to get record at "/Chat Archive/{datetime.now().strftime('%Y/%m')}" in theDB - if theGroup is missing value then - set theGroup to create location "/Chat Archive/{datetime.now().strftime('%Y/%m')}" in theDB - end if - set theRecord to create record with {{ - name: "{title}", - type: markdown, - plain text: "{text}", - tags: "{tags}" - }} in theGroup - end tell - ''' - subprocess.run(["osascript", "-e", script]) - -if __name__ == "__main__": - messages = fetch_chat_messages("general") - digest = "\\n".join([f"**{m['user']}**: {m['msg']}" for m in messages]) - title = f"Chat Digest - {datetime.now().strftime('%Y-%m-%d')}" - send_to_devonthink(digest, title, "채팅로그,아카이브") -``` - -### 백업 전략 - -``` -┌─────────────────────────────────────────────┐ -│ 백업 3-2-1 전략 │ -├─────────────────────────────────────────────┤ -│ │ -│ Mac mini (원본) │ -│ └── DEVONthink DB (내장 SSD 4TB) │ -│ │ │ -│ ├── [실시간] WebDAV Sync → NAS SSD │ -│ │ (DEVONthink 내장 동기화) │ -│ │ │ -│ ├── [매일] Time Machine → NAS HDD │ -│ │ (전체 시스템 백업) │ -│ │ │ -│ └── [주간] Hyper Backup → 외부/클라우드 │ -│ (NAS에서 2차 백업) │ -│ │ -│ NAS 스토리지 배치: │ -│ ├── SSD (SAT5220 480GB): WebDAV Sync Store │ -│ ├── HDD RAID: Time Machine + Hyper Backup │ -│ └── SSD (EVO 870 4TB): 문서/사진 서비스 볼륨 │ -└─────────────────────────────────────────────┘ -``` - ---- - -## AI 통합 아키텍처 - -### 하드웨어별 모델 배치 - -**Mac mini는 메인 범용 모델**, **GPU 서버는 특화 모델 팜 + 미디어 서버**로 역할을 분리합니다. - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🖥️ Mac mini M4 Pro (메인 AI 허브) │ -│ 64GB 통합메모리 · Neural Engine │ -│ │ -│ ┌───────────────────────────────────┐ ┌─────────────────────┐ │ -│ │ 🧠 메인 모델 │ │ 📡 클라우드 │ │ -│ │ Qwen3.5-35B-A3B (4Q) │ │ Claude API │ │ -│ │ │ │ (Sonnet 4.6) │ │ -│ │ MoE: 35B 파라미터, 3B 활성 │ │ │ │ -│ │ 메모리: ~20GB │ │ 심층 분석 │ │ -│ │ 속도: ~80 tok/s │ │ 복잡한 요약 │ │ -│ │ │ │ 리서치 합성 │ │ -│ │ 용도: │ │ 보고서 생성 │ │ -│ │ · Smart Rule 자동 태깅/분류 │ │ │ │ -│ │ · 문서 요약 │ │ 응답: ~3초 │ │ -│ │ · 메타데이터 생성 │ │ 비용: API 과금 │ │ -│ │ · 액션 아이템 추출 │ └─────────────────────┘ │ -│ │ · RAG 질의 응답 │ │ -│ │ · DEVONagent 결과 분석 │ │ -│ │ │ │ -│ │ 품질: 9B 대비 현저히 높음 │ │ -│ │ 비용: 무료 (로컬) │ │ -│ └───────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - │ - 로컬 네트워크 API - (http://gpu-server:11434) - │ -┌─────────────────────────────────────────────────────────────────┐ -│ ⚡ GPU 서버 (특화 모델 팜 + 미디어) │ -│ RTX 4070 Ti Super 16GB VRAM │ -│ │ -│ ┌──────────────────────┐ ┌──────────────────────────────────┐ │ -│ │ 👁️ 비전 모델 │ │ 🔍 리랭커 (Reranker) │ │ -│ │ Qwen2.5-VL-7B (8Q) │ │ bge-reranker-v2-m3 │ │ -│ │ VRAM: ~8GB │ │ VRAM: ~1GB │ │ -│ │ │ │ │ │ -│ │ 용도: │ │ 용도: │ │ -│ │ · 스캔 문서 분석 │ │ · RAG 검색 품질 극대화 │ │ -│ │ · 이미지 캡션/태깅 │ │ · 임베딩 검색 후 정밀 재정렬 │ │ -│ │ · 차트/그래프 해석 │ │ · Top-K → Top-N 정확도 향상 │ │ -│ │ · 사진 자동 분류 │ │ │ │ -│ │ · OCR 보완 │ │ │ │ -│ └──────────────────────┘ └──────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────┐ ┌──────────────────────────────────┐ │ -│ │ 🔗 임베딩 모델 │ │ 📊 VRAM 배분 │ │ -│ │ nomic-embed-text │ │ │ │ -│ │ VRAM: ~0.3GB │ │ 비전 모델 (8Q): ~8GB │ │ -│ │ │ │ 리랭커: ~1GB │ │ -│ │ 용도: │ │ 임베딩: ~0.3GB │ │ -│ │ · 문서 벡터 임베딩 │ │ 시스템: ~2GB │ │ -│ │ · RAG 인덱싱 │ │ ───────────────────── │ │ -│ │ · 쿼리 임베딩 │ │ 합계: ~11.3GB / 16GB │ │ -│ │ │ │ 여유: ~4.7GB ✅ │ │ -│ │ ※ GPU 가속으로 │ │ │ │ -│ │ 대량 임베딩 시 유리 │ │ │ │ -│ └──────────────────────┘ └──────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────────┐ │ -│ │ 🎬 미디어 서비스 │ │ -│ │ Plex Media Server — GPU 하드웨어 트랜스코딩 활용 │ │ -│ └─────────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 임베딩을 GPU 서버로 이전하는 이유 - -임베딩 모델(nomic-embed-text)을 Mac mini에서 GPU 서버로 이전하는 것을 **권장**합니다: - -| 비교 항목 | Mac mini에서 실행 | GPU 서버에서 실행 | -|---|---|---| -| **대량 인덱싱 속도** | CPU 기반, 느림 | CUDA 가속, 5-10배 빠름 | -| **Mac mini 부하** | 35B 모델 + 임베딩 동시 시 경합 | 35B 모델 전용, 쾌적 | -| **VRAM 영향** | 해당 없음 | +0.3GB (무시할 수준) | -| **네트워크 레이턴시** | 없음 | 2.5G 네트워크, 1ms 미만 | -| **배치 처리** | 문서 100개 인덱싱 시 수분 | 문서 100개 인덱싱 시 수십초 | -| **ChromaDB 위치** | Mac mini 유지 | Mac mini 유지 (변동 없음) | - -**결론:** 임베딩 모델은 단일 요청 레이턴시보다 **배치 처리량**이 중요합니다. -GPU 서버의 CUDA 가속을 활용하면 대량 문서 인덱싱이 훨씬 빨라지고, -Mac mini의 통합메모리를 35B 모델에 온전히 할당할 수 있습니다. -nomic-embed-text는 0.3GB에 불과해 GPU 서버 VRAM에 거의 영향이 없고, -2.5G 네트워크 환경이라 API 호출 레이턴시도 무시할 수준입니다. - -다만 **ChromaDB는 Mac mini에 유지**합니다. RAG 질의 시 벡터 검색 → -리랭킹 → 35B 응답 생성이 연속으로 일어나는데, 벡터 DB가 로컬에 있어야 -이 파이프라인이 가장 빠릅니다. - -### 3-Tier AI 라우팅 전략 - -``` - ┌──────────────────────────────┐ - │ 작업 라우터 (AppleScript) │ - │ 문서 유형 + 복잡도 기반 분기 │ - └──────────┬───────────────────┘ - │ - ┌────────────────────┼────────────────────┐ - ▼ ▼ ▼ - ┌─────────────────┐ ┌──────────────┐ ┌────────────────────┐ - │ Tier 1: 로컬 │ │ Tier 2: API │ │ Tier 3: 특화 │ - │ Mac mini │ │ Claude │ │ GPU 서버 │ - │ (메인) │ │ (클라우드) │ │ (보조) │ - ├─────────────────┤ ├──────────────┤ ├────────────────────┤ - │ Qwen3.5-35B-A3B │ │ Sonnet 4.6 │ │ Qwen2.5-VL-7B (8Q)│ - │ 4Q / ~80 tok/s │ │ │ │ bge-reranker-v2-m3 │ - │ │ │ │ │ nomic-embed-text │ - ├─────────────────┤ ├──────────────┤ ├────────────────────┤ - │ · 자동 태깅/분류 │ │ · 심층 분석 │ │ · 이미지/스캔 분석 │ - │ · 문서 요약 │ │ · 리서치 합성 │ │ · RAG 리랭킹 │ - │ · 메타데이터 │ │ · 보고서 생성 │ │ · 문서 임베딩/인덱싱│ - │ · 액션아이템추출 │ │ · 복잡한 추론 │ │ · 사진 자동 분류 │ - │ · RAG 응답생성 │ │ · 다국어 번역 │ │ · OCR 후처리 │ - ├─────────────────┤ ├──────────────┤ ├────────────────────┤ - │ 속도: ~80 tok/s │ │ 속도: ~3초 │ │ 속도: GPU 가속 │ - │ 비용: 무료 │ │ 비용: 과금 │ │ 비용: 무료 │ - │ 품질: ★★★★☆ │ │ 품질: ★★★★★ │ │ 품질: 영역별 최고 │ - └─────────────────┘ └──────────────┘ └────────────────────┘ -``` - -**라우팅 규칙 (상세):** - -| 조건 | 라우팅 | 이유 | -|---|---|---| -| 텍스트 문서 + 태깅/분류/요약 | Tier 1 (Mac mini 35B) | 메인 범용, 품질 충분 | -| 이미지 포함 문서 / 스캔 PDF | Tier 3 → Tier 1 | 비전 모델로 텍스트 추출 후 35B로 분석 | -| 심층 분석 / 긴 보고서 생성 | Tier 2 (Claude API) | 최고 품질 필요 시 | -| RAG 검색 결과 리랭킹 | Tier 3 (GPU reranker) | 검색 정확도 극대화 | -| RAG 최종 응답 생성 | Tier 1 (Mac mini 35B) | 컨텍스트 기반 응답 | -| 새 문서 벡터 인덱싱 | Tier 3 (GPU embed) | CUDA 가속 배치 처리 | -| 대량 배치 (100+ 문서) | Tier 1 + Tier 3 병렬 | 양쪽 분산 처리 | -| Synology Photos 자동 태깅 | Tier 3 (GPU vision) | 이미지 분석 특화 | - -### 모델 간 협업 파이프라인 - -``` -예시: 스캔 문서가 DEVONthink에 들어왔을 때 - -1. [Smart Rule 트리거] 새 PDF 감지, 이미지 기반 문서로 판단 - │ -2. [GPU 서버 · Qwen2.5-VL-7B 8Q] - 이미지 분석 → 텍스트 추출 (OCR) → DEVONthink 본문에 병합 - │ -3. [Mac mini · Qwen3.5-35B-A3B] - 추출된 텍스트로 태그 생성 → DEVONthink 태그에만 저장 - │ -4. [GPU 서버 · nomic-embed-text] - 문서 벡터 임베딩 → ChromaDB에만 저장 - │ -5. [결과] DEVONthink에는 본문(OCR)+태그+처리일시만 - ChromaDB에는 벡터만. 요약은 저장하지 않음 (RAG로 실시간 생성) - - -예시: RAG 질의 시 - -1. [사용자 질문] "서버 마이그레이션 관련 자료 정리해줘" - │ -2. [GPU 서버 · nomic-embed-text] 쿼리 임베딩 - │ -3. [Mac mini · ChromaDB] 벡터 유사도 검색 → Top-20 후보 - │ -4. [GPU 서버 · bge-reranker-v2-m3] - Top-20 → 정밀 리랭킹 → Top-5 선정 - │ -5. [Mac mini · Qwen3.5-35B-A3B] - Top-5 컨텍스트 + 질문 → 종합 답변 생성 - (x-devonthink-item:// 출처 링크 포함) -``` - -### GPU 서버 Ollama 멀티모델 운영 - -```bash -# GPU 서버 Ollama 설정 (/etc/systemd/system/ollama.service) -# 환경변수: -OLLAMA_HOST=0.0.0.0:11434 # 네트워크 노출 -OLLAMA_NUM_PARALLEL=2 # 동시 요청 2개 -OLLAMA_MAX_LOADED_MODELS=3 # 동시 로드 모델 3개 (비전+리랭커+임베딩) -OLLAMA_KEEP_ALIVE=10m # 미사용 시 10분 후 언로드 - -# 모델 다운로드 -ollama pull qwen2.5-vl:7b-instruct-q8_0 # 비전 모델 8Q (~8GB) -ollama pull bge-reranker-v2-m3 # 리랭커 (~1GB) -ollama pull nomic-embed-text # 임베딩 (~0.3GB) - -# Mac mini에서 GPU 서버 호출 예시 -# 비전 분석 -curl http://gpu-server:11434/api/generate \ - -d '{"model":"qwen2.5-vl:7b-instruct-q8_0", ...}' - -# 임베딩 (배치) -curl http://gpu-server:11434/api/embed \ - -d '{"model":"nomic-embed-text", "input":["문서1 텍스트", "문서2 텍스트", ...]}' -``` - -**`keep_alive` 활용 전략:** -- 비전 모델 (8Q): `keep_alive: "30m"` — 자주 사용, 항상 대기 -- 리랭커: `keep_alive: "10m"` — RAG 쿼리 시 활성 -- 임베딩: `keep_alive: "30m"` — 새 문서 인덱싱 빈도에 맞춰 - -세 모델 모두 상주시켜도 ~9.3GB로 16GB VRAM의 58% 수준입니다. - -### RAG 시스템 구성 - -``` -┌─────────────────────────────────────────────────────┐ -│ RAG 파이프라인 │ -│ │ -│ [DEVONthink DB] (Mac mini) │ -│ │ │ -│ ▼ │ -│ [문서 추출] ← Smart Rule (새 문서 추가 시 트리거) │ -│ │ │ -│ ▼ │ -│ [청킹] → 의미 단위로 텍스트 분할 (500토큰) │ -│ │ │ -│ ▼ │ -│ [임베딩] → GPU 서버 Ollama (nomic-embed-text, CUDA) │ -│ │ │ -│ ▼ │ -│ [벡터 저장] → ChromaDB (Mac mini 로컬) │ -│ │ │ -│ ─ ─ ─ ─ ─ ─ 쿼리 시 ─ ─ ─ ─ ─ ─ │ -│ │ │ -│ [질문 입력] │ -│ │ │ -│ ▼ │ -│ [쿼리 임베딩] → GPU 서버 (nomic-embed-text) │ -│ │ │ -│ ▼ │ -│ [유사도 검색] → ChromaDB (Mac mini, Top-20) │ -│ │ │ -│ ▼ │ -│ [리랭킹] → GPU 서버 (bge-reranker, Top-5 선정) │ -│ │ │ -│ ▼ │ -│ [컨텍스트 조합] + 원본 DEVONthink 링크 │ -│ │ │ -│ ▼ │ -│ [LLM 응답] → Mac mini (35B) 또는 Claude API │ -│ └── 출처 링크 포함 (x-devonthink-item://UUID) │ -└─────────────────────────────────────────────────────┘ -``` - -### DEVONagent + AI 리서치 자동화 - -#### 검색 세트 전체 구성 - -``` -Mac mini 자동 스케줄 (9개 검색 세트) - -안전 분야 (7종, 상세 → 산업안전 설계서 §4 참조): -├── [SS-01] 🇰🇷 한국 산업안전 뉴스 매일 06:00 15~25건/주 -├── [SS-02] 🇰🇷 중대재해·판례 매일 06:15 5~10건/주 -├── [SS-04] 🇺🇸 미국 안전 동향 주 1회 (월) 10~15건/주 -├── [SS-05] 🇯🇵 일본 안전 동향 주 1회 (수) 5~10건/주 -├── [SS-06] 🇪🇺 유럽 안전 동향 월 2회 2~4건/주 -├── [SS-07] 🌐 국제 안전 전문지 주 1회 (금) 5~10건/주 -└── [SS-08] 📚 학술 논문 (안전공학) 주 1회 (토) 5~10건/주 - -기술/일반 (2종): -├── [SS-03] 🇰🇷 한국 기술 뉴스 (IT/AI) 매일 06:30 10~15건/주 -└── [SS-09] 🔧 IT/개발 뉴스 매일 06:30 10~15건/주 - -전체 하루 평균: ~15~20건 (안전 8~12 + 기술 5~8) - -MacBook 수동 (필요 시) -├── Quick Research (미팅 전 주제 조사) -└── Deep Dive (특정 사안 심층 리서치) - -※ 안전 분야 검색 세트 상세 (소스, 쿼리, 양 조절 전략) - → 산업안전 설계서 §4 참조 -``` - -#### 수집 결과 자동 처리 흐름 - -``` -DEVONagent 수집 완료 - │ - ▼ -DEVONthink Inbox 도착 (자동 전송) - │ - ▼ -Smart Rule 1차: AI 태깅 + 도메인 DB 판단 - │ - ├─ 산업안전 관련 → 04_Industrial Safety/00_Inbox - ├─ IT/기술 관련 → 05_Programming 또는 03_Engineering - └─ 기타 → 00_Note_BOX - │ - ▼ -Smart Rule 2차: 하위 그룹 라우팅 - │ - ├─ 뉴스 기사 ($유형/기사) - │ ├─ 사고사례 키워드 → 40_Cases/Domestic/ - │ ├─ 법령 개정 키워드 → 10_Legislation/KR_Archive/ - │ ├─ 해외 태그 → 40_Cases/International/ - │ └─ 일반 안전뉴스 → 00_Inbox/ (수동 분류 대기) - │ - ├─ 판례 ($유형/법령 + 사고사례) - │ → 60_Compliance/Audit/ - │ - ├─ 논문 ($유형/논문) - │ → 30_Papers/ - │ - └─ KOSHA Guide / 기술지침 - → 80_Reference/Standards/ - │ - ▼ -ChromaDB 벡터 인덱싱 (비동기) - → RAG 검색에 즉시 반영 - │ - ▼ -주간 다이제스트 생성 (Claude API) -├── 한 주간 수집된 자료 종합 분석 -├── 핵심 트렌드 및 인사이트 추출 -└── OmniOutliner로 구조화된 보고서 생성 -``` - ---- - -## Omni 제품군 통합 - -### 통합 맵 - -``` -┌─────────────┐ ┌──────────────┐ ┌──────────────┐ -│ OmniFocus │◄───►│ DEVONthink │◄───►│ OmniOutliner │ -│ (작업 관리) │ │ (지식 허브) │ │ (구조화 사고) │ -└──────┬──────┘ └──────┬───────┘ └──────────────┘ - │ │ ▲ - │ │ │ - ▼ ▼ │ -┌─────────────┐ ┌──────────────┐ │ -│ OmniPlan │ │ OmniGraffle │──────────────┘ -│ (프로젝트) │ │ (시각화) │ -└─────────────┘ └──────────────┘ -``` - -### 연동 시나리오 - -**시나리오 A: 리서치 → 계획 → 실행** -``` -DEVONagent로 주제 리서치 -→ 결과가 DEVONthink에 축적 -→ AI가 자동 분류 및 요약 -→ OmniOutliner로 리서치 결과 구조화 -→ OmniPlan으로 프로젝트 타임라인 수립 -→ OmniFocus에 실행 가능한 작업 분배 -→ OmniGraffle로 아키텍처/프로세스 다이어그램 작성 -→ 모든 산출물이 DEVONthink에 저장 (역링크 유지) -``` - -**시나리오 B: 문서 처리 파이프라인** -``` -문서 도착 (스캔, 이메일, 웹클립) -→ DEVONthink OCR 처리 (필요 시) -→ Ollama 자동 분류 + 태깅 -→ 액션 아이템 감지 → OmniFocus 작업 생성 -→ 관련 프로젝트 연결 → OmniPlan 업데이트 -→ 정기 리뷰 시 DEVONsphere로 관련 자료 탐색 -``` - -### OmniFocus 통합 스크립트 - -```applescript --- DEVONthink 4에서 선택된 문서의 액션 아이템을 --- OmniFocus 프로젝트로 일괄 생성 -tell application id "DNtp" - set selectedRecords to selection - repeat with theRecord in selectedRecords - set docName to name of theRecord - set docLink to reference URL of theRecord - set docText to plain text of theRecord - end repeat -end tell - --- AI로 액션 아이템 추출 -set extractCmd to "curl -s http://localhost:8800/v1/chat/completions -H 'Content-Type: application/json' -d '{" & ¬ - "\"model\":\"mlx-community/Qwen3.5-35B-A3B-4bit\"," & ¬ - "\"messages\":[{\"role\":\"user\",\"content\":\"다음 텍스트에서 할 일(TODO)만 추출해서 " & ¬ - "한 줄에 하나씩 출력해줘:\\n" & docText & "\"}]}'" -set aiResult to do shell script extractCmd - --- OmniFocus에 작업 생성 -tell application "OmniFocus" - tell default document - set theProject to first flattened project where name is "DEVONthink Actions" - - set taskLines to paragraphs of aiResult - repeat with taskLine in taskLines - if length of taskLine > 2 then - make new task with properties { - name: taskLine, - note: "출처: " & docLink, - project: theProject - } - end if - end repeat - end tell -end tell -``` - ---- - -## MacBook Pro 외부 사용 연동 - -MacBook Pro (M3)는 **소비 + 수집 클라이언트**로 위치시킵니다. -외부에서 자료를 열람/검색하고, DEVONagent로 현장 리서치를 수행하며, -DEVONsphere로 시스템 전역 검색을 하되, AI 처리는 홈 서버에 위임하는 구조입니다. - -### 동기화 구성 - -``` -┌─────────────────────┐ ┌──────────────────┐ -│ MacBook Pro (M3) │ ◄── CloudKit ──► │ Mac mini (허브) │ -│ │ ◄── WebDAV ──► │ │ -│ DEVONthink 4 │ 동기화 DB: │ DEVONthink 4 │ -│ · Inbox (CloudKit) │ · Inbox │ · 전체 13개 DB │ -│ · Projects (CK) │ · Projects │ │ -│ · 05_Programming │ · 자주 쓰는 │ │ -│ · 03_Engineering │ 도메인 DB (WebDAV)│ │ -│ │ │ │ -│ DEVONagent Pro │ → 결과를 Inbox에 │ DEVONagent Pro │ -│ (현장 리서치) │ → CloudKit 동기화 │ (자동 스케줄) │ -│ │ │ │ -│ DEVONsphere Express │ (로컬 전역 검색) │ DEVONsphere │ -│ │ │ │ -│ OmniFocus │ ◄── 자체 동기화 ──► │ OmniFocus │ -│ OmniOutliner │ ◄── 자체 동기화 ──► │ OmniOutliner │ -└─────────────────────┘ └──────────────────┘ -``` - -**동기화 전략:** -- Inbox, Projects → CloudKit (빠른 동기화, 용량 작음) -- 자주 참조하는 도메인 DB → WebDAV 선택 동기화 (예: 05_Programming, 03_Engineering 등 업무에 따라 선택) -- 나머지 도메인 DB → 맥북에 동기화하지 않음 (용량 절약, 필요 시 Tailscale RAG로 검색) -- Archive → 맥북에 동기화하지 않음 (MailPlus 메일은 웹으로 직접 접근) -- 필요 시 Archive를 선택적으로 WebDAV 동기화 가능 - -### DEVONsphere Express — 맥북의 만능 검색 입구 - -DEVONthink를 열지 않고도 **단축키 하나로** 모든 자료를 즉시 찾는 도구입니다. -맥북에서는 특히 가치가 높습니다: - -``` -검색 범위: -├── DEVONthink 동기화 DB (Inbox, Projects, 선택한 도메인 DB들) -├── DEVONthink Archive DB ← MailPlus 메일이 여기로 IMAP 임포트됨 -├── 로컬 파일 (문서, 다운로드 등) -├── 브라우저 북마크 / 히스토리 -└── 연락처 - -⚠️ Synology MailPlus 메일 검색: - DEVONsphere의 "Mail" 검색은 Apple Mail.app 전용입니다. - MailPlus를 쓰는 경우, 메일은 IMAP → DEVONthink Archive DB로 - 자동 임포트되므로 DEVONthink DB 검색을 통해 메일도 검색됩니다. - 즉, MailPlus 메일도 DEVONsphere에서 찾을 수 있습니다. - -사용 시나리오: -· 미팅 중 "그 보고서 어디 있었지?" → Ctrl+Cmd+Space → 즉시 검색 -· "지난달 받은 견적서 어디 있지?" → Archive DB의 MailPlus 메일에서 검색 -· 브라우저에서 읽다가 관련 자료 찾기 → DEVONsphere가 유사도까지 표시 -· DEVONthink 열기 귀찮을 때 빠르게 문서 미리보기 -· Spotlight보다 정확한 결과 (DEVONthink AI 유사도 기반) -``` - -**설정 팁:** -- 시작 시 자동 실행 (로그인 항목에 추가) -- 메모리: ~50MB 수준, 배터리 영향 무시 -- 단축키: `Ctrl+Cmd+Space` (기본값, Spotlight와 충돌 시 변경) - -### DEVONagent Pro — 맥북에서의 역할 분담 - -Mac mini에서는 **자동 스케줄 리서치**, 맥북에서는 **현장 수동 리서치**로 -역할을 나눕니다. - -``` -┌─────────────────────────────────────────────────────────┐ -│ DEVONagent 역할 분담 │ -├────────────────────────┬────────────────────────────────┤ -│ Mac mini (자동화) │ MacBook Pro (현장) │ -├────────────────────────┼────────────────────────────────┤ -│ 스케줄 검색 세트 │ 수동 딥 리서치 │ -│ · 매일 06:00 자동 실행 │ · 미팅 전 주제 사전 조사 │ -│ · 기술뉴스, 학술논문 등 │ · 새 프로젝트 킥오프 자료 수집 │ -│ · 결과 → Inbox → AI 태깅│ · 현장에서 발생한 주제 즉시 리서치│ -│ │ │ -│ 배치 + 자동화 중심 │ 인터랙티브 + 즉시성 중심 │ -├────────────────────────┴────────────────────────────────┤ -│ 공통: 결과는 모두 DEVONthink Inbox → CloudKit 동기화 │ -│ → Mac mini Smart Rule이 자동 태깅 + ChromaDB 인덱싱 │ -└─────────────────────────────────────────────────────────┘ -``` - -**맥북에서 DEVONagent가 브라우저보다 나은 점:** -- 여러 검색엔진(Google, Bing, DuckDuckGo, 학술 DB 등)을 동시에 돌림 -- 결과를 자동 중복 제거 + 관련도 정렬 -- 한 번에 DEVONthink Inbox로 전송 (개별 웹클리핑 불필요) -- 검색 세트를 저장해두면 같은 주제 후속 리서치가 빠름 - -**맥북 전용 검색 세트 추천:** - -``` -검색 세트 예시: -· "Quick Research" — Google + DuckDuckGo + Wikipedia, 상위 20개 결과 - → 미팅 전 빠른 사전 조사용 -· "Academic Deep" — Google Scholar + arXiv + Semantic Scholar - → 논문/기술 자료 심층 검색용 -· "Industry News" — 커스텀 RSS/사이트 기반 - → 업계 동향 현장 체크용 -``` - -**워크플로우: 외부 미팅 시나리오** -``` -미팅 30분 전 -→ DEVONagent "Quick Research"로 상대방/주제 검색 -→ 결과 한 번에 Inbox로 전송 -→ DEVONsphere로 내 기존 자료 중 관련 문서 탐색 -→ 두 결과를 조합해 미팅 준비 완료 - -미팅 중 -→ DEVONsphere로 키워드 즉시 검색 (관련 자료 찾기) -→ 메모를 DEVONthink Inbox에 마크다운으로 작성 - -미팅 후 -→ CloudKit 동기화 → Mac mini가 자동 처리 -→ 태깅 + 인덱싱 완료 → 다음에 RAG로 "지난 미팅 내용" 질의 가능 -``` - -### 외부에서 RAG 접근 - -맥북에서도 내 지식베이스에 질문하고 싶을 때를 위한 두 가지 방안: - -**방안 A: Tailscale VPN (권장)** -``` -MacBook Pro → Tailscale → Mac mini:11434 (Ollama) - → Mac mini:8000 (RAG API) - -장점: 설정 간단, 어디서든 접속, 별도 포트포워딩 불필요 -구현: Mac mini에 간단한 RAG API 서버 (FastAPI) 하나 띄우면 - 맥북에서 웹 브라우저나 Shortcuts로 질의 가능 -``` - -**방안 B: Synology 경유 역방향 프록시** -``` -MacBook Pro → Synology DDNS → 역방향 프록시 → Mac mini RAG API - -장점: 이미 NAS 인프라 활용, HTTPS 자동 (Let's Encrypt) -단점: NAS 설정 필요, 보안 관리 포인트 추가 -``` - -### 맥북에서의 워크플로우 - -``` -[외부 작업 시] -DEVONthink에서 자료 검색/열람 (동기화된 DB) -→ 웹클리핑/메모 → Inbox에 저장 → CloudKit으로 Mac mini에 동기화 -→ Mac mini Smart Rule이 자동 태깅 + 인덱싱 (비동기) -→ 다음에 맥북 열면 결과 동기화되어 있음 - -[RAG 질의 시] -Tailscale 연결 → RAG API에 자연어 질문 -→ Mac mini에서 GPU 임베딩 → ChromaDB 검색 → 리랭킹 → 35B 응답 -→ 결과에 x-devonthink-item:// 링크 포함 -→ 맥북 DEVONthink에서 해당 문서 바로 열기 - -[OmniFocus 연동] -맥북에서 완료한 작업 → 자체 동기화로 Mac mini에 반영 -→ Mac mini AppleScript가 DEVONthink 메타데이터 업데이트 -``` - -### 맥북에 Ollama 로컬을 올릴 필요가 있는가? - -**권장: 올리지 않음.** M3 맥북에서 35B 모델을 돌리면 배터리 소모가 크고, -주 용도가 DEVONthink 브라우징+검색이라면 DEVONthink 자체 검색 + -VPN 경유 RAG API로 충분합니다. 오프라인이 필요한 상황이라면 -가벼운 모델(Qwen3.5-3B 등)을 올리는 것은 선택 가능합니다. - ---- - -## 일일 워크플로우 - -### 아침 루틴 (자동) -``` -06:00 DEVONagent 자동 검색 실행 -06:30 검색 결과 → DEVONthink Inbox -06:31 Smart Rule → Ollama 자동 분류/태깅 -07:00 MailPlus 새 메일 → DEVONthink Archive -07:01 Chat 메시지 다이제스트 → DEVONthink Archive -``` - -### 작업 시 (수동 + 반자동) -``` -DEVONsphere Express로 관련 자료 즉시 검색 -DEVONthink에서 문서 작업 → 자동으로 OmniFocus 작업 생성 -OmniOutliner로 아이디어 구조화 → DEVONthink에 저장 -RAG 시스템으로 내 지식베이스에 질문 -``` - -### Daily Digest (자동 — 매일 20:00) - -전체 DB의 하루 변화를 자동 집계하여 MD 파일 + OmniFocus 액션으로 분리 배달합니다. - -``` -실행 주체: pkm_daily_digest.py (launchd, 매일 20:00) - -수집 대상: - DEVONthink → 오늘 추가/수정된 문서 (모든 DB) - 법령 모니터 → 법령 제·개정 감지 건 - DEVONagent → 오늘 수집된 뉴스/자료 수 - TKSafety → 동기화된 업무 데이터 (활성화 시) - OmniFocus → 오늘 완료/추가/기한초과 작업 - -출력 1 — MD 다이제스트 (읽기용): - 저장: 00_Note_BOX/Daily_Digest/2026-03-24_digest.md - 형식: - - ┌─────────────────────────────────────────────┐ - │ 📋 PKM Daily Digest — 2026-03-24 (월) │ - ├─────────────────────────────────────────────┤ - │ │ - │ ■ 문서 현황 │ - │ 신규 12건 | 수정 3건 | 자동분류 10건 │ - │ Inbox 미처리 2건 │ - │ │ - │ ■ DB별 변동 │ - │ 04_Industrial Safety +5 (뉴스3, 법령1, 업무1)│ - │ 03_Engineering +3 │ - │ 05_Programming +2 │ - │ 99_Reference Data +2 │ - │ │ - │ ■ 법령 변경 │ - │ ⚠ 산업안전보건법 시행규칙 일부개정 │ - │ (시행일: 2026-04-01) │ - │ ⚠ OSHA CFR 1926.502 — Fall Protection │ - │ (Proposed Rule Change) │ - │ │ - │ ■ 주요 뉴스 (DEVONagent 수집) │ - │ · 국내 안전: 8건 수집 │ - │ · 해외 안전: 4건 수집 │ - │ · 상위 3건 요약 (Ollama 35B 생성) │ - │ │ - │ ■ TKSafety 동기화 (활성화 시) │ - │ 위험성평가 2건 | 순회점검 1건 │ - │ 시정조치 overdue 1건 ⚠ │ - │ │ - │ ■ OmniFocus 요약 │ - │ 완료 5건 | 신규 3건 | 기한초과 1건 │ - │ │ - │ ■ 시스템 상태 │ - │ ChromaDB 벡터: 12,847개 (+15) │ - │ Inbox 잔여: 2건 │ - │ NAS 동기화: 정상 │ - └─────────────────────────────────────────────┘ - -출력 2 — OmniFocus 액션 (행동이 필요한 것만): - 자동 생성 조건: - · 법령 변경 감지 → "법령 변경 검토: [법령명]" (프로젝트: PKM/법령리뷰) - · Inbox 미처리 3건 이상 → "Inbox 정리 필요 (N건 미분류)" - · 시정조치 overdue → "시정조치 기한초과: [내용]" (긴급 플래그) - · 분류 실패 문서 존재 → "수동 분류 필요 (N건)" - · ChromaDB 인덱싱 실패 → "벡터 인덱싱 오류 점검" - -출력 3 — Synology Chat 알림 (선택, 한 줄 요약): - "📋 오늘 다이제스트: 신규 12건, 법령변경 2건, overdue 1건 ⚠" - → DEVONthink 다이제스트 파일 링크 포함 -``` - -``` -데이터 수집 방법 (AppleScript + Python 혼합): - -1. DEVONthink 문서 수집 — AppleScript - tell application id "DNtp" - set todayDocs to search "date:today" in database "..." - end tell - → 각 DB별 today 신규/수정 건수 집계 - -2. 법령 변경 — law_monitor.py 로그 파싱 - ~/logs/law_monitor/ 에서 오늘자 변경 감지 로그 읽기 - -3. DEVONagent 수집 현황 — DEVONagent 로그 또는 Inbox 카운트 - 오늘 수집된 문서 중 sourceChannel = devonagent 카운트 - -4. OmniFocus 현황 — AppleScript - tell application "OmniFocus" - set todayCompleted to completed tasks whose completion date is today - end tell - -5. 시스템 상태 — Python - ChromaDB collection.count(), NAS ping, sync 로그 확인 - -6. 상위 뉴스 요약 — Ollama 35B - 오늘 수집된 뉴스 중 상위 3건을 2-3문장으로 요약 - -7. MD 생성 → DEVONthink 00_Note_BOX/Daily_Digest/에 저장 -8. 액션 아이템 → OmniFocus에 자동 생성 -9. (선택) Synology Chat webhook으로 알림 전송 -``` - -``` -보관 정책: -· Daily Digest MD: 90일 보관 후 90_Archive로 이동 (Smart Rule) -· 주간 요약: 매주 월요일, 지난 7일 다이제스트를 Ollama로 요약 → 주간 리포트 생성 -· 월간 통계: 매월 1일, 문서 증가 추이/DB별 성장/법령 변경 이력 → 월간 리포트 -``` - -### 저녁 리뷰 (반자동) -``` -20:00 Daily Digest 자동 생성 (위 참조) -20:01 OmniFocus에 액션 아이템 자동 추가 -DEVONthink Inbox 비우기 (AI가 대부분 처리, 검토만) -OmniFocus 리뷰 → 완료 작업의 DEVONthink 메타데이터 업데이트 -주간 리뷰 시: Claude API로 한 주 다이제스트 자동 생성 -``` - ---- - -## 설치 및 구성 순서 - -### Phase 1: 기반 설치 (Day 1) -``` -□ DEVONthink 4 설치 및 라이선스 -□ DEVONagent Pro 설치 -□ DEVONsphere Express 설치 -□ OmniFocus, OmniOutliner, OmniGraffle, OmniPlan 설치 -□ Ollama 확인 (이미 설치됨) -□ GPU 서버에 nomic-embed-text, Qwen2.5-VL-7B 8Q, bge-reranker 다운로드 -□ ChromaDB 설치 (pip install chromadb) — Mac mini -□ Python 환경 설정 (venv 권장) -□ Plex Media Server를 GPU 서버로 이전 -``` - -### Phase 2: DEVONthink 설정 (Day 2-3) -``` -□ 운영 DB 생성 (Inbox, Archive, Projects) — 기존 10개 도메인 DB는 유지 -□ 기존 도메인 DB 점검 (00_Note_BOX ~ 99_Technicalkorea) — 그룹 구조 정리 -□ 태그 체계 생성 -□ 커스텀 메타데이터 필드 설정 -□ Smart Rules 구성 (AI 자동 분류, 이메일 정리, OmniFocus 연동) -□ AppleScript 모듈 설치 및 테스트 -``` - -### Phase 3: Synology 연동 (Day 3-4) -``` -□ WebDAV Server 설치 및 HTTPS 설정 -□ DEVONthink WebDAV 동기화 구성 -□ MailPlus IMAP → DEVONthink 가져오기 설정 -□ Chat 아카이브 스크립트 설정 (launchd) -□ Time Machine 백업 대상 설정 -``` - -### Phase 4: AI 파이프라인 (Day 4-5) -``` -□ Ollama 태깅/분류 프롬프트 최적화 -□ Claude API 키 Keychain 등록 -□ RAG 파이프라인 구축 (GPU 서버 임베딩 + Mac mini ChromaDB) -□ DEVONthink Smart Rule과 AI 연동 테스트 -□ DEVONagent 자동 검색 스케줄 설정 -``` - -### Phase 5: Omni 통합 (Day 5-6) -``` -□ OmniFocus 프로젝트 구조 설정 -□ DEVONthink ↔ OmniFocus AppleScript 테스트 -□ OmniOutliner 템플릿 생성 -□ 전체 워크플로우 End-to-End 테스트 -``` - -### Phase 6: 최적화 (Day 7+) -``` -□ AI 분류 정확도 모니터링 및 프롬프트 튜닝 -□ 자주 쓰는 워크플로우 Keyboard Maestro/Shortcuts 등록 -□ 성능 모니터링 (RAM, CPU 사용량) -□ 기존 데이터 마이그레이션 (있을 경우) -``` - ---- - -## 하드웨어 리소스 예상 - -### Mac mini M4 Pro (64GB 통합메모리) - -``` -서비스 RAM CPU/GPU 비고 -───────────────────────────────────────────────────────────── -DEVONthink 4 4-8GB 낮음 DB 크기에 따라 -DEVONagent Pro 1-2GB 간헐적 검색 시에만 -OmniFocus 0.5GB 낮음 -OmniPlan 0.5GB 낮음 -OmniOutliner 0.3GB 낮음 -OmniGraffle 0.5GB 낮음 -MLX (Qwen3.5-35B-A3B 4bit) ~20GB 중간 MoE: 3B만 활성 -ChromaDB 1-2GB 낮음 -Roon Core 2-4GB 낮음 -Komga 0.5GB 낮음 -기타 시스템 4-6GB - - -합계 예상 ~35-44GB -여유분 ~20-29GB ✅ 충분 -``` - -**MoE 모델의 이점:** Qwen3.5-35B-A3B는 총 35B 파라미터이지만 추론 시 3B만 -활성화되므로, 실제 연산량은 9B 모델보다 가벼우면서도 품질은 35B급입니다. -M4 Pro의 통합메모리 + Neural Engine 조합으로 ~80 tok/s를 달성하며, -이는 Smart Rule 자동화에 실용적인 속도입니다. - -Plex를 GPU 서버로 이전하고 임베딩도 GPU로 넘김으로써, Mac mini는 이전 대비 -약 2GB 정도 여유가 더 생겼습니다. - -### GPU 서버 (RTX 4070 Ti Super 16GB VRAM) - -``` -서비스 VRAM 상태 비고 -───────────────────────────────────────────────────────────── -Qwen2.5-VL-7B (8Q) ~8GB 상주 비전/이미지 분석 -bge-reranker-v2-m3 ~1GB 상주 RAG 리랭킹 -nomic-embed-text ~0.3GB 상주 임베딩 (CUDA 가속) -Plex HW Transcoding ~1-2GB 간헐적 NVENC/NVDEC 활용 -시스템 오버헤드 ~2GB - - -상주 AI 합계 ~9.3GB / 16GB ✅ 여유 -Plex 트랜스코딩 포함 시 ~11.3GB / 16GB ✅ 가능 -``` - -**VL 모델 8Q 업그레이드 효과:** -이전 4Q 대비 8Q는 VRAM을 ~3GB 더 사용하지만, 코더 모델을 제거하면서 확보한 -여유분으로 충분히 커버됩니다. 비전 분석 품질이 눈에 띄게 향상되어, -스캔 문서 OCR 보완이나 사진 태깅 정확도가 올라갑니다. - -**Plex GPU 서버 이전 효과:** -RTX 4070 Ti Super의 NVENC/NVDEC 하드웨어 인코더를 활용하면 -Mac mini의 소프트웨어 트랜스코딩 대비 전력 효율과 성능이 크게 개선됩니다. -VRAM은 트랜스코딩 시에만 1-2GB 일시 사용하므로 AI 모델과 충돌하지 않습니다. diff --git a/docs/devonagent-setup.md b/docs/devonagent-setup.md deleted file mode 100644 index cec25c5..0000000 --- a/docs/devonagent-setup.md +++ /dev/null @@ -1,103 +0,0 @@ -# DEVONagent Pro — 검색 세트 설정 가이드 - -DEVONagent Pro에서 안전 분야 + 기술 분야 자동 검색 세트를 설정합니다. -주간 합계 50~85건 수준으로 양을 조절합니다. - -## 공통 설정 - -- **Schedule**: 각 세트별 지정 (매일/주간) -- **Action**: Import to DEVONthink → Inbox DB -- **Max Results per Run**: 각 세트별 지정 -- **Language**: 해당 언어 - ---- - -## 검색 세트 1: 국내 산업안전 뉴스 (매일) - -- **키워드**: `산업안전 OR 중대재해 OR 위험성평가 OR 안전사고` -- **사이트**: kosha.or.kr, moel.go.kr, safetynews.co.kr, dailysafety.com -- **Max Results**: 5/일 -- **Schedule**: 매일 08:00 - -## 검색 세트 2: 국내 중대재해 뉴스 (매일) - -- **키워드**: `중대재해 OR 산업재해 OR 작업장사고 -주식 -부동산` -- **사이트**: 뉴스 전체 -- **Max Results**: 3/일 -- **Schedule**: 매일 08:30 - -## 검색 세트 3: KOSHA 가이드/지침 (주간) - -- **키워드**: `site:kosha.or.kr 가이드 OR 지침 OR 기술자료` -- **Max Results**: 5/주 -- **Schedule**: 매주 월요일 09:00 - -## 검색 세트 4: 국내 산업안전 학술/논문 (주간) - -- **키워드**: `산업안전 OR 위험성평가 OR occupational safety site:kci.go.kr OR site:dbpia.co.kr` -- **Max Results**: 3/주 -- **Schedule**: 매주 수요일 09:00 - -## 검색 세트 5: US OSHA / Safety+Health Magazine (주간) - -- **키워드**: `occupational safety OR workplace hazard OR OSHA regulation` -- **사이트**: osha.gov, safetyandhealthmagazine.com, ehstoday.com -- **Max Results**: 5/주 -- **Language**: English -- **Schedule**: 매주 화요일 09:00 - -## 검색 세트 6: JP 厚生労働省 / 安全衛生 (주간) - -- **키워드**: `労働安全 OR 安全衛生 OR 労災` -- **사이트**: mhlw.go.jp, jisha.or.jp -- **Max Results**: 3/주 -- **Language**: Japanese -- **Schedule**: 매주 목요일 09:00 - -## 검색 세트 7: EU-OSHA (월간) - -- **키워드**: `occupational safety health EU regulation` -- **사이트**: osha.europa.eu -- **Max Results**: 5/월 -- **Language**: English -- **Schedule**: 매월 1일 09:00 - -## 검색 세트 8: 기술 뉴스 — AI/서버/네트워크 (매일) - -- **키워드**: `AI model release OR server infrastructure OR homelab OR self-hosted` -- **사이트**: news.ycombinator.com, arstechnica.com, theregister.com -- **Max Results**: 5/일 -- **Schedule**: 매일 12:00 - -## 검색 세트 9: 프로그래밍 기술 동향 (주간) - -- **키워드**: `Python release OR Node.js update OR Docker best practice OR FastAPI` -- **사이트**: dev.to, blog.python.org, nodejs.org -- **Max Results**: 5/주 -- **Schedule**: 매주 금요일 12:00 - ---- - -## 주간 예상 건수 - -| 세트 | 빈도 | 건/주 | -|------|------|-------| -| 1. 국내 안전 뉴스 | 매일 5 | ~35 | -| 2. 중대재해 뉴스 | 매일 3 | ~21 | -| 3. KOSHA 가이드 | 주간 5 | 5 | -| 4. 학술/논문 | 주간 3 | 3 | -| 5. US OSHA | 주간 5 | 5 | -| 6. JP 안전위생 | 주간 3 | 3 | -| 7. EU-OSHA | 월간 5 | ~1 | -| 8. 기술 뉴스 | 매일 5 | ~35 | -| 9. 프로그래밍 | 주간 5 | 5 | -| **합계** | | **~113** | - -> 양이 너무 많으면 세트 1, 2, 8의 Max Results를 3으로 줄이면 주간 ~65건 수준으로 조절 가능. - -## DEVONthink 전송 설정 - -1. DEVONagent → Preferences → DEVONthink -2. Target Database: **Inbox** -3. Auto-Tag: 검색 세트 이름으로 자동 태그 (`devonagent-검색세트명`) -4. DEVONthink Smart Rule이 Inbox에서 자동 분류 처리 diff --git a/docs/industrial-safety-blueprint.md b/docs/industrial-safety-blueprint.md deleted file mode 100644 index b080888..0000000 --- a/docs/industrial-safety-blueprint.md +++ /dev/null @@ -1,684 +0,0 @@ -# 04_Industrial Safety — DEVONthink DB 상세 설계서 - -> 메인 아키텍처: [mac-mini-pkm-architecture.md](computer:///sessions/amazing-vigilant-hypatia/mnt/outputs/mac-mini-pkm-architecture.md) 참조 - ---- - -## 1. DB 그룹 구조 - -``` -04_Industrial Safety/ -├── 00_Inbox ← 2차 분류 대기 -├── 10_Legislation ← 법령, 고시, 행정규칙 -│ ├── Act ← 산업안전보건법 등 법률 원문 -│ ├── Decree ← 시행령, 시행규칙 -│ ├── Notice ← 고시, 지침, 예규, 가이드라인 -│ ├── SAPA ← 중대재해처벌법 (별도 법 체계) -│ ├── KR_Archive ← 개정 이력 자동 수집 (법령 API) -│ └── Foreign ← 해외 법령 (참고용) -│ ├── US ← OSHA Standards, CFR Title 29 -│ ├── JP ← 労働安全衛生法 -│ └── EU ← EU-OSHA Directives, REACH -├── 20_Theory ← 이론서, 교과서, 학습 자료 -├── 30_Papers ← 학술 논문, 연구 보고서 -├── 40_Cases ← 사고 사례, 재해 분석 -│ ├── Domestic ← 국내 사례 -│ └── International ← 해외 사례 -├── 50_Practice ← 실무 문서 (현장 업무) -│ ├── Risk_Assessment ← 위험성평가 -│ ├── Patrol_Inspection ← 순회점검 -│ ├── Safety_Plan ← 안전관리계획서 -│ ├── Education ← 안전교육 자료 -│ ├── Checklist ← 점검표, 체크리스트 -│ ├── Contractor_Management ← 도급/수급업체 안전관리 -│ ├── Permit_to_Work ← 작업허가서 (화기, 밀폐, 고소 등) -│ ├── Emergency_Plan ← 비상조치계획, 대피/소방훈련 -│ └── PPE ← 보호구 관리, 선정 기준, 지급 대장 -├── 60_Compliance ← 신고, 보고, 감독 (실제 행정 문서) -│ ├── Report ← 산재 신고, 중대재해 보고 -│ ├── Audit ← 감독 결과, 시정명령 -│ └── Certification ← 자격증, 인증 관련 -├── 70_Safety_Manager ← 안전관리자 직무 전용 -│ ├── Appointment ← 선임 서류, 자격 관련 -│ ├── Duty_Record ← 직무수행 기록, 월간/분기 보고 -│ ├── Meeting ← 산업안전보건위원회, 회의록 -│ ├── Inspection ← 안전관리자 점검 기록 -│ └── Improvement ← 개선 요청, 시정 조치 이력 -├── 75_Health_Manager ← 보건관리자 직무 전용 -│ ├── Appointment ← 선임 서류, 자격 관련 -│ ├── Duty_Record ← 직무수행 기록, 월간/분기 보고 -│ ├── Work_Environment ← 작업환경측정, 유해인자 관리 -│ ├── Health_Checkup ← 건강검진 관리, 사후관리 -│ ├── MSDS ← 물질안전보건자료 관리 -│ ├── Ergonomics ← 근골격계 유해요인조사, 직업병 예방 -│ └── Mental_Health ← 직무스트레스 평가, 감정노동, 심리상담 -├── 80_Reference ← 규격, 기준, 매뉴얼 -│ ├── Standards ← KS, ISO, KOSHA Guide -│ └── Manual ← 장비 매뉴얼, 작업 지침서 -└── 90_Archive ← 폐기 법령, 구버전 자료 -``` - ---- - -## 2. AI 2차 분류 라우팅 (태그 → 그룹 매핑) - -Inbox에서 1차 분류로 이 DB에 도착한 문서를, AI가 태그와 본문 키워드를 보고 하위 그룹까지 자동 이동시킵니다. - -``` -태그 조합 → 이동 대상 그룹 -────────────────────────────────────────────────────────── -$유형/법령 → 10_Legislation/ -$유형/법령 + #주제/산업안전/법령 → 10_Legislation/ - ├── 텍스트에 "법률" "법" 포함 → Act/ - ├── 텍스트에 "시행령" "시행규칙" 포함 → Decree/ - ├── 텍스트에 "고시" "지침" "예규" 포함 → Notice/ - └── 텍스트에 "중대재해처벌" 포함 → SAPA/ - -$유형/논문 → 30_Papers/ - -#주제/산업안전/사고사례 → 40_Cases/ - ├── sourceURL에 kosha.or.kr 포함 → Domestic/ - └── sourceURL에 osha.gov 등 포함 → International/ - -#주제/산업안전/위험성평가 → 50_Practice/Risk_Assessment/ -#주제/산업안전/순회점검 → 50_Practice/Patrol_Inspection/ -#주제/산업안전/안전교육 → 50_Practice/Education/ -$유형/체크리스트 → 50_Practice/Checklist/ -키워드: "도급" "수급" "협력업체" → 50_Practice/Contractor_Management/ -키워드: "작업허가" "화기" "밀폐" → 50_Practice/Permit_to_Work/ -키워드: "비상" "대피" "소방" → 50_Practice/Emergency_Plan/ -키워드: "보호구" "안전화" "안전모" → 50_Practice/PPE/ - -#주제/산업안전/신고보고 → 60_Compliance/Report/ -키워드: "감독" "시정명령" → 60_Compliance/Audit/ -키워드: "자격증" "인증" "면허" → 60_Compliance/Certification/ - -#주제/산업안전/안전관리자 → 70_Safety_Manager/ - ├── "선임" "자격" → Appointment/ - ├── "직무수행" "월간보고" → Duty_Record/ - ├── "위원회" "회의록" → Meeting/ - ├── "점검" "순회" → Inspection/ - └── "개선" "시정" → Improvement/ - -#주제/산업안전/보건관리자 → 75_Health_Manager/ - ├── "선임" "자격" → Appointment/ - ├── "작업환경측정" "유해인자" → Work_Environment/ - ├── "건강검진" "사후관리" → Health_Checkup/ - ├── "MSDS" "물질안전" → MSDS/ - ├── "근골격계" "직업병" → Ergonomics/ - └── "스트레스" "감정노동" → Mental_Health/ - -#주제/산업안전/규격기준 → 80_Reference/Standards/ - -분류 불가 → 00_Inbox/ (수동 리뷰 대기) -``` - ---- - -## 3. 법령 자동 수집 및 변경 알림 시스템 - -### 3.1 모니터링 대상 법령 - -``` -🇰🇷 한국 (필수) — 국가법령정보센터 Open API (open.law.go.kr) -───────────────────────────────────────── -· 산업안전보건법 (법률/시행령/시행규칙) -· 중대재해 처벌 등에 관한 법률 (법률/시행령) -· 건설기술 진흥법 -· 화학물질관리법 / 화학물질의 등록 및 평가 등에 관한 법률 -· 위험물안전관리법 -· KOSHA Guide (한국산업안전보건공단 기술지침) -· 고용노동부 고시/지침 (관련 행정규칙) - -🇺🇸 미국 (참고) — Federal Register API + OSHA -───────────────────────────────────────── -· OSHA Standards (29 CFR 1910 일반산업, 1926 건설) -· Federal Register: OSHA 관련 규칙 제정/개정 공지 -· NIOSH 권고사항 (새 출판물) - -🇯🇵 일본 (참고) — e-Gov 法令API (laws.e-gov.go.jp) -───────────────────────────────────────── -· 労働安全衛生法 (노동안전위생법) -· 労働安全衛生法施行令 -· 労働安全衛生規則 - -🇪🇺 EU (참고) — EUR-Lex SPARQL / REST -───────────────────────────────────────── -· Framework Directive 89/391/EEC (산업안전 기본지침) -· REACH Regulation (화학물질 규정) -· CLP Regulation (분류/표시 규정) -· Machinery Directive 2006/42/EC -``` - -### 3.2 시스템 아키텍처 - -``` -┌──────────────────────────────────────────────────────────────┐ -│ 법령 모니터링 시스템 (Mac mini, launchd) │ -│ │ -│ ┌─────────────────┐ ┌──────────────────────────────┐ │ -│ │ law_monitor.py │ │ 수집 스케줄 │ │ -│ │ · KR: law.go.kr │ │ · 한국: 매일 06:00 │ │ -│ │ · US: FedReg │ │ · 미국: 주 1회 (월) │ │ -│ │ · JP: e-Gov │ │ · 일본: 주 1회 (수) │ │ -│ │ · EU: EUR-Lex │ │ · EU: 월 1회 (1일) │ │ -│ └───────┬─────────┘ └──────────────────────────────┘ │ -│ ▼ │ -│ ┌──────────────────────────────────────────────────────┐ │ -│ │ 변경 감지: API → SQLite 비교 → diff 생성 │ │ -│ └───────┬──────────────────────────────────────────────┘ │ -│ ▼ │ -│ ┌──────────────────────────────────────────────────────┐ │ -│ │ 알림 + 저장 │ │ -│ │ · DEVONthink: 10_Legislation/ 자동 분류 │ │ -│ │ · Synology Chat 웹훅 즉시 알림 │ │ -│ │ · OmniFocus 작업 생성 ("법령 변경 검토 필요") │ │ -│ │ · Ollama 35B: 변경 요약 + 실무 영향 브리핑 │ │ -│ └──────────────────────────────────────────────────────┘ │ -└──────────────────────────────────────────────────────────────┘ -``` - -### 3.3 법적 근거 — 수집에 문제 없음 - -``` -한국: 저작권법 제7조 — 법령, 조약, 판결 등은 저작권 보호 대상 아님 - + 국가법령정보센터 Open API 공공데이터 자유이용 허용 -미국: 연방법/규정은 Public Domain (17 U.S.C. §105) -일본: 著作権法 第13条 — 법령은 저작권 대상 제외 -EU: EUR-Lex 자유 재사용 정책 (Decision 2011/833/EU) -``` - -### 3.4 저장 구조 예시 - -``` -04_Industrial Safety/10_Legislation/ -├── Act/ -│ └── 산업안전보건법_2026-03-01_시행.pdf -├── Decree/ -│ └── 산업안전보건법_시행령_2026-01-01.pdf -├── SAPA/ -│ └── 중대재해처벌법_2026-01-01_시행.pdf -├── KR_Archive/ -│ ├── 2026-03-24_산업안전보건법_개정_diff.md ← 변경점 요약 -│ └── 2026-03-24_산업안전보건법_개정_원문.pdf -└── Foreign/ - ├── US/ - │ └── 29CFR1910_General_Industry.pdf - ├── JP/ - │ └── 労働安全衛生法_2026.pdf - └── EU/ - └── Directive_89_391_EEC_Framework.pdf -``` - ---- - -## 4. DEVONagent 검색 세트 — 안전 분야 - -### 4.1 전체 구성 - -``` -Mac mini 자동 스케줄 -├── [SS-01] 🇰🇷 한국 산업안전 뉴스 매일 06:00 15~25건/주 -├── [SS-02] 🇰🇷 중대재해·판례 매일 06:15 5~10건/주 -├── [SS-04] 🇺🇸 미국 안전 동향 주 1회 (월) 10~15건/주 -├── [SS-05] 🇯🇵 일본 안전 동향 주 1회 (수) 5~10건/주 -├── [SS-06] 🇪🇺 유럽 안전 동향 월 2회 2~4건/주 -├── [SS-07] 🌐 국제 안전 전문지 주 1회 (금) 5~10건/주 -└── [SS-08] 📚 학술 논문 (안전공학) 주 1회 (토) 5~10건/주 - -안전 분야 주간 합계: ~50~85건 (하루 평균 ~8~12건) -``` - -### 4.2 [SS-01] 한국 산업안전 뉴스 (매일) - -``` -검색 세트: KR_Safety_News -스케줄: 매일 06:00 / 새 결과만 수집 - -소스: -· kosha.or.kr — 공단 공지, 가이드, 재해사례 -· portal.kosha.or.kr — 산재예방 포털 -· moel.go.kr — 고용노동부 보도자료, 정책 -· labor.moel.go.kr — 중대재해 알림e -· safety.or.kr — 대한산업안전협회 -· safetyin.co.kr — 안전저널 -· Google News — "산업안전" OR "산재" OR "안전보건" -채용 -구인 - -→ Inbox → AI 태깅 → 04_Industrial Safety 하위 그룹 자동 분류 -``` - -### 4.3 [SS-02] 중대재해·판례 (매일) - -``` -검색 세트: KR_SAPA_Cases -스케줄: 매일 06:15 / 새 결과만 수집 - -소스: -· labor.moel.go.kr — 중대재해 알림e 공시 -· nosanjae.kr — 중대재해 기업 검색 -· law.go.kr — 판례 검색 (산업안전 관련) -· Google News — "중대재해" OR "중대재해처벌" OR "산재 사망" -채용 - -→ Inbox → AI 태깅 → 40_Cases/Domestic/ 또는 60_Compliance/ -``` - -### 4.4 [SS-04] 🇺🇸 미국 안전 동향 (주 1회) - -``` -검색 세트: US_Safety -스케줄: 월요일 07:00 / 최대 15건 - -소스: -· osha.gov/rss — OSHA 보도자료, 벌금 부과, 규칙 (주 3~5건) -· osha.gov/quicktakes — OSHA QuickTakes 뉴스레터 (격주 1건) -· federalregister.gov — OSHA final rule / proposed (주 1~3건) -· ehstoday.com — EHS Today 산업안전 전문지 (주 3~5건) - -쿼리: ("OSHA" OR "workplace fatality" OR "safety violation") -job -hiring - -→ Inbox → 10_Legislation/Foreign/US/ 또는 40_Cases/International/ -``` - -### 4.5 [SS-05] 🇯🇵 일본 안전 동향 (주 1회) - -``` -검색 세트: JP_Safety -스케줄: 수요일 07:00 / 최대 10건 - -소스: -· mhlw.go.jp/rss — 厚生労働省 보도자료 (주 2~4건) -· anzeninfo.mhlw.go.jp — 職場のあんぜんサイト (재해사례) (주 2~3건) -· jaish.gr.jp — 安全衛生情報センター (통달/지침) (주 1~2건) - -쿼리: ("労働安全" OR "労働災害" OR "安全衛生" OR "重大災害") - -→ Inbox → 10_Legislation/Foreign/JP/ -→ AI 자동 처리: Ollama로 일본어 → 한국어 1줄 요약 생성 -``` - -### 4.6 [SS-06] 🇪🇺 유럽 안전 동향 (월 2회) - -``` -검색 세트: EU_Safety -스케줄: 1일·15일 07:00 / 최대 10건 - -소스: -· osha.europa.eu RSS — EU-OSHA 발간물, 뉴스, 지침 (월 3~5건) -· eur-lex.europa.eu — 산업안전 관련 신규 지침/규정 (월 1~3건) -· hse.gov.uk — UK Health & Safety Executive (월 2~3건) - -쿼리: ("EU-OSHA" OR "workplace safety directive" OR "REACH" OR "safety at work") - -vacancy -recruitment - -→ Inbox → 10_Legislation/Foreign/EU/ -``` - -### 4.7 [SS-07] 🌐 국제 안전 전문지 (주 1회) - -``` -검색 세트: Global_Safety_Magazines -스케줄: 금요일 07:00 / 최대 10건 - -소스: -· ishn.com/rss — Industrial Safety & Hygiene News (주 3~5건) -· ohsonline.com — Occupational Health & Safety (주 2~3건) -· safetyandhealthmagazine.com — NSC Safety+Health Magazine (주 1~2건) - -쿼리: ("industrial safety" OR "process safety" OR "workplace accident" - OR "safety management" OR "risk assessment") - -→ Inbox → AI 태깅 후 주제별 자동 분류 -``` - -### 4.8 [SS-08] 학술 논문 — 안전공학 (주 1회) - -``` -검색 세트: Safety_Academic -스케줄: 토요일 08:00 / 최대 10건 - -소스: -· Google Scholar — 한국어: "산업안전" "위험성평가" "안전공학" -· Google Scholar — 영어: "occupational safety" "risk assessment" -· oshri.kosha.or.kr — 산업안전보건연구원 발간물 -· dbpia.co.kr — 한국 학술논문 -· sciencedirect.com — Safety Science 저널 - -→ Inbox → 30_Papers/ -``` - ---- - -## 5. 양 조절 전략 - -### 5.1 주간 예상 유입량 - -``` -검색 세트 주간 예상 빈도 -────────────────────────────────────────── -SS-01 한국 안전뉴스 15~25건 매일 -SS-02 중대재해/판례 5~10건 매일 -SS-04 🇺🇸 미국 10~15건 주 1회 -SS-05 🇯🇵 일본 5~10건 주 1회 -SS-06 🇪🇺 유럽 2~4건 월 2회 -SS-07 🌐 전문지 5~10건 주 1회 -SS-08 학술 논문 5~10건 주 1회 -────────────────────────────────────────── -안전 분야 합계 ~50~85건 /주 -하루 평균 ~8~12건 -``` - -### 5.2 과다 유입 방지 장치 - -``` -1단계: DEVONagent "새 결과만" — 이전 수집분 자동 제외 -2단계: 검색 세트별 최대 수집량 캡 (Max Results) -3단계: AI 관련도 필터 — Ollama가 관련도 판단 - → 낮으면 @상태/아카이브 → 90_Archive 이동 - → 높으면 @상태/검토필요 → 해당 그룹에 유지 -4단계: 주간 다이제스트 — 금요일 Claude API가 주간 요약 - → "이번 주 꼭 봐야 할 5건" 브리핑 자동 생성 -5단계: 30일 이상 미열람 → Smart Rule로 자동 90_Archive 이동 -``` - -### 5.3 일본어 자료 자동 처리 - -``` -수집 → Smart Rule: 일본 태그 감지 -→ Ollama 35B: 일본어 → 한국어 1줄 요약 -→ DEVONthink 커스텀 메타데이터 "summaryKR" 필드에 저장 -→ 원문은 그대로 보존 - -※ 일본 산업안전 용어는 한자어 공통으로 번역 정확도 높음 -``` - ---- - -## 6. 기존 자료 마이그레이션 - -``` -현재 → 이동 대상 -─────────────────────────────────────────────── -0_Theory/ (72건) → 20_Theory/ -8_Reference/ (1건) → 80_Reference/ -9_일반자료_산업안전/ (33건) → 내용별 분산: - 사고사례 → 40_Cases/Domestic/ - 실무서식 → 50_Practice/ - 신고관련 → 60_Compliance/ -지게차 관련규칙 개정... (PDF) → 10_Legislation/Notice/ -Industrial Safety... (HTML) → 20_Theory/ 또는 80_Reference/ -``` - ---- - -## 7. 관련 태그 체계 (산업안전 영역) - -``` -#주제/산업안전/ -├── 법령 ← 10_Legislation -├── 위험성평가 ← 50_Practice/Risk_Assessment -├── 순회점검 ← 50_Practice/Patrol_Inspection -├── 안전교육 ← 50_Practice/Education -├── 사고사례 ← 40_Cases -├── 신고보고 ← 60_Compliance -├── 안전관리자 ← 70_Safety_Manager -├── 보건관리자 ← 75_Health_Manager -└── 규격기준 ← 80_Reference -``` - ---- - -## 8. 유입 경로 추적 체계 (Source Tracking) - -모든 문서에 유입 경로를 기록하여 "이 자료가 어디서 왔는지"를 즉시 파악할 수 있게 합니다. -실제 업무 데이터와 외부 참고자료를 명확히 구분하는 것이 핵심입니다. - -### 8.1 유입 경로 분류 - -``` -커스텀 메타데이터: sourceChannel (텍스트, 필수) - -┌──────────────────────────────────────────────────────────────────┐ -│ 자동 유입 (시스템) │ -├────────────────┬─────────────────────────────────────────────────┤ -│ tksafety │ TKSafety API 연동 — 업무 실적 (위험성평가, 점검 등) │ -│ devonagent │ DEVONagent 검색 세트 — 뉴스/업계 동향 자동 수집 │ -│ law_monitor │ 법령 모니터링 API — 법령 제·개정 추적 │ -├──────────────────────────────────────────────────────────────────┤ -│ 수동/반자동 유입 │ -├────────────────┬─────────────────────────────────────────────────┤ -│ inbox_route │ Inbox DB → AI 분류 → 이 DB로 라우팅된 문서 │ -│ email │ MailPlus → Archive DB → 안전 관련 메일 전달 │ -│ web_clip │ DEVONthink Web Clipper로 직접 스크랩 │ -│ manual │ 드래그&드롭, 스캔, 파일 직접 추가 │ -└────────────────┴─────────────────────────────────────────────────┘ -``` - -### 8.2 메타데이터 자동 설정 규칙 - -``` -유입 경로별 자동 태그: - -tksafety → @출처/TKSafety + sourceURL = tksafety.technicalkorea.net/... -devonagent → @출처/뉴스수집 + sourceURL = 원본 기사 URL -law_monitor → @출처/법령API + sourceURL = law.go.kr/... 또는 해외 법령 URL -inbox_route → @출처/자동분류 + (원본 sourceURL 유지) -email → @출처/이메일 + sourceURL = mailplus 메시지 링크 -web_clip → @출처/웹스크랩 + sourceURL = 스크랩 원본 URL -manual → @출처/수동입력 + sourceURL = 없음 (직접 기입 가능) -``` - -### 8.3 업무 데이터 vs 참고자료 구분 - -``` -커스텀 메타데이터: dataOrigin (드롭다운, 필수) - - work — 우리 회사 실제 업무에서 발생한 데이터 - (TKSafety 연동, 직접 작성한 보고서, 내부 회의록 등) - external — 외부에서 수집한 참고/학습 자료 - (뉴스, 법령 원문, 타사 사례, 학술 논문 등) - -자동 판별 규칙: -· sourceChannel = tksafety → dataOrigin = work (항상) -· sourceChannel = law_monitor → dataOrigin = external (항상) -· sourceChannel = devonagent → dataOrigin = external (항상) -· sourceChannel = manual → dataOrigin = work (기본값, 수동 변경 가능) -· sourceChannel = inbox_route → AI가 내용 기반으로 판별 -· sourceChannel = email → AI가 발신자/내용 기반으로 판별 -· sourceChannel = web_clip → dataOrigin = external (기본값) -``` - -### 8.4 Smart Rule 적용 - -``` -DEVONthink Smart Rule: "소스 채널 누락 검출" - -조건: custom metadata "sourceChannel" is empty - AND database is "04_Industrial Safety" - AND NOT in group "00_Inbox" -동작: - 1. @상태/미분류출처 태그 추가 - 2. 00_Inbox으로 이동 (출처 확인 후 재분류) - -→ 어떤 경로로든 출처 없이 들어온 문서는 자동 포착 -→ 주간 리뷰에서 정리 (수동 입력 자료 대부분 여기 해당) -``` - -### 8.5 활용 시나리오 - -``` -검색/필터 예시: - -"올해 우리 회사가 실시한 위험성평가만 보기" - → 50_Practice/Risk_Assessment/ + dataOrigin = work - -"외부 위험성평가 사례/참고자료" - → 50_Practice/Risk_Assessment/ + dataOrigin = external - -"TKSafety에서 자동 수집된 문서 전체" - → sourceChannel = tksafety - -"직접 스크랩한 자료 중 미정리 건" - → sourceChannel = web_clip + @상태/미분류출처 - -Smart Group으로 상시 모니터링: -· "출처 미기입 문서" → sourceChannel is empty -· "이번 주 업무 문서" → dataOrigin = work + 최근 7일 -· "외부 수집 미읽음" → dataOrigin = external + unread -``` - ---- - -## 9. TKSafety 시스템 연동 (설정 대기) - -> **현재 상태: 설계 완료, 구현 대기** -> API 엔드포인트 명세와 연동 구조만 확정해두고, 실제 활성화는 PKM 기본 체계가 안정된 후 진행합니다. -> TKSafety는 자체 개발 시스템이므로 필요 시점에 API를 추가하면 됩니다. - -### 9.1 시스템 정보 - -``` -· URL: tksafety.technicalkorea.net (Cloudflare Tunnel) -· 호스팅: Synology DS1525+ Docker -· 내부 접근: Tailscale VPN -· 개발/수정: 직접 가능 -· sourceChannel 값: tksafety -· dataOrigin 값: work (항상) -``` - -### 9.2 연동 아키텍처 (예정) - -``` -┌──────────────────────┐ ┌────────────────────────────┐ -│ TKSafety │ │ Mac mini (PKM 허브) │ -│ (Synology Docker) │ │ │ -│ │ API │ tksafety_sync.py │ -│ /api/v1/ │◄──────►│ (launchd 스케줄) │ -│ risk-assessments │ Tailscale│ │ -│ patrol-inspections │ │ ┌─────────────────────┐ │ -│ corrective-actions │ │ │ 데이터 가공 │ │ -│ incidents │ │ │ · JSON → PDF/MD 변환 │ │ -│ education-records │ │ │ · sourceChannel 설정 │ │ -│ meeting-minutes │ │ │ · dataOrigin = work │ │ -│ │ │ └──────────┬──────────┘ │ -└──────────────────────┘ │ ▼ │ - │ DEVONthink 자동 임포트 │ - │ → 04_Industrial Safety/ │ - │ 하위 그룹 자동 라우팅 │ - │ │ - │ ChromaDB 벡터 인덱싱 │ - │ → RAG 검색 가능 │ - └────────────────────────────┘ -``` - -### 9.3 API 엔드포인트 명세 (TKSafety에 추가 예정) - -``` -GET /api/v1/risk-assessments - ?since=2026-03-01&status=completed → 위험성평가 결과 목록 -GET /api/v1/risk-assessments/{id}/report → 상세 (PDF/JSON) - -GET /api/v1/patrol-inspections - ?since=2026-03-01 → 순회점검 결과 목록 -GET /api/v1/patrol-inspections/{id}/report → 상세 + 사진 - -GET /api/v1/corrective-actions - ?since=2026-03-01&status=open|completed|overdue → 시정조치 내역 - -GET /api/v1/incidents?since=2026-03-01 → 사고/아차사고 보고서 - -GET /api/v1/education-records?since=2026-03-01 → 안전교육 기록 - -GET /api/v1/meetings?type=safety-committee&since=2026-03-01 → 회의록 - -GET /api/v1/sync-status → 마지막 동기화 시점, 대기 건수 -``` - -### 9.4 라우팅 매핑 (활성화 시 적용) - -``` -TKSafety 데이터 → DEVONthink 그룹 → 파일 형식 -──────────────────────────────────────────────────────────────── -risk-assessments → 50_Practice/Risk_Assessment/ → PDF -patrol-inspections → 50_Practice/Patrol_Inspection/ → MD + 사진 -corrective-actions → 70_Safety_Manager/Improvement/ → MD -incidents → 40_Cases/Domestic/ → PDF -education-records → 50_Practice/Education/ → MD -meetings (safety-comm) → 70_Safety_Manager/Meeting/ → MD - -파일명 규칙: - RA_2026-03-24_[작업명]_[위험등급].pdf - PI_2026-03-24_[구역명].md - CA_2026-03-24_[조치내용]_[상태].md - INC_2026-03-24_[사고유형]_[심각도].pdf -``` - -### 9.5 동기화 스케줄 (활성화 시 적용) - -``` -· 위험성평가, 순회점검 → 매일 07:00 -· 시정조치 → 매일 07:00 + 18:00 -· 사고/아차사고 → 1시간마다 (긴급성) -· 교육기록, 회의록 → 주 1회 (월요일 07:00) -· overdue 시정조치 → OmniFocus 작업 자동 생성 -``` - -### 9.6 활성화 단계 - -``` -지금 할 것: - ✓ API 명세 확정 (이 문서) - ✓ sourceChannel/dataOrigin 체계 설계 - ○ TKSafety에 /api/v1/ 엔드포인트 뼈대만 추가 (빈 응답 OK) - -PKM 안정화 후: - Phase 1: API 실제 데이터 응답 구현 - Phase 2: tksafety_sync.py 개발 + DEVONthink 임포트 - Phase 3: 시정조치 → OmniFocus 연동 - Phase 4: 양방향 확장 (DEVONthink → TKSafety 상태 업데이트) -``` - ---- - -## 10. 산업안전 Daily Digest 기여 항목 - -Daily Digest는 전체 PKM 차원에서 운영되지만 (메인 아키텍처 참조), -이 DB는 특히 다음 항목을 다이제스트에 공급합니다. - -``` -04_Industrial Safety → Daily Digest 공급 항목: - -■ 문서 변동 - · 오늘 추가된 문서 수 (sourceChannel별 구분) - 예: "산업안전 +5 (뉴스3, 법령1, 업무1)" - · 분류 실패 → 00_Inbox 잔류 건수 - -■ 법령 변경 (law_monitor 연동) - · 한국 법령 제·개정 감지 → ⚠ 마크로 강조 - · 해외 법령 변경 → 참고 표시 - · OmniFocus 액션: "법령 변경 검토: [법령명]" 자동 생성 - -■ 뉴스/동향 (DEVONagent 연동) - · 오늘 수집된 안전 뉴스 건수 (국내/해외 구분) - · 상위 3건 자동 요약 (Ollama 35B) - -■ 업무 데이터 (TKSafety 연동, 활성화 시) - · 위험성평가/순회점검 신규 건수 - · 시정조치 overdue → ⚠ OmniFocus 긴급 액션 - -■ OmniFocus 액션 자동 생성 조건 (이 DB 관련): - · 법령 변경 감지 → "법령 변경 검토: [법령명]" - · 시정조치 기한초과 → "시정조치 기한초과: [내용]" (긴급) - · 안전 뉴스 중대 키워드 → "뉴스 확인: [제목]" - (키워드: 중대재해, 사망, 작업중지, 과태료) - · Inbox 미처리 5건 이상 → "산업안전 Inbox 정리 필요" -``` - ---- - -## 11. 향후 확장 계획 - -- 나머지 도메인 DB(03_Engineering, 05_Programming 등)도 동일한 넘버링 패턴으로 그룹 구조 설계 예정 -- 각 DB별 DEVONagent 검색 세트 추가 -- DB 간 크로스 레퍼런스 (예: 산업안전 + 공학 문서 연결) -- TKSafety 양방향 연동 확장 (Section 9.6 참조) -- sourceChannel/dataOrigin 체계를 다른 도메인 DB에도 확장 적용 diff --git a/launchd/net.hyungi.pkm.daily-digest.plist b/launchd/net.hyungi.pkm.daily-digest.plist deleted file mode 100644 index 94e7e9a..0000000 --- a/launchd/net.hyungi.pkm.daily-digest.plist +++ /dev/null @@ -1,28 +0,0 @@ - - - - - Label - net.hyungi.pkm.daily-digest - ProgramArguments - - /Users/hyungi/Documents/code/DEVONThink_my server/venv/bin/python3 - /Users/hyungi/Documents/code/DEVONThink_my server/scripts/pkm_daily_digest.py - - WorkingDirectory - /Users/hyungi/Documents/code/DEVONThink_my server - StartCalendarInterval - - Hour - 20 - Minute - 0 - - StandardOutPath - /Users/hyungi/Documents/code/DEVONThink_my server/logs/digest_launchd.log - StandardErrorPath - /Users/hyungi/Documents/code/DEVONThink_my server/logs/digest_launchd_err.log - RunAtLoad - - - diff --git a/launchd/net.hyungi.pkm.law-monitor.plist b/launchd/net.hyungi.pkm.law-monitor.plist deleted file mode 100644 index ed877fc..0000000 --- a/launchd/net.hyungi.pkm.law-monitor.plist +++ /dev/null @@ -1,28 +0,0 @@ - - - - - Label - net.hyungi.pkm.law-monitor - ProgramArguments - - /Users/hyungi/Documents/code/DEVONThink_my server/venv/bin/python3 - /Users/hyungi/Documents/code/DEVONThink_my server/scripts/law_monitor.py - - WorkingDirectory - /Users/hyungi/Documents/code/DEVONThink_my server - StartCalendarInterval - - Hour - 7 - Minute - 0 - - StandardOutPath - /Users/hyungi/Documents/code/DEVONThink_my server/logs/law_monitor_launchd.log - StandardErrorPath - /Users/hyungi/Documents/code/DEVONThink_my server/logs/law_monitor_launchd_err.log - RunAtLoad - - - diff --git a/launchd/net.hyungi.pkm.mailplus.plist b/launchd/net.hyungi.pkm.mailplus.plist deleted file mode 100644 index 9f30ae6..0000000 --- a/launchd/net.hyungi.pkm.mailplus.plist +++ /dev/null @@ -1,36 +0,0 @@ - - - - - Label - net.hyungi.pkm.mailplus - ProgramArguments - - /Users/hyungi/Documents/code/DEVONThink_my server/venv/bin/python3 - /Users/hyungi/Documents/code/DEVONThink_my server/scripts/mailplus_archive.py - - WorkingDirectory - /Users/hyungi/Documents/code/DEVONThink_my server - StartCalendarInterval - - - Hour - 7 - Minute - 0 - - - Hour - 18 - Minute - 0 - - - StandardOutPath - /Users/hyungi/Documents/code/DEVONThink_my server/logs/mailplus_launchd.log - StandardErrorPath - /Users/hyungi/Documents/code/DEVONThink_my server/logs/mailplus_launchd_err.log - RunAtLoad - - - diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index de77c32..0000000 --- a/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -chromadb>=0.4.0 -requests>=2.31.0 -python-dotenv>=1.0.0 -schedule>=1.2.0 -markdown>=3.5.0 -anthropic>=0.40.0 diff --git a/scripts/embed_to_chroma.py b/scripts/embed_to_chroma.py deleted file mode 100644 index 503060f..0000000 --- a/scripts/embed_to_chroma.py +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env python3 -""" -벡터 임베딩 스크립트 -- DEVONthink 문서 UUID로 텍스트 추출 -- GPU 서버(nomic-embed-text)로 임베딩 생성 -- ChromaDB에 저장 -""" - -import os -import sys -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") - -# ChromaDB 저장 경로 -CHROMA_DIR = Path.home() / ".local" / "share" / "pkm" / "chromadb" -CHROMA_DIR.mkdir(parents=True, exist_ok=True) - - -def get_document_text(uuid: str) -> tuple[str, str]: - """DEVONthink에서 UUID로 문서 텍스트 + 제목 추출""" - script = f''' - tell application id "DNtp" - set theRecord to get record with uuid "{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 서버의 nomic-embed-text로 임베딩 생성""" - url = f"http://{gpu_server_ip}:11434/api/embeddings" - try: - resp = requests.post(url, json={ - "model": "nomic-embed-text", - "prompt": text[:8000] # 토큰 제한 - }, timeout=60) - resp.raise_for_status() - return resp.json().get("embedding") - except Exception as e: - logger.error(f"임베딩 생성 실패: {e}") - return None - - -def store_in_chromadb(doc_id: str, title: str, text: str, embedding: list[float]): - """ChromaDB에 저장""" - import chromadb - client = chromadb.PersistentClient(path=str(CHROMA_DIR)) - collection = client.get_or_create_collection( - name="pkm_documents", - metadata={"hnsw:space": "cosine"} - ) - collection.upsert( - ids=[doc_id], - embeddings=[embedding], - documents=[text[:2000]], - metadatas=[{"title": title, "source": "devonthink"}] - ) - logger.info(f"ChromaDB 저장: {doc_id} ({title[:30]})") - - -def run(uuid: str): - """단일 문서 임베딩 처리""" - logger.info(f"임베딩 처리 시작: {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(uuid) - if not text or len(text) < 10: - logger.warning(f"텍스트 부족 [{uuid}]: {len(text)}자") - return - - embedding = get_embedding(text, gpu_ip) - if embedding: - store_in_chromadb(uuid, title, text, embedding) - logger.info(f"임베딩 완료: {uuid}") - else: - logger.error(f"임베딩 실패: {uuid}") - - except Exception as e: - logger.error(f"임베딩 처리 에러 [{uuid}]: {e}", exc_info=True) - - -if __name__ == "__main__": - if len(sys.argv) < 2: - print("사용법: python3 embed_to_chroma.py ") - sys.exit(1) - run(sys.argv[1]) diff --git a/scripts/law_monitor.py b/scripts/law_monitor.py deleted file mode 100644 index 39e5d74..0000000 --- a/scripts/law_monitor.py +++ /dev/null @@ -1,400 +0,0 @@ -#!/usr/bin/env python3 -""" -법령 모니터링 스크립트 -- 국가법령정보센터 OpenAPI (open.law.go.kr) 폴링 -- 산업안전보건법, 중대재해처벌법 등 변경 추적 -- 변경 감지 시 DEVONthink 04_Industrial Safety 자동 임포트 -※ API 승인 대기중 — 스크립트만 작성, 실제 호출은 승인 후 -""" - -import os -import sys -import json -import requests -import xml.etree.ElementTree as ET -from datetime import datetime, timedelta -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).parent)) -from pkm_utils import setup_logger, load_credentials, run_applescript_inline, llm_generate, PROJECT_ROOT, DATA_DIR - -logger = setup_logger("law_monitor") - -# 모니터링 대상 법령 -MONITORED_LAWS = [ - {"name": "산업안전보건법", "law_id": "001789", "category": "법률"}, - {"name": "산업안전보건법 시행령", "law_id": "001790", "category": "대통령령"}, - {"name": "산업안전보건법 시행규칙", "law_id": "001791", "category": "부령"}, - {"name": "중대재해 처벌 등에 관한 법률", "law_id": "019005", "category": "법률"}, - {"name": "중대재해 처벌 등에 관한 법률 시행령", "law_id": "019006", "category": "대통령령"}, - {"name": "화학물질관리법", "law_id": "012354", "category": "법률"}, - {"name": "위험물안전관리법", "law_id": "001478", "category": "법률"}, -] - -# 마지막 확인 일자 저장 파일 -LAST_CHECK_FILE = DATA_DIR / "law_last_check.json" -LAWS_DIR = DATA_DIR / "laws" -LAWS_DIR.mkdir(exist_ok=True) - - -def load_last_check() -> dict: - """마지막 확인 일자 로딩""" - if LAST_CHECK_FILE.exists(): - with open(LAST_CHECK_FILE, "r") as f: - return json.load(f) - return {} - - -def save_last_check(data: dict): - """마지막 확인 일자 저장""" - with open(LAST_CHECK_FILE, "w") as f: - json.dump(data, f, ensure_ascii=False, indent=2) - - -def fetch_law_info(law_oc: str, law_id: str) -> dict | None: - """법령 정보 조회 (법령 API)""" - url = "https://www.law.go.kr/DRF/lawSearch.do" - params = { - "OC": law_oc, - "target": "law", - "type": "JSON", - "MST": law_id, - } - try: - resp = requests.get(url, params=params, timeout=30) - resp.raise_for_status() - data = resp.json() - # API 에러 응답 감지 - if "result" in data and "실패" in str(data.get("result", "")): - logger.error(f"법령 API 에러 [{law_id}]: {data.get('result')} — {data.get('msg')}") - return None - if "LawSearch" in data and "law" in data["LawSearch"]: - laws = data["LawSearch"]["law"] - if isinstance(laws, list): - return laws[0] if laws else None - return laws - logger.warning(f"법령 응답에 데이터 없음 [{law_id}]: {list(data.keys())}") - return None - except Exception as e: - logger.error(f"법령 조회 실패 [{law_id}]: {e}") - return None - - -def fetch_law_text(law_oc: str, law_mst: str) -> str | None: - """법령 본문 XML 다운로드""" - url = "https://www.law.go.kr/DRF/lawService.do" - params = { - "OC": law_oc, - "target": "law", - "type": "XML", - "MST": law_mst, - } - try: - resp = requests.get(url, params=params, timeout=60) - resp.raise_for_status() - return resp.text - except Exception as e: - logger.error(f"법령 본문 다운로드 실패 [{law_mst}]: {e}") - return None - - -def save_law_file(law_name: str, content: str) -> Path: - """법령 XML 저장""" - today = datetime.now().strftime("%Y%m%d") - safe_name = law_name.replace(" ", "_").replace("/", "_") - filepath = LAWS_DIR / f"{safe_name}_{today}.xml" - with open(filepath, "w", encoding="utf-8") as f: - f.write(content) - logger.info(f"법령 저장: {filepath}") - return filepath - - -def import_to_devonthink(filepath: Path, law_name: str, category: str): - """DEVONthink 04_Industrial Safety로 임포트 — 변수 방식""" - fp = str(filepath) - script = f'set fp to "{fp}"\n' - script += 'tell application id "DNtp"\n' - script += ' repeat with db in databases\n' - script += ' if name of db is "04_Industrial safety" then\n' - script += ' set targetGroup to create location "/10_Legislation/Law" in db\n' - script += ' set theRecord to import fp to targetGroup\n' - script += f' set tags of theRecord to {{"#주제/산업안전/법령", "$유형/법령", "{category}"}}\n' - script += ' add custom meta data "law_monitor" for "sourceChannel" to theRecord\n' - script += ' add custom meta data "external" for "dataOrigin" to theRecord\n' - script += ' add custom meta data (current date) for "lastAIProcess" to theRecord\n' - script += ' exit repeat\n' - script += ' end if\n' - script += ' end repeat\n' - script += 'end tell' - try: - run_applescript_inline(script) - logger.info(f"DEVONthink 임포트 완료: {law_name}") - except Exception as e: - logger.error(f"DEVONthink 임포트 실패 [{law_name}]: {e}") - - -def run(): - """메인 실행""" - logger.info("=== 법령 모니터링 시작 ===") - - creds = load_credentials() - law_oc = creds.get("LAW_OC") - if not law_oc: - logger.error("LAW_OC 인증키가 설정되지 않았습니다. credentials.env를 확인하세요.") - sys.exit(1) - - last_check = load_last_check() - changes_found = 0 - - for law in MONITORED_LAWS: - law_name = law["name"] - law_id = law["law_id"] - category = law["category"] - - logger.info(f"확인 중: {law_name} ({law_id})") - - info = fetch_law_info(law_oc, law_id) - if not info: - continue - - # 시행일자 또는 공포일자로 변경 감지 - announce_date = info.get("공포일자", info.get("시행일자", "")) - prev_date = last_check.get(law_id, "") - - if announce_date and announce_date != prev_date: - logger.info(f"변경 감지: {law_name} — 공포일자 {announce_date} (이전: {prev_date or '없음'})") - - # 법령 본문 다운로드 - law_mst = info.get("법령MST", law_id) - text = fetch_law_text(law_oc, law_mst) - if text: - filepath = save_law_file(law_name, text) - import_to_devonthink(filepath, law_name, category) - changes_found += 1 - - last_check[law_id] = announce_date - else: - logger.debug(f"변경 없음: {law_name}") - - save_last_check(last_check) - - # ─── 외국 법령 (빈도 체크 후 실행) ─── - us_count = fetch_us_osha(last_check) - jp_count = fetch_jp_mhlw(last_check) - eu_count = fetch_eu_osha(last_check) - changes_found += us_count + jp_count + eu_count - - save_last_check(last_check) - logger.info(f"=== 법령 모니터링 완료 — {changes_found}건 변경 감지 (한국+외국) ===") - - -# ═══════════════════════════════════════════════ -# 외국 법령 모니터링 -# ═══════════════════════════════════════════════ - -def _should_run(last_check: dict, key: str, interval_days: int) -> bool: - """빈도 체크: 마지막 실행일로부터 interval_days 경과 여부""" - last_run = last_check.get(key, "") - if not last_run: - return True - try: - last_date = datetime.strptime(last_run, "%Y-%m-%d") - return (datetime.now() - last_date).days >= interval_days - except ValueError: - return True - - -def _import_foreign_to_devonthink(filepath: Path, title: str, country: str): - """외국 법령 DEVONthink 임포트 — 변수 방식 (POSIX path 따옴표 문제 회피)""" - folder = {"US": "US", "JP": "JP", "EU": "EU"}.get(country, country) - fp = str(filepath) - script = f'set fp to "{fp}"\n' - script += 'tell application id "DNtp"\n' - script += ' repeat with db in databases\n' - script += ' if name of db is "04_Industrial safety" then\n' - script += f' set targetGroup to create location "/10_Legislation/Foreign/{folder}" in db\n' - script += ' set theRecord to import fp to targetGroup\n' - script += f' set tags of theRecord to {{"#주제/산업안전/법령", "$유형/법령", "{country}"}}\n' - script += ' add custom meta data "law_monitor" for "sourceChannel" to theRecord\n' - script += ' add custom meta data "external" for "dataOrigin" to theRecord\n' - script += ' add custom meta data (current date) for "lastAIProcess" to theRecord\n' - script += ' exit repeat\n' - script += ' end if\n' - script += ' end repeat\n' - script += 'end tell' - try: - run_applescript_inline(script) - safe_title = title[:40].replace('\n', ' ') - logger.info(f"DEVONthink 임포트 [{country}]: {safe_title}") - except Exception as e: - logger.error(f"DEVONthink 임포트 실패 [{country}]: {e}") - - -def fetch_us_osha(last_check: dict) -> int: - """US OSHA — Federal Register API (주 1회)""" - if not _should_run(last_check, "_us_osha_last", 7): - logger.debug("US OSHA: 이번 주 이미 실행됨, 건너뜀") - return 0 - - logger.info("=== US OSHA 확인 ===") - try: - from_date = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d") - resp = requests.get("https://www.federalregister.gov/api/v1/documents.json", params={ - "conditions[agencies][]": "occupational-safety-and-health-administration", - "conditions[publication_date][gte]": from_date, - "per_page": 10, - "order": "newest", - }, timeout=30) - resp.raise_for_status() - data = resp.json() - results = data.get("results", []) - count = 0 - - for doc in results: - doc_id = doc.get("document_number", "") - title = doc.get("title", "") - pub_date = doc.get("publication_date", "") - abstract = doc.get("abstract", "") - doc_url = doc.get("html_url", "") - - # 마크다운으로 저장 - content = f"# {title}\n\n" - content += f"- **Document**: {doc_id}\n" - content += f"- **Date**: {pub_date}\n" - content += f"- **URL**: {doc_url}\n\n" - if abstract: - content += f"## Abstract\n\n{abstract}\n" - - safe_title = "".join(c if c.isalnum() or c in " _-" else "_" for c in title)[:50] - filepath = LAWS_DIR / f"US_OSHA_{pub_date}_{safe_title}.md" - with open(filepath, "w", encoding="utf-8") as f: - f.write(content) - - _import_foreign_to_devonthink(filepath, title, "US") - count += 1 - - last_check["_us_osha_last"] = datetime.now().strftime("%Y-%m-%d") - logger.info(f"US OSHA: {count}건") - return count - - except Exception as e: - logger.error(f"US OSHA 에러: {e}", exc_info=True) - return 0 - - -def fetch_jp_mhlw(last_check: dict) -> int: - """JP 厚生労働省 — RSS 파싱 + MLX 번역 (주 1회)""" - if not _should_run(last_check, "_jp_mhlw_last", 7): - logger.debug("JP 厚労省: 이번 주 이미 실행됨, 건너뜀") - return 0 - - logger.info("=== JP 厚生労働省 확인 ===") - try: - import xml.etree.ElementTree as ET - resp = requests.get("https://www.mhlw.go.jp/stf/news.rdf", timeout=30) - resp.raise_for_status() - root = ET.fromstring(resp.content) - - safety_keywords = ["労働安全", "安全衛生", "労災", "化学物質", "石綿", "安全管理", "労働", "安全", "衛生"] - rss_ns = "http://purl.org/rss/1.0/" - count = 0 - - # RDF 1.0 형식: {http://purl.org/rss/1.0/}item - items = root.findall(f"{{{rss_ns}}}item") - logger.info(f"JP RSS 항목: {len(items)}건") - for item in items: - title = item.findtext(f"{{{rss_ns}}}title", "") - link = item.findtext(f"{{{rss_ns}}}link", "") - pub_date = item.findtext("pubDate", "") - - # 안전위생 키워드 필터 - if not any(kw in title for kw in safety_keywords): - continue - - # MLX 35B로 한국어 번역 - translated = "" - try: - translated = llm_generate( - f"다음 일본어 제목을 한국어로 번역해줘. 번역만 출력하고 다른 말은 하지 마.\n\n{title}" - ) - # thinking 출력 제거 — 마지막 줄만 사용 - lines = [l.strip() for l in translated.strip().split("\n") if l.strip()] - translated = lines[-1] if lines else title - except Exception: - translated = title - - content = f"# {title}\n\n" - content += f"**한국어**: {translated}\n\n" - content += f"- **URL**: {link}\n" - content += f"- **Date**: {pub_date}\n" - - safe_title = "".join(c if c.isalnum() or c in " _-" else "_" for c in title)[:40] - today = datetime.now().strftime("%Y%m%d") - filepath = LAWS_DIR / f"JP_{today}_{safe_title}.md" - with open(filepath, "w", encoding="utf-8") as f: - f.write(content) - - _import_foreign_to_devonthink(filepath, f"{translated} ({title})", "JP") - count += 1 - - if count >= 10: - break - - last_check["_jp_mhlw_last"] = datetime.now().strftime("%Y-%m-%d") - logger.info(f"JP 厚労省: {count}건") - return count - - except Exception as e: - logger.error(f"JP 厚労省 에러: {e}", exc_info=True) - return 0 - - -def fetch_eu_osha(last_check: dict) -> int: - """EU-OSHA — RSS 파싱 (월 1회)""" - if not _should_run(last_check, "_eu_osha_last", 30): - logger.debug("EU-OSHA: 이번 달 이미 실행됨, 건너뜀") - return 0 - - logger.info("=== EU-OSHA 확인 ===") - try: - import xml.etree.ElementTree as ET - resp = requests.get("https://osha.europa.eu/en/rss.xml", timeout=30) - resp.raise_for_status() - root = ET.fromstring(resp.content) - - count = 0 - for item in root.iter("item"): - title = item.findtext("title", "") - link = item.findtext("link", "") - description = item.findtext("description", "") - pub_date = item.findtext("pubDate", "") - - content = f"# {title}\n\n" - content += f"- **URL**: {link}\n" - content += f"- **Date**: {pub_date}\n\n" - if description: - content += f"## Summary\n\n{description}\n" - - safe_title = "".join(c if c.isalnum() or c in " _-" else "" for c in title)[:50].strip() or f"item{count+1}" - today = datetime.now().strftime("%Y%m%d") - filepath = LAWS_DIR / f"EU_{today}_{count+1:02d}_{safe_title}.md" - with open(filepath, "w", encoding="utf-8") as f: - f.write(content) - - _import_foreign_to_devonthink(filepath, title, "EU") - count += 1 - - if count >= 5: - break - - last_check["_eu_osha_last"] = datetime.now().strftime("%Y-%m-%d") - logger.info(f"EU-OSHA: {count}건") - return count - - except Exception as e: - logger.error(f"EU-OSHA 에러: {e}", exc_info=True) - return 0 - - -if __name__ == "__main__": - run() diff --git a/scripts/mailplus_archive.py b/scripts/mailplus_archive.py deleted file mode 100644 index f2ce718..0000000 --- a/scripts/mailplus_archive.py +++ /dev/null @@ -1,209 +0,0 @@ -#!/usr/bin/env python3 -""" -MailPlus → DEVONthink Archive DB 이메일 수집 -- Synology MailPlus IMAP 접속 -- 마지막 동기화 이후 새 메일 가져오기 -- DEVONthink Archive DB 임포트 -""" - -import os -import sys -import imaplib -import email -from email.header import decode_header -from datetime import datetime -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).parent)) -from pkm_utils import setup_logger, load_credentials, run_applescript_inline, DATA_DIR - -logger = setup_logger("mailplus") - -LAST_UID_FILE = DATA_DIR / "mailplus_last_uid.txt" -MAIL_TMP_DIR = DATA_DIR / "mail_tmp" -MAIL_TMP_DIR.mkdir(exist_ok=True) - -# 안전 관련 키워드 (dataOrigin 판별용) -SAFETY_KEYWORDS = [ - "안전", "위험", "사고", "재해", "점검", "보건", "화학물질", - "OSHA", "safety", "hazard", "incident", "KOSHA" -] - - -def decode_mime_header(value: str) -> str: - """MIME 헤더 디코딩""" - if not value: - return "" - decoded_parts = decode_header(value) - result = [] - for part, charset in decoded_parts: - if isinstance(part, bytes): - result.append(part.decode(charset or "utf-8", errors="replace")) - else: - result.append(part) - return " ".join(result) - - -def load_last_uid() -> int: - """마지막 처리 UID 로딩""" - if LAST_UID_FILE.exists(): - return int(LAST_UID_FILE.read_text().strip()) - return 0 - - -def save_last_uid(uid: int): - """마지막 처리 UID 저장""" - LAST_UID_FILE.write_text(str(uid)) - - -def detect_data_origin(subject: str, body: str) -> str: - """안전 키워드 감지로 dataOrigin 판별""" - text = (subject + " " + body).lower() - for kw in SAFETY_KEYWORDS: - if kw.lower() in text: - return "work" - return "external" - - -def save_email_file(msg: email.message.Message, uid: int) -> Path: - """이메일을 .eml 파일로 저장""" - subject = decode_mime_header(msg.get("Subject", "")) - safe_subject = "".join(c if c.isalnum() or c in " _-" else "_" for c in subject)[:50] - date_str = datetime.now().strftime("%Y%m%d_%H%M%S") - filename = f"{date_str}_{uid}_{safe_subject}.eml" - filepath = MAIL_TMP_DIR / filename - with open(filepath, "wb") as f: - f.write(msg.as_bytes()) - return filepath - - -def get_email_body(msg: email.message.Message) -> str: - """이메일 본문 추출""" - body = "" - if msg.is_multipart(): - for part in msg.walk(): - if part.get_content_type() == "text/plain": - payload = part.get_payload(decode=True) - if payload: - charset = part.get_content_charset() or "utf-8" - body += payload.decode(charset, errors="replace") - else: - payload = msg.get_payload(decode=True) - if payload: - charset = msg.get_content_charset() or "utf-8" - body = payload.decode(charset, errors="replace") - return body[:2000] - - -def import_to_devonthink(filepath: Path, subject: str, data_origin: str): - """DEVONthink Archive DB로 임포트""" - escaped_path = str(filepath).replace('"', '\\"') - escaped_subject = subject.replace('"', '\\"').replace("'", "\\'") - script = f''' - tell application id "DNtp" - set targetDB to missing value - repeat with db in databases - if name of db is "Archive" then - set targetDB to db - exit repeat - end if - end repeat - - if targetDB is not missing value then - set targetGroup to create location "/Email" in targetDB - set theRecord to import POSIX path "{escaped_path}" to targetGroup - add custom meta data "email" for "sourceChannel" to theRecord - add custom meta data "{data_origin}" for "dataOrigin" to theRecord - add custom meta data (current date) for "lastAIProcess" to theRecord - end if - end tell - ''' - try: - run_applescript_inline(script) - logger.info(f"DEVONthink 임포트: {subject[:40]}") - except Exception as e: - logger.error(f"DEVONthink 임포트 실패: {e}") - - -def run(): - """메인 실행""" - logger.info("=== MailPlus 이메일 수집 시작 ===") - - creds = load_credentials() - host = creds.get("MAILPLUS_HOST") - port = int(creds.get("MAILPLUS_PORT", "993")) - user = creds.get("MAILPLUS_USER") - password = creds.get("MAILPLUS_PASS") - - if not all([host, user, password]): - logger.error("MAILPLUS 접속 정보가 불완전합니다. credentials.env를 확인하세요.") - sys.exit(1) - - last_uid = load_last_uid() - logger.info(f"마지막 처리 UID: {last_uid}") - - try: - # IMAP SSL 접속 - mail = imaplib.IMAP4_SSL(host, port) - mail.login(user, password) - mail.select("INBOX") - logger.info("IMAP 접속 성공") - - # 마지막 UID 이후 메일 검색 - if last_uid > 0: - status, data = mail.uid("search", None, f"UID {last_uid + 1}:*") - else: - # 최초 실행: 최근 7일치만 - from datetime import timedelta - since = (datetime.now() - timedelta(days=7)).strftime("%d-%b-%Y") - status, data = mail.uid("search", None, f"SINCE {since}") - - if status != "OK": - logger.error(f"메일 검색 실패: {status}") - mail.logout() - sys.exit(1) - - uids = data[0].split() - logger.info(f"새 메일: {len(uids)}건") - - max_uid = last_uid - imported = 0 - - for uid_bytes in uids: - uid = int(uid_bytes) - if uid <= last_uid: - continue - - status, msg_data = mail.uid("fetch", uid_bytes, "(RFC822)") - if status != "OK": - continue - - raw_email = msg_data[0][1] - msg = email.message_from_bytes(raw_email) - - subject = decode_mime_header(msg.get("Subject", "(제목 없음)")) - body = get_email_body(msg) - data_origin = detect_data_origin(subject, body) - - filepath = save_email_file(msg, uid) - import_to_devonthink(filepath, subject, data_origin) - - max_uid = max(max_uid, uid) - imported += 1 - - if max_uid > last_uid: - save_last_uid(max_uid) - - mail.logout() - logger.info(f"=== MailPlus 수집 완료 — {imported}건 임포트 ===") - - except imaplib.IMAP4.error as e: - logger.error(f"IMAP 에러: {e}") - sys.exit(1) - except Exception as e: - logger.error(f"예상치 못한 에러: {e}", exc_info=True) - sys.exit(1) - - -if __name__ == "__main__": - run() diff --git a/scripts/pkm_daily_digest.py b/scripts/pkm_daily_digest.py deleted file mode 100644 index 2924f7b..0000000 --- a/scripts/pkm_daily_digest.py +++ /dev/null @@ -1,284 +0,0 @@ -#!/usr/bin/env python3 -""" -PKM 일일 다이제스트 -- DEVONthink 오늘 추가/수정 집계 -- law_monitor 법령 변경 건 파싱 -- OmniFocus 완료/추가/기한초과 집계 -- 상위 뉴스 Ollama 요약 -- OmniFocus 액션 자동 생성 -- 90일 지난 다이제스트 아카이브 -""" - -import os -import sys -import re -from datetime import datetime, timedelta -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).parent)) -from pkm_utils import ( - setup_logger, load_credentials, run_applescript_inline, - ollama_generate, count_log_errors, PROJECT_ROOT, LOGS_DIR, DATA_DIR -) - -logger = setup_logger("digest") - -DIGEST_DIR = DATA_DIR / "digests" -DIGEST_DIR.mkdir(exist_ok=True) - - -def get_devonthink_stats() -> dict: - """DEVONthink 오늘 추가/수정 문서 집계""" - script = ''' - tell application id "DNtp" - set today to current date - set time of today to 0 - set stats to {} - - repeat with db in databases - set dbName to name of db - set addedCount to count of (search "date:today" in db) - set modifiedCount to count of (search "modified:today" in db) - - if addedCount > 0 or modifiedCount > 0 then - set end of stats to dbName & ":" & addedCount & ":" & modifiedCount - end if - end repeat - - set AppleScript's text item delimiters to "|" - return stats as text - end tell - ''' - try: - result = run_applescript_inline(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])} - return stats - except Exception as e: - logger.error(f"DEVONthink 집계 실패: {e}") - return {} - - -def get_omnifocus_stats() -> dict: - """OmniFocus 오늘 완료/추가/기한초과 집계""" - script = ''' - tell application "OmniFocus" - tell default document - set today to current date - set time of today to 0 - set tomorrow to today + 1 * days - - set completedCount to count of (every flattened task whose completed is true and completion date ≥ today) - set addedCount to count of (every flattened task whose creation date ≥ today) - set overdueCount to count of (every flattened task whose completed is false and due date < today and due date is not missing value) - - return (completedCount as text) & "|" & (addedCount as text) & "|" & (overdueCount as text) - end tell - end tell - ''' - try: - result = run_applescript_inline(script) - parts = result.split("|") - return { - "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: - logger.error(f"OmniFocus 집계 실패: {e}") - return {"completed": 0, "added": 0, "overdue": 0} - - -def parse_law_changes() -> list: - """law_monitor 로그에서 오늘 법령 변경 건 파싱""" - log_file = LOGS_DIR / "law_monitor.log" - if not log_file.exists(): - return [] - - today = datetime.now().strftime("%Y-%m-%d") - changes = [] - with open(log_file, "r", encoding="utf-8") as f: - for line in f: - if today in line and "변경 감지" in line: - # "[2026-03-26 07:00:15] [law_monitor] [INFO] 변경 감지: 산업안전보건법 — 공포일자 ..." - match = re.search(r"변경 감지: (.+?)$", line) - if match: - changes.append(match.group(1).strip()) - return changes - - -def get_inbox_count() -> int: - """DEVONthink Inbox 미처리 문서 수""" - script = ''' - tell application id "DNtp" - repeat with db in databases - if name of db is "Inbox" then - return count of children of root group of db - end if - end repeat - return 0 - end tell - ''' - try: - return int(run_applescript_inline(script)) - except: - return 0 - - -def create_omnifocus_task(task_name: str, note: str = "", flagged: bool = False): - """OmniFocus 작업 생성""" - flag_str = "true" if flagged else "false" - escaped_name = task_name.replace('"', '\\"') - escaped_note = note.replace('"', '\\"') - script = f''' - tell application "OmniFocus" - tell default document - make new inbox task with properties {{name:"{escaped_name}", note:"{escaped_note}", flagged:{flag_str}}} - end tell - end tell - ''' - try: - run_applescript_inline(script) - logger.info(f"OmniFocus 작업 생성: {task_name}") - except Exception as e: - logger.error(f"OmniFocus 작업 생성 실패: {e}") - - -def get_system_health() -> dict: - """각 모듈 로그의 최근 24시간 ERROR 카운트""" - modules = ["law_monitor", "mailplus", "digest", "embed", "auto_classify"] - health = {} - for mod in modules: - log_file = LOGS_DIR / f"{mod}.log" - health[mod] = count_log_errors(log_file, since_hours=24) - return health - - -def generate_digest(): - """다이제스트 생성""" - logger.info("=== Daily Digest 생성 시작 ===") - today = datetime.now() - date_str = today.strftime("%Y-%m-%d") - - # 데이터 수집 - dt_stats = get_devonthink_stats() - of_stats = get_omnifocus_stats() - law_changes = parse_law_changes() - inbox_count = get_inbox_count() - system_health = get_system_health() - - # 마크다운 생성 - md = f"# PKM Daily Digest — {date_str}\n\n" - - # DEVONthink 현황 - md += "## DEVONthink 변화\n\n" - if dt_stats: - md += "| DB | 신규 | 수정 |\n|---|---|---|\n" - total_added = 0 - total_modified = 0 - for db_name, counts in dt_stats.items(): - md += f"| {db_name} | {counts['added']} | {counts['modified']} |\n" - total_added += counts["added"] - total_modified += counts["modified"] - md += f"| **합계** | **{total_added}** | **{total_modified}** |\n\n" - else: - md += "변화 없음\n\n" - - # 법령 변경 - md += "## 법령 변경\n\n" - if law_changes: - for change in law_changes: - md += f"- {change}\n" - md += "\n" - else: - md += "변경 없음\n\n" - - # OmniFocus 현황 - md += "## OmniFocus 현황\n\n" - md += f"- 완료: {of_stats['completed']}건\n" - md += f"- 신규: {of_stats['added']}건\n" - md += f"- 기한초과: {of_stats['overdue']}건\n\n" - - # Inbox 상태 - md += f"## Inbox 미처리: {inbox_count}건\n\n" - - # 시스템 상태 - md += "## 시스템 상태\n\n" - total_errors = sum(system_health.values()) - if total_errors == 0: - md += "모든 모듈 정상\n\n" - else: - md += "| 모듈 | 에러 수 |\n|---|---|\n" - for mod, cnt in system_health.items(): - status = f"**{cnt}**" if cnt > 0 else "0" - md += f"| {mod} | {status} |\n" - md += "\n" - - # 파일 저장 - digest_file = DIGEST_DIR / f"{date_str}_digest.md" - with open(digest_file, "w", encoding="utf-8") as f: - f.write(md) - logger.info(f"다이제스트 저장: {digest_file}") - - # DEVONthink 저장 - import_digest_to_devonthink(digest_file, date_str) - - # OmniFocus 액션 자동 생성 - if law_changes: - for change in law_changes: - create_omnifocus_task(f"법령 변경 검토: {change[:30]}", note=change) - - if inbox_count >= 3: - create_omnifocus_task(f"Inbox 정리 ({inbox_count}건 미처리)", note="DEVONthink Inbox에 미분류 문서가 쌓여있습니다.") - - if of_stats["overdue"] > 0: - create_omnifocus_task(f"기한초과 작업 처리 ({of_stats['overdue']}건)", flagged=True) - - # 90일 지난 다이제스트 아카이브 - archive_old_digests() - - logger.info("=== Daily Digest 완료 ===") - - -def import_digest_to_devonthink(filepath: Path, date_str: str): - """다이제스트를 DEVONthink에 저장""" - escaped_path = str(filepath).replace('"', '\\"') - script = f''' - tell application id "DNtp" - repeat with db in databases - if name of db is "00_Note_BOX" then - set targetGroup to create location "/Daily_Digest" in db - import POSIX path "{escaped_path}" to targetGroup - exit repeat - end if - end repeat - end tell - ''' - try: - run_applescript_inline(script) - except Exception as e: - logger.error(f"DEVONthink 다이제스트 임포트 실패: {e}") - - -def archive_old_digests(): - """90일 지난 다이제스트 이동""" - cutoff = datetime.now() - timedelta(days=90) - for f in DIGEST_DIR.glob("*_digest.md"): - try: - date_part = f.stem.split("_digest")[0] - file_date = datetime.strptime(date_part, "%Y-%m-%d") - if file_date < cutoff: - archive_dir = DIGEST_DIR / "archive" - archive_dir.mkdir(exist_ok=True) - f.rename(archive_dir / f.name) - logger.info(f"아카이브: {f.name}") - except ValueError: - pass - - -if __name__ == "__main__": - generate_digest() diff --git a/scripts/pkm_utils.py b/scripts/pkm_utils.py deleted file mode 100644 index 66c5284..0000000 --- a/scripts/pkm_utils.py +++ /dev/null @@ -1,161 +0,0 @@ -""" -PKM 시스템 공통 유틸리티 -- 로거 설정 (파일 + 콘솔) -- credentials.env 로딩 -- osascript 호출 래퍼 -""" - -import os -import sys -import logging -import subprocess -from pathlib import Path -from dotenv import load_dotenv - -# 프로젝트 루트 디렉토리 -PROJECT_ROOT = Path(__file__).parent.parent -LOGS_DIR = PROJECT_ROOT / "logs" -DATA_DIR = PROJECT_ROOT / "data" -SCRIPTS_DIR = PROJECT_ROOT / "scripts" -APPLESCRIPT_DIR = PROJECT_ROOT / "applescript" - -# 디렉토리 생성 -LOGS_DIR.mkdir(exist_ok=True) -DATA_DIR.mkdir(exist_ok=True) - - -def setup_logger(name: str) -> logging.Logger: - """모듈별 로거 설정 — 파일 + 콘솔 핸들러""" - logger = logging.getLogger(name) - if logger.handlers: - return logger # 중복 핸들러 방지 - - logger.setLevel(logging.DEBUG) - fmt = logging.Formatter("[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s", - datefmt="%Y-%m-%d %H:%M:%S") - - # 파일 핸들러 - fh = logging.FileHandler(LOGS_DIR / f"{name}.log", encoding="utf-8") - fh.setLevel(logging.DEBUG) - fh.setFormatter(fmt) - logger.addHandler(fh) - - # 콘솔 핸들러 - ch = logging.StreamHandler(sys.stdout) - ch.setLevel(logging.INFO) - ch.setFormatter(fmt) - logger.addHandler(ch) - - return logger - - -def load_credentials() -> dict: - """~/.config/pkm/credentials.env 로딩 + 누락 키 경고""" - cred_path = Path.home() / ".config" / "pkm" / "credentials.env" - if not cred_path.exists(): - # 폴백: 프로젝트 내 credentials.env (개발용) - cred_path = PROJECT_ROOT / "credentials.env" - - if cred_path.exists(): - load_dotenv(cred_path) - else: - print(f"[경고] credentials.env를 찾을 수 없습니다: {cred_path}") - - keys = { - "CLAUDE_API_KEY": os.getenv("CLAUDE_API_KEY"), - "LAW_OC": os.getenv("LAW_OC"), - "NAS_DOMAIN": os.getenv("NAS_DOMAIN"), - "NAS_TAILSCALE_IP": os.getenv("NAS_TAILSCALE_IP"), - "NAS_PORT": os.getenv("NAS_PORT", "15001"), - "MAILPLUS_HOST": os.getenv("MAILPLUS_HOST"), - "MAILPLUS_PORT": os.getenv("MAILPLUS_PORT", "993"), - "MAILPLUS_USER": os.getenv("MAILPLUS_USER"), - "MAILPLUS_PASS": os.getenv("MAILPLUS_PASS"), - "GPU_SERVER_IP": os.getenv("GPU_SERVER_IP"), - } - - missing = [k for k, v in keys.items() if not v and k not in ("GPU_SERVER_IP", "CLAUDE_API_KEY")] - if missing: - print(f"[경고] 누락된 인증 키: {', '.join(missing)}") - - return keys - - -def run_applescript(script_path: str, *args) -> str: - """osascript 호출 래퍼 + 에러 캡처""" - cmd = ["osascript", str(script_path)] + [str(a) for a in args] - try: - result = subprocess.run(cmd, capture_output=True, text=True, timeout=120) - if result.returncode != 0: - raise RuntimeError(f"AppleScript 에러: {result.stderr.strip()}") - return result.stdout.strip() - except subprocess.TimeoutExpired: - raise RuntimeError(f"AppleScript 타임아웃: {script_path}") - - -def run_applescript_inline(script: str) -> str: - """인라인 AppleScript 실행 — 단일 -e 방식""" - cmd = ["osascript", "-e", script] - try: - result = subprocess.run(cmd, capture_output=True, text=True, timeout=120) - if result.returncode != 0: - raise RuntimeError(f"AppleScript 에러: {result.stderr.strip()}") - return result.stdout.strip() - except subprocess.TimeoutExpired: - raise RuntimeError("AppleScript 타임아웃 (인라인)") - - -def llm_generate(prompt: str, model: str = "mlx-community/Qwen3.5-35B-A3B-4bit", - host: str = "http://localhost:8800", json_mode: bool = False) -> str: - """MLX 서버 API 호출 (OpenAI 호환)""" - import requests - messages = [{"role": "user", "content": prompt}] - resp = requests.post(f"{host}/v1/chat/completions", json={ - "model": model, - "messages": messages, - "temperature": 0.3, - "max_tokens": 4096, - }, timeout=300) - resp.raise_for_status() - content = resp.json()["choices"][0]["message"]["content"] - if not json_mode: - return content - # JSON 모드: thinking 허용 → 마지막 유효 JSON 객체 추출 - import re - import json as _json - # 배열이 포함된 JSON 객체 매칭 - all_jsons = re.findall(r'\{[^{}]*(?:\[[^\]]*\])?[^{}]*\}', content) - for j in reversed(all_jsons): - try: - parsed = _json.loads(j) - if any(k in parsed for k in ("domain_db", "tags", "domain", "classification")): - return j - except _json.JSONDecodeError: - continue - # 폴백: 전체에서 가장 큰 JSON 추출 - json_match = re.search(r'\{[\s\S]*\}', content) - return json_match.group(0) if json_match else content - - -# 하위호환 별칭 -ollama_generate = llm_generate - - -def count_log_errors(log_file: Path, since_hours: int = 24) -> int: - """로그 파일에서 최근 N시간 ERROR 카운트""" - from datetime import datetime, timedelta - if not log_file.exists(): - return 0 - cutoff = datetime.now() - timedelta(hours=since_hours) - count = 0 - with open(log_file, "r", encoding="utf-8") as f: - for line in f: - if "[ERROR]" in line: - try: - ts_str = line[1:20] # [YYYY-MM-DD HH:MM:SS] - ts = datetime.strptime(ts_str, "%Y-%m-%d %H:%M:%S") - if ts >= cutoff: - count += 1 - except (ValueError, IndexError): - count += 1 - return count diff --git a/tests/test_classify.py b/tests/test_classify.py deleted file mode 100644 index 13ffee6..0000000 --- a/tests/test_classify.py +++ /dev/null @@ -1,129 +0,0 @@ -#!/usr/bin/env python3 -""" -분류 프롬프트 단위 테스트 -5종 문서(법령, 뉴스, 논문, 메모, 이메일)로 분류 정확도 확인 -※ Ollama가 실행 중인 Mac mini에서 테스트해야 함 -""" - -import json -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).parent.parent / "scripts")) -from pkm_utils import llm_generate, PROJECT_ROOT - -PROMPT_TEMPLATE = (PROJECT_ROOT / "scripts" / "prompts" / "classify_document.txt").read_text() - -# 테스트 문서 5종 -TEST_DOCUMENTS = [ - { - "name": "법령 — 산업안전보건법 시행규칙 개정", - "text": "산업안전보건법 시행규칙 일부개정령안 입법예고. 고용노동부는 위험성평가에 관한 지침을 개정하여 사업장의 위험성평가 실시 주기를 연 1회에서 반기 1회로 강화하고, 위험성평가 결과의 기록 보존 기간을 3년에서 5년으로 확대하는 내용을 포함합니다.", - "expected_db": "04_Industrial safety", - "expected_origin": "external", - }, - { - "name": "뉴스 — AI 기술 동향", - "text": "OpenAI가 GPT-5를 발표했습니다. 이번 모델은 멀티모달 기능이 대폭 강화되었으며, 코드 생성 능력이 기존 대비 40% 향상되었습니다. 특히 에이전트 기능이 추가되어 복잡한 작업을 자율적으로 수행할 수 있게 되었습니다.", - "expected_db": "05_Programming", - "expected_origin": "external", - }, - { - "name": "논문 — 위험성평가 방법론", - "text": "A Literature Review on Risk Assessment Methodologies in Manufacturing Industry. This paper reviews the current state of risk assessment practices in Korean manufacturing facilities, comparing KOSHA guidelines with international standards including ISO 45001 and OSHA regulations.", - "expected_db": "04_Industrial safety", - "expected_origin": "external", - }, - { - "name": "메모 — 업무 노트", - "text": "오늘 공장 순회점검에서 3층 용접 작업장의 환기 시설이 미작동 상태인 것을 확인했다. 시정조치 요청서를 작성하고 생산팀장에게 전달해야 한다. TODO: 시정조치 결과 확인 (3일 내)", - "expected_db": "04_Industrial safety", - "expected_origin": "work", - }, - { - "name": "이메일 — 일반 업무", - "text": "안녕하세요 테크니컬코리아 안현기 과장님, 지난번 요청하신 용접기 부품 견적서를 첨부합니다. 납기는 발주 후 2주입니다. 감사합니다. - ○○산업 김철수 드림", - "expected_db": "99_Technicalkorea", - "expected_origin": "work", - }, -] - - -def run_classify_test(doc: dict) -> dict: - """단일 문서 분류 테스트""" - prompt = PROMPT_TEMPLATE.replace("{document_text}", doc["text"]) - - try: - response = llm_generate(prompt, json_mode=True) - result = json.loads(response) - - db_match = result.get("domain_db") == doc["expected_db"] - origin_match = result.get("dataOrigin") == doc["expected_origin"] - - return { - "name": doc["name"], - "pass": db_match and origin_match, - "expected_db": doc["expected_db"], - "actual_db": result.get("domain_db"), - "expected_origin": doc["expected_origin"], - "actual_origin": result.get("dataOrigin"), - "tags": result.get("tags", []), - "sub_group": result.get("sub_group"), - "error": None, - } - except json.JSONDecodeError as e: - return {"name": doc["name"], "pass": False, "error": f"JSON 파싱 실패: {e}"} - except Exception as e: - return {"name": doc["name"], "pass": False, "error": str(e)} - - -def main(): - print("=" * 60) - print("PKM 문서 분류 테스트") - print("=" * 60) - - results = [] - for doc in TEST_DOCUMENTS: - print(f"\n테스트: {doc['name']}") - result = run_classify_test(doc) - results.append(result) - - status = "PASS" if result["pass"] else "FAIL" - print(f" [{status}]") - if result.get("error"): - print(f" 에러: {result['error']}") - else: - print(f" DB: {result.get('actual_db')} (기대: {result.get('expected_db')})") - print(f" Origin: {result.get('actual_origin')} (기대: {result.get('expected_origin')})") - print(f" 태그: {result.get('tags')}") - print(f" 그룹: {result.get('sub_group')}") - - # 요약 - passed = sum(1 for r in results if r["pass"]) - total = len(results) - print(f"\n{'=' * 60}") - print(f"결과: {passed}/{total} 통과") - print("=" * 60) - - # 리포트 저장 - report_path = PROJECT_ROOT / "docs" / "test-report.md" - with open(report_path, "w", encoding="utf-8") as f: - f.write(f"# 분류 테스트 리포트\n\n") - f.write(f"실행일시: {__import__('datetime').datetime.now()}\n\n") - f.write(f"## 결과: {passed}/{total}\n\n") - for r in results: - status = "PASS" if r["pass"] else "FAIL" - f.write(f"### [{status}] {r['name']}\n") - if r.get("error"): - f.write(f"- 에러: {r['error']}\n") - else: - f.write(f"- DB: {r.get('actual_db')} (기대: {r.get('expected_db')})\n") - f.write(f"- Origin: {r.get('actual_origin')} (기대: {r.get('expected_origin')})\n") - f.write(f"- 태그: {r.get('tags')}\n\n") - - print(f"\n리포트 저장: {report_path}") - sys.exit(0 if passed == total else 1) - - -if __name__ == "__main__": - main()