c2077b3108
plan ds-presegment-mapreduce-2. TRIGGER(25K tok) 이하 = 기존 단일콜 byte-불변 무회귀. 초과 시 3-way over% 게이트: auto=유닛별 map(26B)→reduce(26B, p3c_deep_summary_reduce 변형) → ai_detail_summary 동일 기록(불일치=reduce+map 합본 dedup) / hybrid·whole= HOLD(payload.presegment.awaiting_split + StageDeferred 24h, 맥미니 미전송 — 알람· 클로드 유인 분할은 PR3). - 유닛 단위 멱등 재개: 성공 유닛 즉시 payload.map_results commit — 502/defer/재시작 후 완료 유닛 skip, 실패 유닛만 raise→기존 attempts/백오프 재사용 - 모든 LLM 콜 캡(12K tok) 이하 — map=greedy-pack 보장, reduce=build_reduce_units_block 비례 절단 보장, est_tokens 로그로 단정 가능 - 콜 사이 gate 해제 → 짧은 인터랙티브 요청 interleave (허브 굶김 해소 본체) - fix: summarize_units 의 `from app.services...` 절대 import — 컨테이너(빌드 컨텍스트 ./app)에 app 패키지가 없어 배선 시 ModuleNotFoundError 나는 PR1 잠복 버그 → 상대 import 로 수정 (컨테이너/repo-root 테스트 양쪽 동작) - tests: 헬퍼 6 + worker seam 5 (map-reduce e2e·재개·유닛실패·drain 보류·HOLD) — PR1 15 포함 26 passed, 인접 policy/hier_decomp/fair_share 123 passed Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
156 lines
5.1 KiB
Python
156 lines
5.1 KiB
Python
"""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",
|
|
# presegment PR2 — 거대문서 map-reduce 의 reduce 단계 (요약들의 요약)
|
|
"p3c_deep_summary_reduce",
|
|
"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()
|