개선관리 요구사항과 Google 로그인을 완료
This commit is contained in:
parent
cc0a15b7c6
commit
2a39636163
112 changed files with 10166 additions and 527 deletions
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue