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:
24
launchd/net.hyungi.pkm-api.plist
Normal file
24
launchd/net.hyungi.pkm-api.plist
Normal 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>
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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'])
|
||||
|
||||
Reference in New Issue
Block a user