From a4f8e566330cab515b63f1197e76b1dac488b018 Mon Sep 17 00:00:00 2001 From: hyungi Date: Mon, 30 Mar 2026 15:28:36 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=B2=95=EB=A0=B9=20=ED=81=AC=EB=A1=9C?= =?UTF-8?q?=EC=8A=A4=20=EB=A7=81=ED=81=AC=202-pass=20+=20launchd=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20+=20RAG=20thinking=20=ED=95=84=ED=84=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - law_monitor.py: 2-pass 크로스 링크 적용 - Pass 1: 전체 법령 파싱 + 조문-장 매핑 테이블 생성 - Pass 2: 「법령명」 제X조 → [[법명_제N장#제X조]] wiki-link 일괄 적용 - 변경된 법령에만 크로스 링크 적용 후 DEVONthink 임포트 - pkm_api_server.py: RAG 응답에 enable_thinking=false + strip_thinking 적용 - launchd: pkm-api(Flask), law-monitor(07:00), mailplus(07:00+18:00), digest(20:00) plist Co-Authored-By: Claude Opus 4.6 (1M context) --- launchd/net.hyungi.pkm-api.plist | 24 +++++++++++++++ scripts/law_monitor.py | 51 ++++++++++++++++++++++++++++++-- scripts/pkm_api_server.py | 7 +++-- 3 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 launchd/net.hyungi.pkm-api.plist diff --git a/launchd/net.hyungi.pkm-api.plist b/launchd/net.hyungi.pkm-api.plist new file mode 100644 index 0000000..44950af --- /dev/null +++ b/launchd/net.hyungi.pkm-api.plist @@ -0,0 +1,24 @@ + + + + + Label + net.hyungi.pkm-api + ProgramArguments + + /Users/hyungi/Documents/code/DEVONThink_my server/venv/bin/python3 + /Users/hyungi/Documents/code/DEVONThink_my server/scripts/pkm_api_server.py + 9900 + + WorkingDirectory + /Users/hyungi/Documents/code/DEVONThink_my server + RunAtLoad + + KeepAlive + + StandardOutPath + /Users/hyungi/Documents/code/DEVONThink_my server/logs/pkm-api.log + StandardErrorPath + /Users/hyungi/Documents/code/DEVONThink_my server/logs/pkm-api.error.log + + diff --git a/scripts/law_monitor.py b/scripts/law_monitor.py index 10f17b5..3cfa08b 100644 --- a/scripts/law_monitor.py +++ b/scripts/law_monitor.py @@ -237,6 +237,7 @@ def run(include_tier2: bool = False): last_check = load_last_check() changes_found = 0 failures = [] + parsed_laws = {} # 크로스 링크 2-pass용 for law in laws: law_name = law["name"] @@ -281,11 +282,17 @@ def run(include_tier2: bool = False): # XML 저장 xml_path = save_law_file(law_name, xml_text) - # XML → MD 장 분할 + # Pass 1: XML 파싱 + 장 분할 MD 저장 (내부 링크만) try: parsed = parse_law_xml(str(xml_path)) md_files = save_law_as_markdown(law_name, parsed, MD_OUTPUT_DIR) - import_law_to_devonthink(law_name, md_files, category) + # 크로스 링크용 매핑 수집 + parsed_laws[law_name] = { + "parsed": parsed, + "md_files": md_files, + "category": category, + "article_map": build_article_chapter_map(law_name, parsed), + } changes_found += 1 except Exception as e: logger.error(f"법령 파싱/임포트 실패 [{law_name}]: {e}", exc_info=True) @@ -294,7 +301,45 @@ def run(include_tier2: bool = False): last_check[law_name] = announce_date else: - logger.debug(f"변경 없음: {law_name}") + # 변경 없어도 기존 파싱 데이터로 매핑 수집 (크로스 링크용) + xml_path = LAWS_DIR / f"{law_name.replace(' ', '_').replace('/', '_')}_{datetime.now().strftime('%Y%m%d')}.xml" + if not xml_path.exists(): + # 오늘 날짜 파일이 없으면 가장 최근 파일 찾기 + candidates = sorted(LAWS_DIR.glob(f"{law_name.replace(' ', '_').replace('/', '_')}_*.xml")) + xml_path = candidates[-1] if candidates else None + if xml_path and xml_path.exists(): + try: + parsed = parse_law_xml(str(xml_path)) + parsed_laws[law_name] = { + "parsed": parsed, + "md_files": [], + "category": category, + "article_map": build_article_chapter_map(law_name, parsed), + } + except Exception: + pass + + # Pass 2: 크로스 링크 일괄 적용 (변경된 법령만) + if parsed_laws: + # 전체 조문-장 매핑 테이블 + global_article_map = {name: data["article_map"] for name, data in parsed_laws.items()} + changed_laws = {name: data for name, data in parsed_laws.items() if data["md_files"]} + + if changed_laws and len(global_article_map) > 1: + logger.info(f"크로스 링크 적용: {len(changed_laws)}개 법령, 매핑 {len(global_article_map)}개") + for law_name, data in changed_laws.items(): + for md_file in data["md_files"]: + if md_file.name == "00_기본정보.md" or md_file.name == "부칙.md": + continue + content = md_file.read_text(encoding="utf-8") + updated = add_cross_law_links(content, law_name, global_article_map) + if updated != content: + md_file.write_text(updated, encoding="utf-8") + + # DEVONthink 임포트 (크로스 링크 적용 후) + for law_name, data in changed_laws.items(): + if data["md_files"]: + import_law_to_devonthink(law_name, data["md_files"], data["category"]) save_last_check(last_check) diff --git a/scripts/pkm_api_server.py b/scripts/pkm_api_server.py index a820398..8393240 100644 --- a/scripts/pkm_api_server.py +++ b/scripts/pkm_api_server.py @@ -252,16 +252,19 @@ def _search_qdrant(vector: list[float], limit: int = 20) -> list[dict]: def _llm_generate(prompt: str) -> str: - """Mac Mini MLX로 답변 생성""" + """Mac Mini MLX로 답변 생성 (thinking 필터링 포함)""" import requests as req + from pkm_utils import strip_thinking resp = req.post("http://localhost:8800/v1/chat/completions", json={ "model": "mlx-community/Qwen3.5-35B-A3B-4bit", "messages": [{"role": "user", "content": prompt}], "temperature": 0.3, "max_tokens": 2048, + "enable_thinking": False, }, timeout=120) resp.raise_for_status() - return resp.json()["choices"][0]["message"]["content"] + content = resp.json()["choices"][0]["message"]["content"] + return strip_thinking(content) @app.route('/rag/query', methods=['POST'])