런타임 계약과 학습자 흐름 보강
This commit is contained in:
parent
f456b8997a
commit
206018b088
56 changed files with 4306 additions and 1008 deletions
|
|
@ -37,6 +37,21 @@ _COUNSELING_AGREEMENT_WITHDRAWAL_RE = re.compile(
|
|||
r"더\s*이상.*(상담|회기).*(안\s*하|하지\s*않|못\s*하)"
|
||||
)
|
||||
_DIGEST_SPEAKERS = {"counselor", "client"}
|
||||
_LLM_DIGEST_MIN_CHARS = 60
|
||||
_LLM_DIGEST_INTERNAL_MARKERS = (
|
||||
"rapport_credit",
|
||||
"effective_openness",
|
||||
"end_state",
|
||||
"evaluation",
|
||||
"정답",
|
||||
"점수",
|
||||
"평가 payload",
|
||||
"평가점수",
|
||||
"평가 점수",
|
||||
"core_belief",
|
||||
"CCD",
|
||||
)
|
||||
_SESSION_DIGEST_PREFIX_RE = re.compile(r"^S(?P<session_no>\d+):")
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
|
@ -139,7 +154,7 @@ class SessionDigestInput:
|
|||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class SessionDigestResult:
|
||||
"""Digest writer output contract shared by fallback and future LLM worker."""
|
||||
"""Digest writer output contract shared by fallback and LLM worker paths."""
|
||||
|
||||
session_id: str
|
||||
case_id: str | None
|
||||
|
|
@ -149,6 +164,35 @@ class SessionDigestResult:
|
|||
source: Literal["fallback", "llm"] = "fallback"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DigestQualityAssessment:
|
||||
"""Local quality gate result before an LLM digest can replace fallback."""
|
||||
|
||||
accepted: bool
|
||||
reason: Literal[
|
||||
"ok",
|
||||
"empty",
|
||||
"too_short",
|
||||
"forbidden_substring",
|
||||
"internal_marker",
|
||||
"wrong_session_prefix",
|
||||
]
|
||||
retryable: bool = False
|
||||
details: tuple[str, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class SessionDigestWorkerOutcome:
|
||||
"""Accepted LLM digest result or the reason fallback must remain authoritative."""
|
||||
|
||||
result: SessionDigestResult | None
|
||||
quality: DigestQualityAssessment
|
||||
|
||||
@property
|
||||
def fallback_required(self) -> bool:
|
||||
return self.result is None
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class CompressionJob:
|
||||
"""회기종료 narrative 압축 작업(LLM, 비동기 비블로킹). 큐에 적재될 페이로드.
|
||||
|
|
@ -305,6 +349,101 @@ def build_compression_messages(job: CompressionJob) -> list[dict[str, str]]:
|
|||
return [{"role": "system", "content": system}, {"role": "user", "content": user}]
|
||||
|
||||
|
||||
def _normalize_digest_text(value: Any) -> str:
|
||||
return " ".join(str(value or "").split())
|
||||
|
||||
|
||||
def assess_llm_digest_quality(
|
||||
digest_input: SessionDigestInput,
|
||||
digest: Any,
|
||||
*,
|
||||
forbidden_substrings: tuple[str, ...] = (),
|
||||
min_chars: int = _LLM_DIGEST_MIN_CHARS,
|
||||
) -> DigestQualityAssessment:
|
||||
"""Validate an LLM digest candidate without using raw text or end-state data."""
|
||||
|
||||
text = _normalize_digest_text(digest)
|
||||
if not text:
|
||||
return DigestQualityAssessment(accepted=False, reason="empty", retryable=True)
|
||||
|
||||
prefix_match = _SESSION_DIGEST_PREFIX_RE.match(text)
|
||||
if prefix_match and int(prefix_match.group("session_no")) != digest_input.session_no:
|
||||
return DigestQualityAssessment(
|
||||
accepted=False,
|
||||
reason="wrong_session_prefix",
|
||||
retryable=True,
|
||||
details=(prefix_match.group(0),),
|
||||
)
|
||||
|
||||
forbidden_hits = tuple(
|
||||
marker
|
||||
for marker in (str(item).strip() for item in forbidden_substrings)
|
||||
if marker and marker in text
|
||||
)
|
||||
if forbidden_hits:
|
||||
return DigestQualityAssessment(
|
||||
accepted=False,
|
||||
reason="forbidden_substring",
|
||||
retryable=True,
|
||||
details=forbidden_hits[:5],
|
||||
)
|
||||
|
||||
lowered = text.lower()
|
||||
marker_hits = tuple(
|
||||
marker
|
||||
for marker in _LLM_DIGEST_INTERNAL_MARKERS
|
||||
if marker.lower() in lowered
|
||||
)
|
||||
if marker_hits:
|
||||
return DigestQualityAssessment(
|
||||
accepted=False,
|
||||
reason="internal_marker",
|
||||
retryable=True,
|
||||
details=marker_hits,
|
||||
)
|
||||
|
||||
if len(text) < max(1, int(min_chars)):
|
||||
return DigestQualityAssessment(accepted=False, reason="too_short", retryable=True)
|
||||
|
||||
return DigestQualityAssessment(accepted=True, reason="ok")
|
||||
|
||||
|
||||
def build_llm_digest_worker_outcome(
|
||||
digest_input: SessionDigestInput,
|
||||
digest: Any,
|
||||
*,
|
||||
forbidden_substrings: tuple[str, ...] = (),
|
||||
min_chars: int = _LLM_DIGEST_MIN_CHARS,
|
||||
) -> SessionDigestWorkerOutcome:
|
||||
"""Coerce a candidate LLM digest into the shared result contract if it passes."""
|
||||
|
||||
quality = assess_llm_digest_quality(
|
||||
digest_input,
|
||||
digest,
|
||||
forbidden_substrings=forbidden_substrings,
|
||||
min_chars=min_chars,
|
||||
)
|
||||
if not quality.accepted:
|
||||
return SessionDigestWorkerOutcome(result=None, quality=quality)
|
||||
|
||||
normalized = _normalize_digest_text(digest)
|
||||
prefix = f"S{digest_input.session_no}:"
|
||||
if not normalized.startswith(prefix):
|
||||
normalized = f"{prefix} {normalized}"
|
||||
|
||||
return SessionDigestWorkerOutcome(
|
||||
result=SessionDigestResult(
|
||||
session_id=digest_input.session_id,
|
||||
case_id=digest_input.case_id,
|
||||
session_no=digest_input.session_no,
|
||||
digest=normalized,
|
||||
open_threads=digest_input.open_threads,
|
||||
source="llm",
|
||||
),
|
||||
quality=quality,
|
||||
)
|
||||
|
||||
|
||||
def _compact(value: Any, *, limit: int = _SESSION_DIGEST_EXCERPT_CHARS) -> str:
|
||||
text = " ".join(str(value or "").split())
|
||||
if len(text) <= limit:
|
||||
|
|
@ -518,9 +657,13 @@ __all__ = [
|
|||
"MaskedDigestTurn",
|
||||
"SessionDigestInput",
|
||||
"SessionDigestResult",
|
||||
"DigestQualityAssessment",
|
||||
"SessionDigestWorkerOutcome",
|
||||
"make_carry_over",
|
||||
"build_session_digest_input",
|
||||
"build_compression_messages",
|
||||
"assess_llm_digest_quality",
|
||||
"build_llm_digest_worker_outcome",
|
||||
"build_fallback_digest_result",
|
||||
"build_fallback_session_digest",
|
||||
"PinnedFactCandidate",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue