fix: Phase 2 버그 픽스 — JP 번역, API 서버, AppleScript 경로

- pkm_utils.py: strip_thinking() 추가 + llm_generate() no_think 옵션
  - <think> 태그 제거 + thinking 패턴("Wait,", "Let me" 등) 필터링
  - enable_thinking: false 파라미터 지원
- law_monitor.py: JP 번역 호출에 no_think=True 적용
- pkm_api_server.py: /devonthink/stats 최적화 (children 순회 → count 사용)
  + /devonthink/search 한글 쿼리 이스케이프 수정
- auto_classify.scpt: baseDir property로 경로 변수화
- omnifocus_sync.scpt: 로그 경로 변수화

인프라: MailPlus IMAP HOST → LAN IP(192.168.1.227)로 변경
참고: 한국 법령 API IP(122.153.226.74) open.law.go.kr 등록 필요

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hyungi
2026-03-30 14:00:46 +09:00
parent f21f950c04
commit dc3f03b421
5 changed files with 59 additions and 38 deletions

View File

@@ -2,7 +2,14 @@
-- Inbox DB 새 문서 → OCR 전처리 → MLX 분류 → 태그 + 메타데이터 + 도메인 DB 이동 → Qdrant 임베딩 -- Inbox DB 새 문서 → OCR 전처리 → MLX 분류 → 태그 + 메타데이터 + 도메인 DB 이동 → Qdrant 임베딩
-- Smart Rule 설정: Event = On Import, 조건 = Tags is empty -- Smart Rule 설정: Event = On Import, 조건 = Tags is empty
property baseDir : "Documents/code/DEVONThink_my server"
on performSmartRule(theRecords) on performSmartRule(theRecords)
set homeDir to POSIX path of (path to home folder)
set pkmRoot to homeDir & baseDir
set venvPython to pkmRoot & "/venv/bin/python3"
set logFile to pkmRoot & "/logs/auto_classify.log"
tell application id "DNtp" tell application id "DNtp"
repeat with theRecord in theRecords repeat with theRecord in theRecords
try try
@@ -13,16 +20,15 @@ on performSmartRule(theRecords)
if docText is "" then if docText is "" then
if docType is in {"PDF Document", "JPEG image", "PNG image", "TIFF image"} then if docType is in {"PDF Document", "JPEG image", "PNG image", "TIFF image"} then
set ocrScript to (POSIX path of (path to home folder)) & "Documents/code/DEVONThink_my server/venv/bin/python3" set ocrPy to pkmRoot & "/scripts/ocr_preprocess.py"
set ocrPy to (POSIX path of (path to home folder)) & "Documents/code/DEVONThink_my server/scripts/ocr_preprocess.py"
try try
set ocrText to do shell script ocrScript & " " & quoted form of ocrPy & " " & quoted form of docUUID set ocrText to do shell script venvPython & " " & quoted form of ocrPy & " " & quoted form of docUUID
if length of ocrText > 0 then if length of ocrText > 0 then
set plain text of theRecord to ocrText set plain text of theRecord to ocrText
set docText to ocrText set docText to ocrText
end if end if
on error ocrErr on error ocrErr
do shell script "echo '[OCR ERROR] " & ocrErr & "' >> ~/Documents/code/DEVONThink_my\\ server/logs/auto_classify.log" do shell script "echo '[OCR ERROR] " & ocrErr & "' >> " & quoted form of logFile
end try end try
end if end if
end if end if
@@ -39,7 +45,7 @@ on performSmartRule(theRecords)
end if end if
-- 2. 분류 프롬프트 로딩 -- 2. 분류 프롬프트 로딩
set promptPath to (POSIX path of (path to home folder)) & "Documents/code/DEVONThink_my server/scripts/prompts/classify_document.txt" set promptPath to pkmRoot & "/scripts/prompts/classify_document.txt"
set promptTemplate to do shell script "cat " & quoted form of promptPath set promptTemplate to do shell script "cat " & quoted form of promptPath
-- 문서 텍스트를 프롬프트에 삽입 (특수문자 이스케이프) -- 문서 텍스트를 프롬프트에 삽입 (특수문자 이스케이프)
@@ -105,14 +111,13 @@ except:
end if end if
-- 8. GPU 서버 벡터 임베딩 비동기 전송 -- 8. GPU 서버 벡터 임베딩 비동기 전송
set embedScript to (POSIX path of (path to home folder)) & "Documents/code/DEVONThink_my server/venv/bin/python3" set embedPy to pkmRoot & "/scripts/embed_to_qdrant.py"
set embedPy to (POSIX path of (path to home folder)) & "Documents/code/DEVONThink_my server/scripts/embed_to_qdrant.py" do shell script venvPython & " " & quoted form of embedPy & " " & quoted form of docUUID & " &> /dev/null &"
do shell script embedScript & " " & quoted form of embedPy & " " & quoted form of docUUID & " &> /dev/null &"
on error errMsg on error errMsg
-- 에러 시 로그 기록 + 검토필요 태그 -- 에러 시 로그 기록 + 검토필요 태그
set tags of theRecord to {"@상태/검토필요", "AI분류실패"} 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" do shell script "echo '[" & (current date) & "] [auto_classify] [ERROR] " & errMsg & "' >> " & quoted form of logFile
end try end try
end repeat end repeat
end tell end tell

View File

@@ -2,7 +2,11 @@
-- Projects DB 새 문서에서 TODO 패턴 감지 → OmniFocus 작업 생성 -- Projects DB 새 문서에서 TODO 패턴 감지 → OmniFocus 작업 생성
-- Smart Rule 설정: Event = On Import, DB = Projects -- Smart Rule 설정: Event = On Import, DB = Projects
property baseDir : "Documents/code/DEVONThink_my server"
on performSmartRule(theRecords) on performSmartRule(theRecords)
set homeDir to POSIX path of (path to home folder)
set logFile to homeDir & baseDir & "/logs/omnifocus_sync.log"
tell application id "DNtp" tell application id "DNtp"
repeat with theRecord in theRecords repeat with theRecord in theRecords
try try
@@ -64,7 +68,7 @@ for item in items[:10]:
add custom meta data taskIDString for "omnifocusTaskID" to theRecord add custom meta data taskIDString for "omnifocusTaskID" to theRecord
on error errMsg on error errMsg
do shell script "echo '[" & (current date) & "] [omnifocus_sync] [ERROR] " & errMsg & "' >> ~/Documents/code/DEVONThink_my\\ server/logs/omnifocus_sync.log" do shell script "echo '[" & (current date) & "] [omnifocus_sync] [ERROR] " & errMsg & "' >> " & quoted form of logFile
end try end try
end repeat end repeat
end tell end tell

View File

@@ -315,11 +315,9 @@ def fetch_jp_mhlw(last_check: dict) -> int:
translated = "" translated = ""
try: try:
translated = llm_generate( translated = llm_generate(
f"다음 일본어 제목을 한국어로 번역해줘. 번역만 출력하고 다른 말은 하지 마.\n\n{title}" f"다음 일본어 제목을 한국어로 번역해줘. 번역만 출력하고 다른 말은 하지 마.\n\n{title}",
no_think=True
) )
# thinking 출력 제거 — 마지막 줄만 사용
lines = [l.strip() for l in translated.strip().split("\n") if l.strip()]
translated = lines[-1] if lines else title
except Exception: except Exception:
translated = title translated = title

View File

@@ -35,24 +35,14 @@ def run_applescript(script: str, timeout: int = 120) -> str:
@app.route('/devonthink/stats') @app.route('/devonthink/stats')
def devonthink_stats(): def devonthink_stats():
try: try:
# DB별 문서 수만 빠르게 조회 (children 순회 대신 count 사용)
script = ( script = (
'tell application id "DNtp"\n' 'tell application id "DNtp"\n'
' set today to current date\n'
' set time of today to 0\n'
' set stats to {}\n' ' set stats to {}\n'
' repeat with db in databases\n' ' repeat with db in databases\n'
' set dbName to name of db\n' ' set dbName to name of db\n'
' set addedCount to 0\n' ' set docCount to count of contents of db\n'
' set modifiedCount to 0\n' ' set end of stats to dbName & ":" & docCount\n'
' repeat with rec in children of root of db\n'
' try\n'
' if creation date of rec >= today then set addedCount to addedCount + 1\n'
' if modification date of rec >= today then set modifiedCount to modifiedCount + 1\n'
' end try\n'
' end repeat\n'
' if addedCount > 0 or modifiedCount > 0 then\n'
' set end of stats to dbName & ":" & addedCount & ":" & modifiedCount\n'
' end if\n'
' end repeat\n' ' end repeat\n'
' set AppleScript\'s text item delimiters to "|"\n' ' set AppleScript\'s text item delimiters to "|"\n'
' return stats as text\n' ' return stats as text\n'
@@ -60,17 +50,18 @@ def devonthink_stats():
) )
result = run_applescript(script) result = run_applescript(script)
stats = {} stats = {}
total = 0
if result: if result:
for item in result.split('|'): for item in result.split('|'):
parts = item.split(':') parts = item.split(':')
if len(parts) == 3: if len(parts) == 2:
stats[parts[0]] = {'added': int(parts[1]), 'modified': int(parts[2])} count = int(parts[1])
total_added = sum(s['added'] for s in stats.values()) stats[parts[0]] = {'count': count}
total_modified = sum(s['modified'] for s in stats.values()) total += count
return jsonify(success=True, data={ return jsonify(success=True, data={
'databases': stats, 'databases': stats,
'total_added': total_added, 'total_documents': total,
'total_modified': total_modified 'database_count': len(stats),
}) })
except Exception as e: except Exception as e:
return jsonify(success=False, error=str(e)), 500 return jsonify(success=False, error=str(e)), 500
@@ -83,9 +74,11 @@ def devonthink_search():
if not q: if not q:
return jsonify(success=False, error='q parameter required'), 400 return jsonify(success=False, error='q parameter required'), 400
try: try:
# 한글 쿼리 이스케이프 (따옴표, 백슬래시)
safe_q = q.replace('\\', '\\\\').replace('"', '\\"')
script = ( script = (
'tell application id "DNtp"\n' 'tell application id "DNtp"\n'
f' set results to search "{q}"\n' f' set results to search "{safe_q}"\n'
' set output to {}\n' ' set output to {}\n'
f' set maxCount to {limit}\n' f' set maxCount to {limit}\n'
' set i to 0\n' ' set i to 0\n'

View File

@@ -105,19 +105,40 @@ def run_applescript_inline(script: str) -> str:
raise RuntimeError("AppleScript 타임아웃 (인라인)") raise RuntimeError("AppleScript 타임아웃 (인라인)")
def strip_thinking(text: str) -> str:
"""LLM thinking 출력 제거 — <think>...</think> 태그 및 thinking 패턴 필터링"""
import re
# <think>...</think> 태그 제거
text = re.sub(r'<think>[\s\S]*?</think>\s*', '', text)
# "Wait,", "Let me", "I'll check" 등으로 시작하는 thinking 줄 제거
lines = text.strip().split('\n')
filtered = [l for l in lines if not re.match(
r'^\s*(Wait|Let me|I\'ll|Hmm|OK,|Okay|Let\'s|Actually|So,|First)', l, re.IGNORECASE
)]
return '\n'.join(filtered).strip() if filtered else text.strip()
def llm_generate(prompt: str, model: str = "mlx-community/Qwen3.5-35B-A3B-4bit", def llm_generate(prompt: str, model: str = "mlx-community/Qwen3.5-35B-A3B-4bit",
host: str = "http://localhost:8800", json_mode: bool = False) -> str: host: str = "http://localhost:8800", json_mode: bool = False,
"""MLX 서버 API 호출 (OpenAI 호환)""" no_think: bool = False) -> str:
"""MLX 서버 API 호출 (OpenAI 호환)
no_think=True: thinking 비활성화 + 응답 필터링 (번역 등 단순 작업용)
"""
import requests import requests
messages = [{"role": "user", "content": prompt}] messages = [{"role": "user", "content": prompt}]
resp = requests.post(f"{host}/v1/chat/completions", json={ payload = {
"model": model, "model": model,
"messages": messages, "messages": messages,
"temperature": 0.3, "temperature": 0.3,
"max_tokens": 4096, "max_tokens": 4096,
}, timeout=300) }
if no_think:
payload["enable_thinking"] = False
resp = requests.post(f"{host}/v1/chat/completions", json=payload, timeout=300)
resp.raise_for_status() resp.raise_for_status()
content = resp.json()["choices"][0]["message"]["content"] content = resp.json()["choices"][0]["message"]["content"]
if no_think:
content = strip_thinking(content)
if not json_mode: if not json_mode:
return content return content
# JSON 모드: thinking 허용 → 마지막 유효 JSON 객체 추출 # JSON 모드: thinking 허용 → 마지막 유효 JSON 객체 추출