feat: 법령 크로스 링크 2-pass + launchd 등록 + RAG thinking 필터

- 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) <noreply@anthropic.com>
This commit is contained in:
hyungi
2026-03-30 15:28:36 +09:00
parent c79e26e822
commit a4f8e56633
3 changed files with 77 additions and 5 deletions

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>net.hyungi.pkm-api</string>
<key>ProgramArguments</key>
<array>
<string>/Users/hyungi/Documents/code/DEVONThink_my server/venv/bin/python3</string>
<string>/Users/hyungi/Documents/code/DEVONThink_my server/scripts/pkm_api_server.py</string>
<string>9900</string>
</array>
<key>WorkingDirectory</key>
<string>/Users/hyungi/Documents/code/DEVONThink_my server</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/hyungi/Documents/code/DEVONThink_my server/logs/pkm-api.log</string>
<key>StandardErrorPath</key>
<string>/Users/hyungi/Documents/code/DEVONThink_my server/logs/pkm-api.error.log</string>
</dict>
</plist>

View File

@@ -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)

View File

@@ -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'])