개선관리 요구사항과 Google 로그인을 완료

This commit is contained in:
Yun Chan 2026-08-28 16:07:09 +09:00
parent cc0a15b7c6
commit 2a39636163
112 changed files with 10166 additions and 527 deletions

View file

@ -153,6 +153,8 @@ class LiveCoachGrounding(BaseModel):
source_type: Optional[str] = None
version: Optional[str] = None
citation: Optional[str] = None
license_class: Literal["A", "B", "C", "D"] | None = None
external_llm_ok: bool = False
summary: str
@ -388,7 +390,8 @@ def _local_reference_grounding(item: LiveCoachInput, *, limit: int = 5) -> list[
fallback: list[tuple[int, dict[str, Any], dict[str, Any]]] = []
for source_index, payload in enumerate(_local_source_payloads()):
source = payload.get("source") or {}
if source.get("external_llm_ok") is False:
license_class = str(source.get("license_class") or "").strip().upper()
if source.get("external_llm_ok") is not True or license_class not in {"A", "B"}:
continue
for chunk_index, chunk in enumerate(payload.get("chunks") or []):
if not isinstance(chunk, dict):
@ -404,6 +407,9 @@ def _local_reference_grounding(item: LiveCoachInput, *, limit: int = 5) -> list[
scored.sort(key=lambda pair: (pair[0], pair[1]), reverse=True)
out: list[LiveCoachGrounding] = []
for _, _, source, chunk in scored[:limit]:
source_license_class = str(source.get("license_class") or "").strip().upper()
if source_license_class not in {"A", "B"}:
continue
out.append(
LiveCoachGrounding(
source_id=str(chunk.get("source_id") or source.get("source_id") or "live_coaching_source"),
@ -413,12 +419,26 @@ def _local_reference_grounding(item: LiveCoachInput, *, limit: int = 5) -> list[
source_type=str(chunk.get("source_type") or source.get("source_type") or "") or None,
version=str(chunk.get("version") or source.get("version") or "") or None,
citation=str(chunk.get("citation") or source.get("citation") or "") or None,
license_class=source_license_class,
external_llm_ok=source.get("external_llm_ok") is True,
summary=str(chunk.get("summary") or ""),
)
)
return out
def _external_safe_grounding(
grounding: list[LiveCoachGrounding],
) -> list[LiveCoachGrounding]:
"""외부 엔진 요청에 명시적으로 허용된 A/B 근거만 전달한다."""
return [
item
for item in grounding
if item.external_llm_ok is True and item.license_class in {"A", "B"}
]
def _source_refs(grounding: list[LiveCoachGrounding]) -> list[LiveCoachSource]:
seen: set[tuple[str, str | None]] = set()
refs: list[LiveCoachSource] = []
@ -478,7 +498,30 @@ def _clip(value: Any, limit: int) -> Optional[str]:
return text if len(text) <= limit else text[: limit - 1].rstrip() + ""
def _coerce_payload(payload: dict[str, Any], *, sources: list[LiveCoachSource], latency_ms: int) -> LiveCoachSuggestion:
def _mask_generated_text(
value: Any,
*,
client_identity: str | None,
limit: int,
) -> Optional[str]:
text = str(value or "").strip()
if not text:
return None
masked = guardrail.mask_role_identities(
text,
client_identity=client_identity,
synthetic_generated=True,
).text_masked
return _clip(masked, limit)
def _coerce_payload(
payload: dict[str, Any],
*,
sources: list[LiveCoachSource],
latency_ms: int,
client_identity: str | None = None,
) -> LiveCoachSuggestion:
tone = str(payload.get("tone") or "neutral")
if tone not in ("pos", "warn", "neutral"):
tone = "neutral"
@ -500,11 +543,25 @@ def _coerce_payload(payload: dict[str, Any], *, sources: list[LiveCoachSource],
status="ready",
tone=tone, # type: ignore[arg-type]
focus=focus, # type: ignore[arg-type]
title=_clip(payload.get("title"), 32) or "다음 발화 조정",
message=_clip(payload.get("message"), 120) or "지금은 내담자 말을 더 구체적으로 따라가는 편이 낫다.",
next_utterance=_clip(payload.get("next_utterance"), 140),
rationale=_clip(payload.get("rationale"), 180),
safety_note=_clip(payload.get("safety_note"), 120),
title=_mask_generated_text(
payload.get("title"), client_identity=client_identity, limit=32
)
or "다음 발화 조정",
message=_mask_generated_text(
payload.get("message"), client_identity=client_identity, limit=120
)
or "지금은 내담자 말을 더 구체적으로 따라가는 편이 낫다.",
next_utterance=_mask_generated_text(
payload.get("next_utterance"),
client_identity=client_identity,
limit=140,
),
rationale=_mask_generated_text(
payload.get("rationale"), client_identity=client_identity, limit=180
),
safety_note=_mask_generated_text(
payload.get("safety_note"), client_identity=client_identity, limit=120
),
sources=sources,
latency_ms=latency_ms,
)
@ -672,7 +729,8 @@ def _messages(item: LiveCoachInput, grounding: list[LiveCoachGrounding]) -> list
goals = ", ".join(item.goal_stages) if item.goal_stages else "(미지정)"
prior = (
"\n".join(
f"- {entry.get('title', '')} (focus: {entry.get('focus', '')})"
f"- {_mask_generated_text(entry.get('title'), client_identity=item.persona_name, limit=80) or ''} "
f"(focus: {entry.get('focus', '')})"
for entry in item.prior_coach[-2:]
if entry.get("title")
)
@ -685,7 +743,7 @@ def _messages(item: LiveCoachInput, grounding: list[LiveCoachGrounding]) -> list
)
user = (
f"[세션] {item.session_id} / turn {item.turn_seq}\n"
f"[내담자] {item.persona_name} ({item.persona_code})\n"
f"[내담자] [CLIENT] ({item.persona_code})\n"
f"[단계] {item.stage} / openness {item.effective_openness:.2f} / 이론 {item.theory_mode}\n"
f"[이번 회기 목표 단계] {goals} — 코칭은 목표 단계 작업에 정렬하고, 목표를 이미 이뤘다면 심화를 제안한다.\n"
f"[직전 코칭]\n{prior}\n(같은 조언을 반복하지 말고 다음 단계를 제시한다)\n\n"
@ -726,7 +784,7 @@ async def generate_live_coaching(
) -> LiveCoachSuggestion:
"""턴 직후 라이브 코칭을 생성한다. 실패해도 규칙 기반 제안으로 반환한다."""
local_grounding = _local_reference_grounding(item)
all_grounding = [*local_grounding, *(grounding or [])]
all_grounding = _external_safe_grounding([*local_grounding, *(grounding or [])])
crisis = guardrail.classify_crisis(item.learner_text)
if crisis.escalate:
return _fallback_suggestion(item, grounding=all_grounding, status="ready")
@ -769,7 +827,12 @@ async def generate_live_coaching(
grounding=all_grounding,
reason="구조화 출력을 반환하지 않았다",
)
return _coerce_payload(payload, sources=_source_refs(all_grounding), latency_ms=latency_ms)
return _coerce_payload(
payload,
sources=_source_refs(all_grounding),
latency_ms=latency_ms,
client_identity=item.persona_name,
)
except EngineError as exc:
return _fallback_suggestion(item, grounding=all_grounding, reason=str(exc))
except Exception as exc: