"""Prompt rendering — yaml excerpt 를 template placeholder 에 주입. 템플릿에는 다음 placeholder 가 있다: {forbidden_block} — subject 별 forbidden_for_4b 블록 주입 {subject_description} — subject_domains[domain].description {confidence_threshold} — escalation.confidence_threshold {context_cap} — escalation.context_char_cap_4b {context_cap_doc_count} — P6 전용 (batch 문서 수 cap, 기본 500) policy_version() = sha256(yaml_bytes + template_bytes)[:12]. yaml 또는 template 이 바뀌면 자동 bump → analyze_events.policy_version 으로 추적. """ from __future__ import annotations import hashlib from functools import lru_cache from pathlib import Path from policy.loader import load_policy, read_policy_bytes from policy.schema import DomainPolicy # 기본 템플릿 경로 — repo root 기준 TEMPLATE_DIR = Path(__file__).resolve().parent.parent / "prompts" / "policy" # 4B / 26B 구분 (관측성 + 테스트 편의) KNOWN_4B_TASKS = { "p1_triage", "p2_nas_rule", "p3a_short_summary", "p3b_entities", "p4a_advice_trigger", "p4b_retrieval", "p6_night_sweep", } KNOWN_26B_TASKS = { "p3c_deep_summary", "p4b_synthesis", } def _template_path(task: str) -> Path: return TEMPLATE_DIR / f"{task}.txt" @lru_cache(maxsize=64) def _read_template(task: str) -> str: path = _template_path(task) if not path.exists(): raise FileNotFoundError(f"policy template '{task}' not found at {path}") return path.read_text(encoding="utf-8") @lru_cache(maxsize=64) def _read_template_bytes(task: str) -> bytes: return _template_path(task).read_bytes() def _forbidden_block_for( policy: DomainPolicy, subject_domain: str ) -> str: """해당 도메인에 적용되는 forbidden_for_4b 규칙을 프롬프트 블록으로 렌더.""" lines = ["=== 4B 절대 금지 작업 ===", "다음에 해당하면 자체 답변 금지, escalate_to_26b=true + envelope 만 응답.", ""] count = 0 for rule in policy.forbidden_for_4b: if subject_domain in rule.applies_when_subject_in: count += 1 lines.append(f"{count}. [{rule.id}] {rule.description}") if count == 0: lines.append("(해당 도메인에 등록된 금지 항목 없음 — 일반 규칙만 적용)") lines.append("") lines.append("금지 위반 시 사후 audit (check_4b_output_violations) 에서 탐지되어") lines.append("policy_violation=true 로 기록 + under_escalation 큐로 재처리.") return "\n".join(lines) def render_4b( task: str, subject_domain: str, *, policy: DomainPolicy | None = None, ) -> str: """4B 용 템플릿에 정책 excerpt 를 주입하고 반환. 사용자 input placeholder ({{filename}}, {{extracted_text}} 등, 이중중괄호) 는 그대로 남는다. PR-B 의 worker 가 str.format 또는 Template 으로 최종 주입. """ if task not in KNOWN_4B_TASKS: raise ValueError(f"'{task}' is not a 4B task (known: {KNOWN_4B_TASKS})") if policy is None: policy = load_policy() template = _read_template(task) domain_spec = ( policy.subject_domains.get(subject_domain) or policy.fallback_domain ) return template.format( forbidden_block=_forbidden_block_for(policy, subject_domain), subject_description=domain_spec.description, confidence_threshold=policy.escalation.confidence_threshold, context_cap=policy.escalation.context_char_cap_4b, context_cap_doc_count=500, ) def render_26b( task: str, subject_domain: str, *, policy: DomainPolicy | None = None, ) -> str: """26B 용 템플릿 렌더.""" if task not in KNOWN_26B_TASKS: raise ValueError(f"'{task}' is not a 26B task (known: {KNOWN_26B_TASKS})") if policy is None: policy = load_policy() template = _read_template(task) domain_spec = ( policy.subject_domains.get(subject_domain) or policy.fallback_domain ) return template.format( forbidden_block=_forbidden_block_for(policy, subject_domain), subject_description=domain_spec.description, confidence_threshold=policy.escalation.confidence_threshold, context_cap=policy.escalation.context_char_cap_26b, context_cap_doc_count=500, ) def policy_version(task: str, *, policy_path: str | None = None) -> str: """Return sha256(yaml_bytes + template_bytes)[:12]. Deterministic — 같은 (yaml, template) → 같은 hash. 한 쪽만 변경돼도 변경됨. analyze_events.policy_version 에 저장되어 drift 추적. """ yaml_bytes = read_policy_bytes(policy_path) template_bytes = _read_template_bytes(task) h = hashlib.sha256(yaml_bytes + template_bytes).hexdigest() return h[:12] def clear_cache() -> None: """테스트용 — 템플릿 재읽기.""" _read_template.cache_clear() _read_template_bytes.cache_clear()