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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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 객체 추출
|
||||||
|
|||||||
Reference in New Issue
Block a user