From dc3f03b42102fd0b02c27a07130d6e3fc611b4cf Mon Sep 17 00:00:00 2001 From: hyungi Date: Mon, 30 Mar 2026 14:00:46 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20Phase=202=20=EB=B2=84=EA=B7=B8=20?= =?UTF-8?q?=ED=94=BD=EC=8A=A4=20=E2=80=94=20JP=20=EB=B2=88=EC=97=AD,=20API?= =?UTF-8?q?=20=EC=84=9C=EB=B2=84,=20AppleScript=20=EA=B2=BD=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pkm_utils.py: strip_thinking() 추가 + llm_generate() no_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) --- applescript/auto_classify.scpt | 23 ++++++++++++++--------- applescript/omnifocus_sync.scpt | 6 +++++- scripts/law_monitor.py | 6 ++---- scripts/pkm_api_server.py | 33 +++++++++++++-------------------- scripts/pkm_utils.py | 29 +++++++++++++++++++++++++---- 5 files changed, 59 insertions(+), 38 deletions(-) diff --git a/applescript/auto_classify.scpt b/applescript/auto_classify.scpt index 49c2820..c119463 100644 --- a/applescript/auto_classify.scpt +++ b/applescript/auto_classify.scpt @@ -2,7 +2,14 @@ -- Inbox DB 새 문서 → OCR 전처리 → MLX 분류 → 태그 + 메타데이터 + 도메인 DB 이동 → Qdrant 임베딩 -- Smart Rule 설정: Event = On Import, 조건 = Tags is empty +property baseDir : "Documents/code/DEVONThink_my server" + 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" repeat with theRecord in theRecords try @@ -13,16 +20,15 @@ on performSmartRule(theRecords) if docText is "" 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 (POSIX path of (path to home folder)) & "Documents/code/DEVONThink_my server/scripts/ocr_preprocess.py" + set ocrPy to pkmRoot & "/scripts/ocr_preprocess.py" 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 set plain text of theRecord to ocrText set docText to ocrText end if 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 if end if @@ -39,7 +45,7 @@ on performSmartRule(theRecords) end if -- 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 -- 문서 텍스트를 프롬프트에 삽입 (특수문자 이스케이프) @@ -105,14 +111,13 @@ except: 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_qdrant.py" - do shell script embedScript & " " & quoted form of embedPy & " " & quoted form of docUUID & " &> /dev/null &" + set embedPy to pkmRoot & "/scripts/embed_to_qdrant.py" + do shell script venvPython & " " & 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" + do shell script "echo '[" & (current date) & "] [auto_classify] [ERROR] " & errMsg & "' >> " & quoted form of logFile end try end repeat end tell diff --git a/applescript/omnifocus_sync.scpt b/applescript/omnifocus_sync.scpt index dfa32d6..036ab65 100644 --- a/applescript/omnifocus_sync.scpt +++ b/applescript/omnifocus_sync.scpt @@ -2,7 +2,11 @@ -- Projects DB 새 문서에서 TODO 패턴 감지 → OmniFocus 작업 생성 -- Smart Rule 설정: Event = On Import, DB = Projects +property baseDir : "Documents/code/DEVONThink_my server" + 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" repeat with theRecord in theRecords try @@ -64,7 +68,7 @@ for item in items[:10]: 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" + do shell script "echo '[" & (current date) & "] [omnifocus_sync] [ERROR] " & errMsg & "' >> " & quoted form of logFile end try end repeat end tell diff --git a/scripts/law_monitor.py b/scripts/law_monitor.py index 39e5d74..d8d94b3 100644 --- a/scripts/law_monitor.py +++ b/scripts/law_monitor.py @@ -315,11 +315,9 @@ def fetch_jp_mhlw(last_check: dict) -> int: translated = "" try: 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: translated = title diff --git a/scripts/pkm_api_server.py b/scripts/pkm_api_server.py index 8a301f5..a820398 100644 --- a/scripts/pkm_api_server.py +++ b/scripts/pkm_api_server.py @@ -35,24 +35,14 @@ def run_applescript(script: str, timeout: int = 120) -> str: @app.route('/devonthink/stats') def devonthink_stats(): try: + # DB별 문서 수만 빠르게 조회 (children 순회 대신 count 사용) script = ( 'tell application id "DNtp"\n' - ' set today to current date\n' - ' set time of today to 0\n' ' set stats to {}\n' ' repeat with db in databases\n' ' set dbName to name of db\n' - ' set addedCount to 0\n' - ' set modifiedCount to 0\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' + ' set docCount to count of contents of db\n' + ' set end of stats to dbName & ":" & docCount\n' ' end repeat\n' ' set AppleScript\'s text item delimiters to "|"\n' ' return stats as text\n' @@ -60,17 +50,18 @@ def devonthink_stats(): ) result = run_applescript(script) stats = {} + total = 0 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])} - total_added = sum(s['added'] for s in stats.values()) - total_modified = sum(s['modified'] for s in stats.values()) + if len(parts) == 2: + count = int(parts[1]) + stats[parts[0]] = {'count': count} + total += count return jsonify(success=True, data={ 'databases': stats, - 'total_added': total_added, - 'total_modified': total_modified + 'total_documents': total, + 'database_count': len(stats), }) except Exception as e: return jsonify(success=False, error=str(e)), 500 @@ -83,9 +74,11 @@ def devonthink_search(): if not q: return jsonify(success=False, error='q parameter required'), 400 try: + # 한글 쿼리 이스케이프 (따옴표, 백슬래시) + safe_q = q.replace('\\', '\\\\').replace('"', '\\"') script = ( 'tell application id "DNtp"\n' - f' set results to search "{q}"\n' + f' set results to search "{safe_q}"\n' ' set output to {}\n' f' set maxCount to {limit}\n' ' set i to 0\n' diff --git a/scripts/pkm_utils.py b/scripts/pkm_utils.py index 66c5284..fdf706e 100644 --- a/scripts/pkm_utils.py +++ b/scripts/pkm_utils.py @@ -105,19 +105,40 @@ def run_applescript_inline(script: str) -> str: raise RuntimeError("AppleScript 타임아웃 (인라인)") +def strip_thinking(text: str) -> str: + """LLM thinking 출력 제거 — ... 태그 및 thinking 패턴 필터링""" + import re + # ... 태그 제거 + text = re.sub(r'[\s\S]*?\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", - host: str = "http://localhost:8800", json_mode: bool = False) -> str: - """MLX 서버 API 호출 (OpenAI 호환)""" + host: str = "http://localhost:8800", json_mode: bool = False, + no_think: bool = False) -> str: + """MLX 서버 API 호출 (OpenAI 호환) + no_think=True: thinking 비활성화 + 응답 필터링 (번역 등 단순 작업용) + """ import requests messages = [{"role": "user", "content": prompt}] - resp = requests.post(f"{host}/v1/chat/completions", json={ + payload = { "model": model, "messages": messages, "temperature": 0.3, "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() content = resp.json()["choices"][0]["message"]["content"] + if no_think: + content = strip_thinking(content) if not json_mode: return content # JSON 모드: thinking 허용 → 마지막 유효 JSON 객체 추출