G0~G8 성과·동맹 측정 OS 작업 일괄 고정
8월 7일까지 워킹트리에만 남아 있던 미커밋 작업을 커밋한다. 여러 사본 폴더(worktree·clone)에 흩어져 있던 중간 스냅샷을 정리하기 전에 원본을 git 이력으로 고정하는 것이 목적이다. - contracts/routes/services: measurement, outcome_trajectory, rupture_repair, deliberate_practice, calibration_transfer, supervision_research, multimodal_alliance, continuous_improvement 계열 신규 모듈과 테스트 - infra/db/init: 07~16 마이그레이션(측정 기반~calibration transfer 실행) - apps/web: 세션 리뷰 카드·관리 화면·E2E 스펙 추가 - docs/ops: G0~G8 라이브 통합·배포·롤백 증거 문서와 evidence JSON/PNG - scripts: smoke·ledger·릴리스 에이전트·NAS 프리뷰 운영 스크립트 engine.public 로그 .bak과 apps/web/test-results 산출물은 커밋에서 제외했다.
This commit is contained in:
parent
93dd8f82d7
commit
16e791e044
390 changed files with 243188 additions and 499 deletions
|
|
@ -40,7 +40,7 @@ from ..contracts.engine_gateway import (
|
|||
StreamErrorEvent,
|
||||
StreamTokenEvent,
|
||||
)
|
||||
from . import guardrail, persona, state_machine
|
||||
from . import guardrail, persona, rupture_scenario_director, state_machine
|
||||
from .llm_audit import LlmAuditHook, generate_with_audit, record_llm_audit
|
||||
from .persona import PersonaCard, PersonaStateContext, TurnMemory
|
||||
from .state_machine import SessionState
|
||||
|
|
@ -86,6 +86,8 @@ class TurnContext:
|
|||
memory: TurnMemory = field(default_factory=TurnMemory)
|
||||
# 회기 이론모드(학습자 선택: humanistic|cbt|integrative). 평가 이론부합·생성 프레이밍에 사용.
|
||||
theory_mode: Optional[str] = None
|
||||
# Scenario Director 내부 선택. ID/유형/provenance는 엔진 request metadata에만 존재한다.
|
||||
scenario_directive: Optional[rupture_scenario_director.ScenarioDirective] = None
|
||||
|
||||
def to_state_context(self) -> PersonaStateContext:
|
||||
st = self.state_after or self.state_before
|
||||
|
|
@ -134,6 +136,9 @@ def prepare_turn(
|
|||
memory: Optional[TurnMemory] = None,
|
||||
theory_mode: Optional[str] = None,
|
||||
eval_rapport_signal: Optional[float] = None,
|
||||
scenario_context: Optional[
|
||||
rupture_scenario_director.StoredScenarioContext
|
||||
] = None,
|
||||
) -> TurnContext:
|
||||
"""엔진 호출 전 결정론 전처리(1~3단계). 순수 — IO/LLM 없음.
|
||||
|
||||
|
|
@ -182,6 +187,17 @@ def prepare_turn(
|
|||
ideation_observed=crisis_ideation,
|
||||
)
|
||||
|
||||
ctx.scenario_directive = rupture_scenario_director.select_scenario_directive(
|
||||
case_id=ctx.case_id,
|
||||
session_id=ctx.session_id,
|
||||
turn_seq=ctx.state_after.turn_seq,
|
||||
safety_escalated=bool(ctx.crisis is not None and ctx.crisis.escalate),
|
||||
scenario_context=scenario_context,
|
||||
)
|
||||
hidden_behavior_cue = rupture_scenario_director.render_hidden_behavior_prompt(
|
||||
ctx.scenario_directive
|
||||
)
|
||||
|
||||
# 3) 페르소나 컨텍스트 — L0~L6 messages 조립 (CCD 는 행동으로만, L0 가 강제)
|
||||
ctx.messages = persona.build_turn_messages(
|
||||
card,
|
||||
|
|
@ -189,6 +205,7 @@ def prepare_turn(
|
|||
ctx.learner_text_masked,
|
||||
memory=ctx.memory,
|
||||
theory_mode=ctx.theory_mode,
|
||||
hidden_behavior_cue=hidden_behavior_cue,
|
||||
)
|
||||
return ctx
|
||||
|
||||
|
|
@ -221,6 +238,21 @@ def _latest_client_reply(turns: list[dict[str, str]]) -> Optional[str]:
|
|||
return None
|
||||
|
||||
|
||||
def _client_request_metadata(ctx: TurnContext) -> dict[str, Any]:
|
||||
assert ctx.state_after is not None
|
||||
metadata: dict[str, Any] = {"stage": ctx.state_after.stage.value}
|
||||
if ctx.scenario_directive is not None:
|
||||
metadata["scenario_director"] = ctx.scenario_directive.request_metadata()
|
||||
return metadata
|
||||
|
||||
|
||||
def _safe_engine_error_detail(error: BaseException | str, *, fallback: str) -> str:
|
||||
detail = str(error).strip() or fallback
|
||||
if rupture_scenario_director.contains_internal_scenario_leakage(detail):
|
||||
return fallback
|
||||
return detail
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# 4~8단계 — 동기 생성 경로 (폴백/테스트)
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
|
@ -246,16 +278,44 @@ async def run_turn_generate(
|
|||
ai_role="client",
|
||||
messages=ctx.messages,
|
||||
session_id=ctx.session_id,
|
||||
metadata={"stage": st.stage.value},
|
||||
metadata=_client_request_metadata(ctx),
|
||||
)
|
||||
previous_client_reply = _latest_client_reply(ctx.memory.recent_turns)
|
||||
resp: GenerateResponse | None = None
|
||||
reply = ""
|
||||
safety_flagged = ctx.crisis is not None and ctx.crisis.escalate
|
||||
for attempt in range(2):
|
||||
resp = await generate_with_audit(engine, req, audit_hook)
|
||||
try:
|
||||
resp = await generate_with_audit(engine, req, audit_hook)
|
||||
except EngineError as exc:
|
||||
detail = _safe_engine_error_detail(
|
||||
exc,
|
||||
fallback="engine generation failed",
|
||||
)
|
||||
if detail != str(exc):
|
||||
raise EngineError(detail) from exc
|
||||
raise
|
||||
|
||||
# 5) 출력 가드레일 — 수단 차단 + persona 품질 재생성
|
||||
if rupture_scenario_director.contains_internal_scenario_leakage(resp.text):
|
||||
if attempt == 0:
|
||||
continue
|
||||
return TurnResult(
|
||||
turn_seq=st.turn_seq,
|
||||
stage=st.stage.value,
|
||||
effective_openness=st.effective_openness,
|
||||
client_reply=None,
|
||||
safety_flagged=True,
|
||||
state_after=st,
|
||||
evaluation=None,
|
||||
crisis_kind=ctx.crisis.kind.value if ctx.crisis else "none",
|
||||
llm_provider=resp.provider,
|
||||
model=resp.model,
|
||||
tokens_in=resp.tokens_in,
|
||||
tokens_out=resp.tokens_out,
|
||||
cost_usd=resp.cost_usd,
|
||||
output_error="client_reply_quality_retryable",
|
||||
)
|
||||
guard = guardrail.sanitize_client_reply(
|
||||
resp.text,
|
||||
ideation_stage=st.ideation_stage,
|
||||
|
|
@ -360,7 +420,7 @@ async def run_turn_stream(
|
|||
ai_role="client",
|
||||
messages=ctx.messages,
|
||||
session_id=ctx.session_id,
|
||||
metadata={"stage": st.stage.value},
|
||||
metadata=_client_request_metadata(ctx),
|
||||
)
|
||||
|
||||
accumulated = ""
|
||||
|
|
@ -401,6 +461,10 @@ async def run_turn_stream(
|
|||
if packet.event == ENGINE_GATEWAY_SSE_ERROR:
|
||||
payload = packet.payload
|
||||
detail = payload.detail if isinstance(payload, StreamErrorEvent) else "engine stream error"
|
||||
detail = _safe_engine_error_detail(
|
||||
detail,
|
||||
fallback="engine stream error",
|
||||
)
|
||||
yield StreamEvent("error", {"detail": detail})
|
||||
return
|
||||
if packet.event == ENGINE_GATEWAY_SSE_DONE:
|
||||
|
|
@ -415,20 +479,23 @@ async def run_turn_stream(
|
|||
text_piece = payload.text
|
||||
accumulated += text_piece
|
||||
|
||||
scenario_leakage = rupture_scenario_director.contains_internal_scenario_leakage(
|
||||
accumulated
|
||||
)
|
||||
guard = guardrail.sanitize_client_reply(
|
||||
accumulated,
|
||||
ideation_stage=st.ideation_stage,
|
||||
turn_seq=st.turn_seq,
|
||||
previous_client_reply=previous_client_reply,
|
||||
)
|
||||
if guard.needs_regeneration:
|
||||
if scenario_leakage or guard.needs_regeneration:
|
||||
flagged = True
|
||||
output_error = "client_reply_quality_retryable"
|
||||
yield StreamEvent(
|
||||
"safety",
|
||||
{
|
||||
"reason": output_error,
|
||||
"reasons": guard.reasons,
|
||||
"reasons": guard.reasons if not scenario_leakage else ["quality"],
|
||||
},
|
||||
)
|
||||
accumulated = ""
|
||||
|
|
@ -475,9 +542,15 @@ async def run_turn_stream(
|
|||
},
|
||||
)
|
||||
except EngineGatewaySseDecodeError as e:
|
||||
yield StreamEvent("error", {"detail": str(e)})
|
||||
yield StreamEvent(
|
||||
"error",
|
||||
{"detail": _safe_engine_error_detail(e, fallback="engine stream decode error")},
|
||||
)
|
||||
except EngineError as e:
|
||||
yield StreamEvent("error", {"detail": str(e)})
|
||||
yield StreamEvent(
|
||||
"error",
|
||||
{"detail": _safe_engine_error_detail(e, fallback="engine stream error")},
|
||||
)
|
||||
|
||||
|
||||
def _optional_str(value: Any) -> Optional[str]:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue