Files
hyungi_document_server/scripts/pkm_utils.py
Hyungi Ahn 084d3a8c63 feat: 전체 PKM 스크립트 일괄 작성 — 분류/법령/메일/다이제스트/임베딩
- scripts/pkm_utils.py: 공통 유틸 (로거, dotenv, osascript 래퍼)
- scripts/prompts/classify_document.txt: Ollama 분류 프롬프트
- applescript/auto_classify.scpt: Inbox → AI 분류 → DB 이동
- applescript/omnifocus_sync.scpt: Projects → OmniFocus 작업 생성
- scripts/law_monitor.py: 법령 변경 모니터링 + DEVONthink 임포트
- scripts/mailplus_archive.py: MailPlus IMAP → Archive DB
- scripts/pkm_daily_digest.py: 일일 다이제스트 + OmniFocus 액션
- scripts/embed_to_chroma.py: GPU 서버 벡터 임베딩 → ChromaDB
- launchd/*.plist: 3개 스케줄 (07:00, 07:00+18:00, 20:00)
- docs/deploy.md: Mac mini 배포 가이드
- docs/devonagent-setup.md: 검색 세트 9종 설정 가이드
- tests/test_classify.py: 5종 문서 분류 테스트

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 12:32:36 +09:00

139 lines
4.5 KiB
Python

"""
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 실행"""
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 ollama_generate(prompt: str, model: str = "qwen3.5:35b-a3b-q4_K_M",
host: str = "http://localhost:11434") -> str:
"""Ollama API 호출"""
import requests
resp = requests.post(f"{host}/api/generate", json={
"model": model,
"prompt": prompt,
"stream": False
}, timeout=120)
resp.raise_for_status()
return resp.json().get("response", "")
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