세션 평가와 교수자 분석 보강

This commit is contained in:
Yun Chan 2026-07-01 12:10:52 +09:00
parent 5c4ac04e06
commit fe2796f05a
51 changed files with 4928 additions and 240 deletions

View file

@ -87,16 +87,17 @@ def _assert_masked_ko_pii_present(test: unittest.TestCase, value: object) -> Non
class CaptureGenerateEngine:
def __init__(self) -> None:
def __init__(self, text: str = "Masked engine reply.") -> None:
self.request = None
self.payload: dict[str, Any] | None = None
self._payload_builder = EngineClient(base_url="http://engine.test")
self.text = text
async def generate(self, req):
self.request = req
self.payload = self._payload_builder._payload(req)
return GenerateResponse(
text="Masked engine reply.",
text=self.text,
model="fake-model",
provider="fake-provider",
tokens_in=3,
@ -109,16 +110,18 @@ class CaptureStreamEngine:
engine_mode = "fake-provider"
default_model = "fake-model"
def __init__(self) -> None:
def __init__(self, chunks: list[str] | None = None) -> None:
self.request = None
self.payload: dict[str, Any] | None = None
self._payload_builder = EngineClient(base_url="http://engine.test")
self.chunks = chunks or ["Masked stream reply."]
async def stream(self, req):
self.request = req
self.payload = self._payload_builder._payload(req)
yield "event: token"
yield 'data: {"text":"Masked stream reply."}'
for chunk in self.chunks:
yield "event: token"
yield "data: " + json.dumps({"text": chunk}, ensure_ascii=False)
yield "event: done"
yield (
'data: {"provider":"fake-provider","model":"fake-model",'
@ -200,6 +203,15 @@ class OrchestratorMaskingGateTest(unittest.IsolatedAsyncioTestCase):
self.assertNotIn("NAME", masked.entities)
self.assertNotIn("ORG", masked.entities)
def test_mask_pii_does_not_mask_career_topic_as_name(self) -> None:
masked = guardrail.mask_pii("아직 정해진 건 없어요. 진로는 그대로고 엄마한테 말도 못 했어요.")
self.assertEqual(
masked.text_masked,
"아직 정해진 건 없어요. 진로는 그대로고 엄마한테 말도 못 했어요.",
)
self.assertNotIn("NAME", masked.entities)
def test_prepare_turn_masks_korean_pii_from_engine_messages(self) -> None:
ctx = orchestrator.prepare_turn(
session_id="masking-session",
@ -316,7 +328,11 @@ class OrchestratorMaskingGateTest(unittest.IsolatedAsyncioTestCase):
)
]
self.assertEqual([event.event for event in events], ["token", "done"])
self.assertEqual(events[-1].event, "done")
self.assertEqual(
"".join(str(event.data.get("text", "")) for event in events if event.event == "token"),
"Masked stream reply.",
)
self.assertIsNotNone(engine.request)
self.assertIsNotNone(engine.payload)
_assert_no_raw_pii(self, engine.request.messages)
@ -359,6 +375,53 @@ class OrchestratorMaskingGateTest(unittest.IsolatedAsyncioTestCase):
_assert_masked_ko_pii_present(self, engine.payload)
_assert_no_raw_ko_pii(self, audit_payloads)
async def test_run_turn_generate_humanizes_masked_placeholder_reply(self) -> None:
ctx = orchestrator.prepare_turn(
session_id="masking-session",
case_id="masking-case",
card=persona.P1,
state=_initial_state(),
learner_text="진로 이야기를 이어가고 싶어요.",
)
engine = CaptureGenerateEngine(
text="아직 정해진 건 하나도 없어요. [NAME]는 그대로고, 엄마한테 뭐라고 말할지도 모르겠고요."
)
result = await orchestrator.run_turn_generate(
ctx,
engine, # type: ignore[arg-type]
)
self.assertNotIn("[NAME]", result.client_reply or "")
self.assertIn("그 이름은 그대로고", result.client_reply or "")
async def test_run_turn_stream_humanizes_split_masked_placeholder_reply(self) -> None:
ctx = orchestrator.prepare_turn(
session_id="masking-session",
case_id="masking-case",
card=persona.P1,
state=_initial_state(),
learner_text="진로 이야기를 이어가고 싶어요.",
)
engine = CaptureStreamEngine(
chunks=["아직 정해진 건 하나도 없어요. ", "[NA", "ME]는", " 그대로고요."]
)
events = [
event
async for event in orchestrator.run_turn_stream(
ctx,
engine, # type: ignore[arg-type]
)
]
streamed = "".join(
str(event.data.get("text", "")) for event in events if event.event == "token"
)
self.assertNotIn("[NAME]", streamed)
self.assertIn("그 이름은 그대로고요.", streamed)
self.assertEqual(events[-1].event, "done")
if __name__ == "__main__":
unittest.main()