여러 줄 주석이 설명보다 경위(예전·실측·지적)를 적고 있어 읽는 사람이 결론을 찾기 어려웠다. - ts·tsx·js·mjs·css·py 478개: 여러 줄 주석은 첫 문장 한 줄로, 과거형·날짜 문장은 삭제 - 주석 위치는 TypeScript 파서·파이썬 tokenize/ast 로 찾는다 — 문자열 안의 # · /* 는 건드리지 않는다 - eslint·ts·noqa·type: ignore 같은 지시 주석은 그대로 둔다 파이썬 275개 정리 전후 AST 동일, TS 298개 주석 뺀 토큰 동일(빈 JSX 주석 10곳만 차이). site·frontend·admin tsc, site vitest 105 passed Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
85 lines
4.1 KiB
Python
85 lines
4.1 KiB
Python
"""원문 텍스트 → 업종 스키마 fact 추출 프롬프트·응답 스키마."""
|
|
from common.category_schema import get_schema
|
|
from common.enums import PlaceCategory
|
|
|
|
# Gemini responseSchema(OpenAPI subset).
|
|
RESPONSE_SCHEMA = {
|
|
"type": "object",
|
|
"properties": {
|
|
"facts": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"key": {"type": "string", "description": "아래 필드 목록의 key 중 하나. 목록에 없는 key 는 절대 쓰지 않는다"},
|
|
"value": {"type": "string", "description": "원문 표기 그대로. bool 필드는 'true' 또는 'false'"},
|
|
"unit_name": {"type": "string", "description": "scope=unit 필드일 때 어느 객실·메뉴·프로그램인지. place 스코프면 빈 문자열"},
|
|
"evidence": {"type": "string", "description": "이 값의 근거가 된 원문 문장을 **글자 그대로** 복사. 요약·수정 금지"},
|
|
},
|
|
"required": ["key", "value", "evidence"],
|
|
},
|
|
}
|
|
},
|
|
"required": ["facts"],
|
|
}
|
|
|
|
_RULES = """규칙
|
|
1. 아래 [필드 목록]에 있는 key 만 쓴다. 목록에 없는 key 는 버린다.
|
|
2. value 는 원문 표기를 **그대로** 옮긴다. 단위 환산·반올림·요약·번역을 하지 않는다.
|
|
- "오후 3시" 를 "15:00" 으로 바꾸지 않는다. 원문이 "오후 3시" 면 "오후 3시" 다.
|
|
3. evidence 에는 그 값이 적힌 **원문 문장을 글자 그대로 복사**한다.
|
|
- 원문에 없는 문장을 쓰면 그 항목은 버려진다.
|
|
- 여러 문장에 걸쳐 있으면 값이 실제로 적힌 한 문장만 고른다.
|
|
4. 원문에 없는 것은 만들지 않는다. 모르면 그 key 를 빼면 된다. 빈 값으로 채우지 않는다.
|
|
- "보통 펜션은 3시 체크인" 같은 일반 상식으로 채우지 않는다.
|
|
5. type=bool 필드는 value 를 "true" 또는 "false" 로만 쓴다.
|
|
- 원문에 "반려동물 동반 불가" 라고 있으면 pet_allowed = "false" 다. 부정 표현을 놓치지 않는다.
|
|
- 원문에 언급이 아예 없으면 그 key 를 **쓰지 않는다**. "없다"고 단정하지 않는다.
|
|
6. type=number 필드는 숫자만 쓴다(단위·쉼표 없이). 원문이 "최대 4인" 이면 "4" 다.
|
|
7. scope=unit 필드는 unit_name 을 반드시 채운다. 같은 객실·메뉴의 항목은 unit_name 을 똑같이 쓴다.
|
|
- unit_name 은 원문에 적힌 이름 그대로. "A동", "커플룸", "김치찌개".
|
|
8. 홍보 문구·후기·다른 업소 이야기는 근거로 쓰지 않는다.
|
|
- "최고의 전망", "인생 맛집" 같은 표현은 사실이 아니다.
|
|
9. 같은 key 가 원문에서 여러 번 다르게 적혀 있으면, 가장 구체적이고 최신인 한 건만 고른다.
|
|
"""
|
|
|
|
|
|
def _field_lines(category: PlaceCategory) -> str:
|
|
"""업종 스키마를 그대로 프롬프트에 편다."""
|
|
schema = get_schema(category)
|
|
lines = []
|
|
for spec in schema.fields.values():
|
|
# allow_llm 필드(소개문 등)는 목록에 넣지 않는다 — LLM 이 **쓰는** 칸이지 원문에서 옮겨 담는 사실이 아니다.
|
|
if spec.allow_llm:
|
|
continue
|
|
bits = [f"- {spec.key} ({spec.label})", f"type={spec.type}", f"scope={spec.scope}"]
|
|
if spec.unit:
|
|
bits.append(f"단위={spec.unit}")
|
|
if spec.critical:
|
|
# 틀리면 예약 클레임이 나는 항목.
|
|
bits.append("※중요-확실할 때만")
|
|
lines.append(" · ".join(bits))
|
|
return "\n".join(lines)
|
|
|
|
|
|
def build_prompt(place_name: str, category: PlaceCategory, source_text: str, *, max_chars: int = 30000) -> str:
|
|
"""추출 프롬프트."""
|
|
schema = get_schema(category)
|
|
text = (source_text or "").strip()[:max_chars]
|
|
|
|
return f"""너는 사업장 정보를 **원문에서 찾아 옮겨 적는** 추출기다. 글을 쓰는 것이 아니다.
|
|
|
|
[사업장] {place_name} (업종: {schema.label})
|
|
|
|
{_RULES}
|
|
|
|
[필드 목록]
|
|
{_field_lines(category)}
|
|
|
|
[원문]
|
|
\"\"\"
|
|
{text}
|
|
\"\"\"
|
|
|
|
원문에서 찾은 항목만 facts 배열에 담아라. 찾은 게 없으면 빈 배열을 반환한다."""
|