음성 재생과 운영 배포 정리
This commit is contained in:
parent
8ed185ce6c
commit
ac7db95542
1020 changed files with 46863 additions and 2175 deletions
|
|
@ -12,7 +12,7 @@ from .deps import Principal, Role
|
|||
from .engine_client import EngineError
|
||||
from .routes import sessions
|
||||
from .routes import voice as voice_routes
|
||||
from .services import memory, orchestrator, persona as persona_service, state_machine
|
||||
from .services import live_coach, memory, orchestrator, persona as persona_service, state_machine
|
||||
from .services.voice import TTSChunk, TranscriptResult, VoicePreset
|
||||
from .store import InProcSession, TurnRecord, store
|
||||
|
||||
|
|
@ -25,6 +25,7 @@ def _principal() -> Principal:
|
|||
email="turn-test@hs.ac.kr",
|
||||
display_name="Turn Test",
|
||||
consent_at=1.0,
|
||||
profile_completed_at=1.0,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -310,6 +311,79 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase):
|
|||
self.assertIn("조금 말해볼게요", learner_turn.evaluation["appropriateness_note"])
|
||||
self.assertIsNone(client_turn.evaluation)
|
||||
|
||||
async def test_live_coach_degrades_to_rule_based_suggestion_when_engine_fails(self) -> None:
|
||||
principal = _principal()
|
||||
sess = _session(principal)
|
||||
sess.turns.append(
|
||||
TurnRecord(
|
||||
turn_seq=1,
|
||||
speaker="counselor",
|
||||
stage=sess.state.stage.value,
|
||||
text="그냥 학교는 가야 하는 거 아닐까요?",
|
||||
text_masked="그냥 학교는 가야 하는 거 아닐까요?",
|
||||
evaluation={"appropriateness": "warn", "appropriateness_note": "조언이 빠름"},
|
||||
)
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
sessions,
|
||||
"_retrieve_live_coach_grounding",
|
||||
AsyncMock(return_value=[]),
|
||||
), patch.object(
|
||||
sessions.engine_client,
|
||||
"generate",
|
||||
AsyncMock(side_effect=EngineError("offline")),
|
||||
):
|
||||
response = await sessions.live_coach_turn(
|
||||
sess.session_id,
|
||||
sessions.LiveCoachRequest(
|
||||
learner_text="그냥 학교는 가야 하는 거 아닐까요?",
|
||||
client_reply="몰라요. 그런 말 들으려고 온 건 아닌데요.",
|
||||
turn_seq=1,
|
||||
),
|
||||
principal,
|
||||
)
|
||||
|
||||
self.assertEqual(response.status, "degraded")
|
||||
self.assertEqual(response.tone, "warn")
|
||||
self.assertEqual(response.focus, "rapport")
|
||||
self.assertIn("조언", response.title + response.message)
|
||||
self.assertTrue(response.next_utterance)
|
||||
self.assertTrue(response.sources)
|
||||
self.assertEqual(response.sources[0].source_id, "workbook_0615_case_conceptualization")
|
||||
history = await sessions.list_live_coach_history(sess.session_id, principal)
|
||||
self.assertEqual(history.source, "runtime")
|
||||
self.assertEqual(len(history.events), 1)
|
||||
self.assertEqual(history.events[0].turn_seq, 1)
|
||||
self.assertEqual(history.events[0].suggestion.title, response.title)
|
||||
self.assertIn("학교", history.events[0].learner_text_excerpt or "")
|
||||
|
||||
async def test_live_coach_uses_official_risk_reference_pack_for_crisis_signal(self) -> None:
|
||||
item = live_coach.LiveCoachInput(
|
||||
session_id="risk-coach-session",
|
||||
turn_seq=3,
|
||||
stage="exploration",
|
||||
effective_openness=0.45,
|
||||
theory_mode="humanistic",
|
||||
persona_code="P1",
|
||||
persona_name="서연",
|
||||
learner_text="죽고 싶다는 생각이 들 때도 있나요?",
|
||||
client_reply="가끔 그런 생각이 들어요.",
|
||||
recent_turns=[],
|
||||
evaluation={"appropriateness": "warn", "appropriateness_note": "위험사정 필요"},
|
||||
)
|
||||
|
||||
engine = SimpleNamespace(generate=AsyncMock(side_effect=EngineError("offline")))
|
||||
suggestion = await live_coach.generate_live_coaching(item, engine=engine)
|
||||
|
||||
self.assertEqual(suggestion.focus, "risk")
|
||||
source_ids = [source.source_id for source in suggestion.sources]
|
||||
self.assertIn("official_suicide_risk_guidelines", source_ids)
|
||||
official = next(source for source in suggestion.sources if source.source_id == "official_suicide_risk_guidelines")
|
||||
self.assertEqual(official.source_type, "official_guideline")
|
||||
self.assertTrue(official.citation)
|
||||
engine.generate.assert_called_once()
|
||||
|
||||
async def test_start_session_uses_stable_case_context_and_seed_recall(self) -> None:
|
||||
principal = _principal()
|
||||
card = persona_service.P1
|
||||
|
|
@ -394,6 +468,25 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase):
|
|||
self.assertEqual(caught.exception.detail, "consent_required")
|
||||
get_persona.assert_not_awaited()
|
||||
|
||||
async def test_start_session_requires_onboarding_before_consent_and_catalog_lookup(self) -> None:
|
||||
principal = _principal()
|
||||
principal.profile_completed_at = None
|
||||
|
||||
with patch.object(
|
||||
sessions,
|
||||
"get_catalog_persona",
|
||||
AsyncMock(side_effect=AssertionError("onboarding gate must run before catalog lookup")),
|
||||
) as get_persona:
|
||||
with self.assertRaises(sessions.HTTPException) as caught:
|
||||
await sessions.start_session(
|
||||
sessions.SessionStartRequest(persona_code=persona_service.P1.code),
|
||||
principal,
|
||||
)
|
||||
|
||||
self.assertEqual(caught.exception.status_code, 403)
|
||||
self.assertEqual(caught.exception.detail, "onboarding_required")
|
||||
get_persona.assert_not_awaited()
|
||||
|
||||
async def test_run_turn_stream_parses_gateway_done_telemetry(self) -> None:
|
||||
class FakeStreamEngine:
|
||||
engine_mode = "claude_cli"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue