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>
This commit is contained in:
Hyungi Ahn
2026-03-26 12:32:36 +09:00
parent bec9579a8a
commit 084d3a8c63
14 changed files with 1564 additions and 0 deletions

129
tests/test_classify.py Normal file
View File

@@ -0,0 +1,129 @@
#!/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 ollama_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 = ollama_generate(prompt)
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()