한신대 피드백 개선팩 반영

This commit is contained in:
Yun Chan 2026-07-03 19:53:14 +09:00
parent 5a9c110c11
commit 6b6241f468
25 changed files with 1247 additions and 94 deletions

View file

@ -92,9 +92,11 @@ class CaptureGenerateEngine:
self.payload: dict[str, Any] | None = None
self._payload_builder = EngineClient(base_url="http://engine.test")
self.text = text
self.requests: list[Any] = []
async def generate(self, req):
self.request = req
self.requests.append(req)
self.payload = self._payload_builder._payload(req)
return GenerateResponse(
text=self.text,
@ -283,6 +285,26 @@ class OrchestratorMaskingGateTest(unittest.IsolatedAsyncioTestCase):
self.assertIn("자동적 사고", blob)
self.assertIn("행동활성화", blob)
def test_prepare_turn_maps_recent_turns_from_client_ai_perspective(self) -> None:
ctx = orchestrator.prepare_turn(
session_id="history-session",
case_id="history-case",
card=persona.P1,
state=_initial_state(),
learner_text="그 말을 듣고 어떤 생각이 들었나요?",
memory=orchestrator.TurnMemory(
recent_turns=[
{"speaker": "counselor", "text": "왜 상담에 오게 됐나요?"},
{"speaker": "client", "text": "엄마가 가보라고 해서요."},
],
),
)
recent = ctx.messages[-3:-1]
self.assertEqual([message.role for message in recent], ["user", "assistant"])
self.assertEqual(recent[0].content, "왜 상담에 오게 됐나요?")
self.assertEqual(recent[1].content, "엄마가 가보라고 해서요.")
async def test_run_turn_generate_sends_only_masked_engine_payload(self) -> None:
ctx = _prepare_context()
engine = CaptureGenerateEngine()
@ -395,6 +417,78 @@ class OrchestratorMaskingGateTest(unittest.IsolatedAsyncioTestCase):
self.assertNotIn("[NAME]", result.client_reply or "")
self.assertIn("그 이름은 그대로고", result.client_reply or "")
async def test_run_turn_generate_retries_once_after_role_meta_reply(self) -> None:
class SequenceEngine(CaptureGenerateEngine):
def __init__(self) -> None:
super().__init__("")
self.responses = [
"내담자 역할로 응답하겠습니다. 엄마가 가보라고 해서요.",
"엄마가 그냥 가보라고 해서 왔어요.",
]
async def generate(self, req):
self.request = req
self.requests.append(req)
self.payload = self._payload_builder._payload(req)
text = self.responses.pop(0)
return GenerateResponse(
text=text,
model="fake-model",
provider="fake-provider",
tokens_in=3,
tokens_out=4,
cost_usd=0.0,
)
ctx = orchestrator.prepare_turn(
session_id="quality-session",
case_id="quality-case",
card=persona.P1,
state=_initial_state(),
learner_text="어머니가 오라고 하셨군요. 지금은 어떤 마음인가요?",
)
engine = SequenceEngine()
result = await orchestrator.run_turn_generate(ctx, engine) # type: ignore[arg-type]
self.assertEqual(len(engine.requests), 2)
self.assertEqual(result.client_reply, "엄마가 그냥 가보라고 해서 왔어요.")
self.assertFalse(result.safety_flagged)
async def test_run_turn_generate_returns_retryable_error_without_saving_bad_fallback(self) -> None:
class AlwaysBadEngine(CaptureGenerateEngine):
def __init__(self) -> None:
super().__init__("")
async def generate(self, req):
self.request = req
self.requests.append(req)
self.payload = self._payload_builder._payload(req)
return GenerateResponse(
text="AI로서 내담자 역할로 응답하겠습니다.",
model="fake-model",
provider="fake-provider",
tokens_in=3,
tokens_out=4,
cost_usd=0.0,
)
ctx = orchestrator.prepare_turn(
session_id="quality-session",
case_id="quality-case",
card=persona.P1,
state=_initial_state(),
learner_text="지금 이 자리에서 가장 말하기 어려운 게 뭔가요?",
)
engine = AlwaysBadEngine()
result = await orchestrator.run_turn_generate(ctx, engine) # type: ignore[arg-type]
self.assertEqual(len(engine.requests), 2)
self.assertIsNone(result.client_reply)
self.assertTrue(result.safety_flagged)
self.assertEqual(getattr(result, "output_error", None), "client_reply_quality_retryable")
async def test_run_turn_stream_humanizes_split_masked_placeholder_reply(self) -> None:
ctx = orchestrator.prepare_turn(
session_id="masking-session",
@ -422,6 +516,38 @@ class OrchestratorMaskingGateTest(unittest.IsolatedAsyncioTestCase):
self.assertIn("그 이름은 그대로고요.", streamed)
self.assertEqual(events[-1].event, "done")
async def test_run_turn_stream_buffers_role_meta_reply_without_token_leak(self) -> None:
ctx = orchestrator.prepare_turn(
session_id="quality-stream-session",
case_id="quality-stream-case",
card=persona.P1,
state=_initial_state(),
learner_text="어머니가 오라고 하셨군요. 지금은 어떤 마음인가요?",
)
engine = CaptureStreamEngine(
chunks=[
"내담자 ",
"역할로 응답하겠습니다. 엄마가 가보라고 해서요.",
]
)
events = [
event
async for event in orchestrator.run_turn_stream(
ctx,
engine, # type: ignore[arg-type]
)
]
self.assertEqual([event.event for event in events], ["safety", "done"])
self.assertEqual(
"".join(str(event.data.get("text", "")) for event in events if event.event == "token"),
"",
)
self.assertEqual(events[0].data["reason"], "client_reply_quality_retryable")
self.assertTrue(events[-1].data["safety_flagged"])
self.assertEqual(events[-1].data["output_error"], "client_reply_quality_retryable")
if __name__ == "__main__":
unittest.main()