"""Persona draft generation contract helpers.""" from __future__ import annotations import hashlib from typing import Any from .contracts.engine_gateway import GenerateResponse, structured_payload_from_response from .persona_read_model import PersonaDraftGenerateRequest, PersonaDraftPayload PERSONA_DRAFT_PROMPT_BUNDLE_ID = "persona-draft-rag" PERSONA_DRAFT_PROMPT_BUNDLE_VERSION = "2026-06-28.1" PERSONA_DRAFT_SYSTEM_PROMPT = ( "출력은 반드시 structured_schema를 따른다. code는 P숫자 형식을 선호하되 " "힌트가 없으면 빈 문자열 대신 임시값 P로 둔다. source_provenance에는 " "RAG source_id와 첨부 근거 기반 초안임을 남긴다. evidence chunk id를 " "임상 필드 본문에 그대로 노출하지 않는다." ) PERSONA_DRAFT_USER_PROMPT_PREAMBLE = ( "너는 Vignette 임상 콘텐츠 저작 보조자다. 아래 RAG 근거 청크만 바탕으로 교육용 " "가상내담자 페르소나 초안을 만든다. 첨부 원문은 KB 문서가 SSOT이며, 근거 밖 내용을 " "임의로 꾸며 핵심 임상 정보처럼 쓰지 않는다. 실제 개인정보는 이미 마스킹됐으며, " "원문 표현을 복사하지 말고 " "범주화·합성화된 임상 훈련용 설정으로 변환한다. CCD/DSM/역린은 런타임 내부 설정이므로 " "내담자 발화에 직접 노출되지 않는 형태로 작성한다." ) def persona_draft_prompt_bundle() -> dict[str, str]: payload = "\n".join( [ PERSONA_DRAFT_PROMPT_BUNDLE_ID, PERSONA_DRAFT_PROMPT_BUNDLE_VERSION, PERSONA_DRAFT_SYSTEM_PROMPT, PERSONA_DRAFT_USER_PROMPT_PREAMBLE, ] ) return { "id": PERSONA_DRAFT_PROMPT_BUNDLE_ID, "version": PERSONA_DRAFT_PROMPT_BUNDLE_VERSION, "hash": hashlib.sha256(payload.encode("utf-8")).hexdigest()[:12], } def persona_generation_schema() -> dict[str, Any]: return { "type": "object", "additionalProperties": False, "properties": { "draft": { "type": "object", "additionalProperties": False, "properties": { "code": {"type": "string"}, "display_name": {"type": "string"}, "difficulty": {"type": "string", "enum": ["easy", "moderate", "hard"]}, "theory_target": {"type": "array", "items": {"type": "string"}}, "demographics": {"type": "object"}, "presenting": {"type": "object"}, "history": {"type": "object"}, "big5": {"type": "object"}, "resistance": {"type": "object"}, "speech_style": {"type": "object"}, "affect_baseline": {"type": "object"}, "ccd": {"type": "object"}, "dsm5_dimensional": {"type": "object"}, "triggers": {"type": "object"}, "source_provenance": {"type": "string"}, "is_synthetic": {"type": "boolean"}, }, "required": [ "code", "display_name", "difficulty", "theory_target", "demographics", "presenting", "history", "big5", "resistance", "speech_style", "affect_baseline", "ccd", "dsm5_dimensional", "triggers", "source_provenance", "is_synthetic", ], }, "source_summary": {"type": "string"}, "warnings": {"type": "array", "items": {"type": "string"}}, }, "required": ["draft", "source_summary", "warnings"], } def persona_generation_payload_from_response(response: GenerateResponse) -> dict[str, Any]: return structured_payload_from_response(response) or {} def coerce_persona_generated_draft( payload: dict[str, Any], request: PersonaDraftGenerateRequest, ) -> PersonaDraftPayload: raw = payload.get("draft") if isinstance(payload.get("draft"), dict) else payload if not isinstance(raw, dict): raw = {} theory_target = raw.get("theory_target") theory_values = ( [str(item).strip().lower() for item in theory_target if str(item).strip()] if isinstance(theory_target, list) else [value.strip().lower() for value in request.theory_target if value.strip()] ) code = str(raw.get("code") or request.code_hint or "").strip().upper() display_name = str(raw.get("display_name") or request.display_name_hint or "자료 기반 새 페르소나").strip() difficulty = str(raw.get("difficulty") or request.difficulty) if difficulty not in {"easy", "moderate", "hard"}: difficulty = request.difficulty return PersonaDraftPayload( code=code or "P", display_name=display_name, difficulty=difficulty, # type: ignore[arg-type] theory_target=theory_values or ["humanistic"], demographics=_json_object(raw.get("demographics")), presenting=_json_object(raw.get("presenting")), history=_json_object(raw.get("history")), big5=_float_dict(raw.get("big5")) or {"O": 0.5, "C": 0.5, "E": 0.5, "A": 0.5, "N": 0.5}, resistance=_float_dict(raw.get("resistance")) or { "base_resistance": 0.5, "unlock_rate": 0.1, "decay_floor": 0.05, "silence_prob": 0.15, "deflection_prob": 0.25, }, speech_style=_json_object(raw.get("speech_style")), affect_baseline=_float_dict(raw.get("affect_baseline")) or { "negative_affect": 0.45, "hopelessness": 0.2, "anhedonia": 0.2, "sleep": 0.2, "anxiety": 0.35, "suicide_ideation_stage": 1, }, ccd=_json_object(raw.get("ccd")), dsm5_dimensional=_json_object(raw.get("dsm5_dimensional")), triggers=_json_object(raw.get("triggers")), source_provenance=str(raw.get("source_provenance") or f"masked {request.source_kind}"), is_synthetic=bool(raw.get("is_synthetic", True)), submit_for_review=False, ) def _float_dict(value: Any) -> dict[str, float]: if not isinstance(value, dict): return {} result: dict[str, float] = {} for key, item in value.items(): if isinstance(item, (int, float)): result[str(key)] = float(item) return result def _json_object(value: Any) -> dict[str, Any]: return value if isinstance(value, dict) else {}