#!/bin/sh
# Claude Code Skill 설치 스크립트
# 생성: 2026-08-11 · Skill 1개 · 파일 1개
#
#   ./install-skills.sh            ~/.claude/skills/ 에 설치 (모든 프로젝트에서 사용)
#   ./install-skills.sh ./myrepo   ./myrepo/.claude/skills/ 에 설치 (그 저장소 전용)
set -e

DEST="${1:+$1/.claude/skills}"
DEST="${DEST:-$HOME/.claude/skills}"
mkdir -p "$DEST"
echo "설치 위치: $DEST"


mkdir -p "$DEST/medical-code-extract"
cat > "$DEST/medical-code-extract/SKILL.md" <<'SKILL_PAYLOAD_EOF'
---
name: medical-code-extract
description: 검사 판독문(경동맥·관상동맥 초음파/CT, PWV·ABI 등)의 결론 텍스트에서 질병코드와 수치를 추출한다. 규칙 기반 키워드 추출로 힌트를 만들어 LLM에 주입하고, LLM JSON 파싱이 실패하면 키워드 결과로 폴백하는 하이브리드. 부정문·중증도 처리 포함. TRIGGER - "질병코드 추출", "판독문에서 코드 뽑기", "경동맥/관상동맥/PWV/ABI 판정", "키워드+LLM 하이브리드", 검사 결론문을 코드로 자동 변환해야 할 때.
---

# 검사 판독문 → 질병코드 추출

## 언제 쓰나

건강검진·영상 검사의 **결론(conclusion) 텍스트**에서 질병코드나 수치 판정을 뽑을 때. 코드 체계가 정해져 있고(유효 코드 집합이 있음) 오답을 내면 안 되는 상황.

`[[pathology-llm-extract]]`가 자유 필드를 뽑는다면, 이쪽은 **닫힌 코드 집합**으로 분류한다 — 그래서 규칙 기반 폴백이 성립한다.

## 정본 코드

```
~/Documents/medical_code_extractor 2/
  keyword_extractor.py   규칙 기반 추출 (문장 단위 정밀 분석)
  llm_extractor.py       LLM + 힌트 주입 + 폴백
  coronary_extractor.py  관상동맥 전용 (Keyword/LLM 쌍 + 팩토리)
  pwv_abi_extractor.py   PWV·ABI 전용
  keywords/              carotid_keywords.py, coronary_keywords.py, pwv_abi_keywords.py
  prompts/               검사별 프롬프트 빌더
  ollama_client.py       온프렘 LLM
  excel_handler.py       입출력
  api_server.py          FastAPI
~/Documents/건강의학과/                검증 케이스 (120/140/199/206건 xlsx)
```

`건강의학과/medical_code_extractor`에도 사본이 있다. **`~/Documents/medical_code_extractor 2/`가 정본이다.**

## 핵심: 규칙이 힌트를 만들고 LLM이 판단한다

```
1) KeywordExtractor.extract(text)      → KeywordHints
2) format_hints_for_llm(hints)         → 힌트 텍스트
3) prompt_builder(text, hints_text)    → system/user 프롬프트
4) LLM 호출 → JSON 파싱
5) _postprocess_codes(codes, hints)    → 유효 코드 검증
6) JSON 파싱 3회 실패 → KeywordExtractor 결과로 폴백
```

**규칙 결과를 LLM에 넘기되 최종 판단은 LLM이 한다.** 규칙만 쓰면 표현 변형에 약하고, LLM만 쓰면 근거 없이 코드를 만든다. 그리고 **규칙 결과가 살아 있으므로 LLM이 무너져도 답을 낼 수 있다** — 이게 이 구조의 진짜 가치다.

## 규칙 추출기가 잡아야 하는 것

`VesselFinding`이 담는 필드가 곧 규칙 추출기의 요구사항이다:

```
vessel            어느 혈관/부위
severity          중증도 표현
severity_level    비교 가능한 숫자 등급
stenosis_percent  협착률 수치
context           근거 문장
negated           ★ 부정문 여부
```

`negated`가 가장 중요하다. **"no significant stenosis"를 협착으로 잡으면 전부 틀린다.** 의료 판독문은 정상 소견을 부정문으로 쓴다. 그래서 문서 전체가 아니라 **문장 단위로** 쪼개 분석한다.

`vessel_max_severity: Dict[str, VesselFinding]`로 혈관별 최고 중증도만 남긴다. 같은 혈관이 여러 문장에 나오면 가장 심한 것이 판정 기준이다.

## 폴백 정책

```python
max_json_retries = 3   # JSON 파싱 실패 재시도
```

3회 실패하면 `KeywordExtractor.get_disease_codes(hints)`로 폴백하고, `reasoning`에 `"KeywordExtractor fallback (JSON 파싱 3회 실패)"`를 남긴다. 폴백 코드도 없으면 `success=False`.

**폴백으로 나온 결과임을 반드시 표시해라.** 나중에 정확도를 볼 때 LLM 결과와 폴백 결과를 섞어 계산하면 안 된다.

`json_mode=False`로 호출하고 직접 파싱한다(`_parse_response_strict`). 온프렘 모델은 json_mode를 제대로 지원하지 않는 경우가 많다. `max_tokens=8192` — 4096에서 올렸다. reasoning 모델은 thinking에 토큰을 많이 쓴다.

## 검사 종류 추가하는 법

각 검사가 동일한 3종 세트를 가진다:

```
keywords/<검사>_keywords.py     위치·중증도 키워드 사전
prompts/<검사>_prompt.py        프롬프트 빌더
<검사>_extractor.py             KeywordExtractor + LLMExtractor + create_<검사>_extractor() 팩토리
```

`create_carotid_extractor(ollama_client)` 같은 팩토리로 조립한다. **키워드 사전과 프롬프트를 데이터로 분리했기 때문에 새 검사 추가가 파일 3개 추가로 끝난다.**

## 함정

- `valid_codes: Set[int]`로 유효 코드 집합을 강제한다. LLM이 존재하지 않는 코드를 만들어내면 `_postprocess_codes`에서 걸린다. **닫힌 코드 체계면 반드시 사후 검증해라.**
- 결론 텍스트만 넣는다. 판독문 전체를 넣으면 소견 부분의 언급이 결론과 충돌한다.
- `thinking` 필드를 따로 보관한다 — 온프렘 reasoning 모델 대응은 `[[llm-provider-switch]]` 참조.
- 검증 케이스가 `건강의학과/`에 xlsx로 있다(경동맥 120·170건, 관상동맥 140건, 199·206건). 규칙을 고치면 여기로 회귀 검증해라.

## 연관 skill

`[[pathology-llm-extract]]`, `[[llm-provider-switch]]`, `[[excel-case-validator]]`
SKILL_PAYLOAD_EOF

echo ""
echo "완료 — Skill 1개를 설치했습니다."
echo "Claude Code를 다시 시작하면 인식됩니다."
