feat(voice): add duplex streaming v2 pipeline
This commit is contained in:
@@ -33,6 +33,7 @@ from services.shared.sql_models import (
|
||||
VoiceNameCollectionSettingsRow,
|
||||
VoiceTTSSettingsRow,
|
||||
VoiceAISessionRow,
|
||||
VoiceTranscriptSegmentRow,
|
||||
WhatsAppThreadRow,
|
||||
)
|
||||
from services.telegram_adapter_service import app as telegram_module
|
||||
@@ -1419,6 +1420,91 @@ def test_voice_v2_off_domain_request_returns_fast_operator_fallback_without_llm(
|
||||
assert decision["metadata"]["response_plan_id"] == "rsp_off_domain"
|
||||
|
||||
|
||||
def test_voice_v2_streaming_duplex_early_plan_returns_fast_safe_reply_without_llm(monkeypatch):
|
||||
monkeypatch.setenv("AI_VOICE_POLICY_MODE", "v2_streaming_duplex")
|
||||
|
||||
def _unexpected_llm(messages):
|
||||
raise AssertionError(f"LLM should not be called for early plan: {messages!r}")
|
||||
|
||||
monkeypatch.setattr(ai_module, "_request_structured_model_decision", _unexpected_llm)
|
||||
|
||||
decision = voice_module._voice_decision(
|
||||
language="ru",
|
||||
customer=None,
|
||||
interaction=SimpleNamespace(interaction_id="int_voice_early_plan", status="new", queue_id="que_voice", subject="unknown"),
|
||||
transcript_text="Расскажи, как устроен кондиционер",
|
||||
transcript_window=[],
|
||||
kb_results=[],
|
||||
disclosure_required=False,
|
||||
request_metadata={
|
||||
"voice_v2_enabled": True,
|
||||
"reply_phase": "early_plan",
|
||||
"response_plan_id": "rsp_early",
|
||||
},
|
||||
)
|
||||
|
||||
assert decision["model"] == "voice_early_plan_off_domain"
|
||||
assert decision["metadata"]["reply_phase"] == "early_plan"
|
||||
assert decision["metadata"]["voice_v2_enabled"] is True
|
||||
assert decision["metadata"]["response_plan_id"] == "rsp_early"
|
||||
assert "кондиционер" not in decision["reply_text"].lower()
|
||||
assert "оператор" in decision["reply_text"].lower()
|
||||
|
||||
|
||||
def test_turn_voice_session_early_plan_does_not_persist_partial_turns(monkeypatch):
|
||||
monkeypatch.setenv("AI_VOICE_POLICY_MODE", "v2_streaming_duplex")
|
||||
|
||||
def _unexpected_llm(messages):
|
||||
raise AssertionError(f"LLM should not be called for early plan: {messages!r}")
|
||||
|
||||
monkeypatch.setattr(ai_module, "_request_structured_model_decision", _unexpected_llm)
|
||||
|
||||
seeded = seed_voice_downstream_session(
|
||||
marker=f"voice_early_plan_{new_id('seed')}",
|
||||
name_status="name_not_obtained",
|
||||
customer_display_name="+77010009999",
|
||||
)
|
||||
|
||||
decision = voice_module.turn_voice_session(
|
||||
seeded["session_id"],
|
||||
VoiceAITurnIn(
|
||||
voice_session_id=seeded["session_id"],
|
||||
call_id=seeded["call_id"],
|
||||
interaction_id=seeded["interaction_id"],
|
||||
transcript_text="Расскажи, как устроен кондиционер",
|
||||
language="ru",
|
||||
sequence_no=1,
|
||||
metadata={
|
||||
"voice_v2_enabled": True,
|
||||
"reply_phase": "early_plan",
|
||||
"response_plan_id": "rsp_early_turn",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
assert decision.metadata["reply_phase"] == "early_plan"
|
||||
assert decision.metadata["response_plan_id"] == "rsp_early_turn"
|
||||
assert decision.status == "active"
|
||||
|
||||
session = get_session()
|
||||
try:
|
||||
voice_session = session.execute(
|
||||
select(VoiceAISessionRow).where(VoiceAISessionRow.session_id == seeded["session_id"])
|
||||
).scalar_one()
|
||||
ai_turns = session.execute(
|
||||
select(AITurnRow).where(AITurnRow.interaction_id == seeded["interaction_id"])
|
||||
).scalars().all()
|
||||
transcript_segments = session.execute(
|
||||
select(VoiceTranscriptSegmentRow).where(VoiceTranscriptSegmentRow.session_id == seeded["session_id"])
|
||||
).scalars().all()
|
||||
assert voice_session.ai_session_id is None
|
||||
assert voice_session.status == "active"
|
||||
assert ai_turns == []
|
||||
assert transcript_segments == []
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
def test_ai_enqueue_creates_outbound_ai_reply_and_delivery_flow(monkeypatch):
|
||||
monkeypatch.setenv("AI_TELEGRAM_ENABLED", "1")
|
||||
monkeypatch.setenv("AI_PROVIDER", "stub")
|
||||
|
||||
Reference in New Issue
Block a user