255 lines
11 KiB
Python
255 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
import unittest
|
|
|
|
from app.livekit.call_config import (
|
|
normalize_call_config,
|
|
resolve_fake_agent_overrides,
|
|
resolve_agent_backend_name,
|
|
resolve_stt_overrides,
|
|
resolve_tts_overrides,
|
|
resolve_vad_logging_overrides,
|
|
resolve_vad_overrides,
|
|
resolve_ws_overrides,
|
|
)
|
|
from app.ws_gateway.call_config import build_call_config
|
|
|
|
|
|
class CallConfigTests(unittest.TestCase):
|
|
def test_build_and_normalize_call_config(self) -> None:
|
|
payload = {
|
|
"agentBackend": "remote_ws_fake",
|
|
"stt": {
|
|
"provider": "internal_http",
|
|
"language": "pt-BR",
|
|
"configOverride": "{\"processor\":{\"strategy\":\"faster_default\"}}",
|
|
"minProbSingleWord": "0.15",
|
|
"disableVosk": True,
|
|
},
|
|
"tts": {
|
|
"provider": "elevenlabs",
|
|
"voiceId": "voice-123",
|
|
"modelId": "model-456",
|
|
},
|
|
"vad": {
|
|
"minSpeechDuration": 0.04,
|
|
"activationThreshold": 0.18,
|
|
"deactivationThreshold": 0.10,
|
|
"minSilenceDuration": 0.7,
|
|
"prefixPaddingDuration": 0.75,
|
|
"preBackendWaitNoticeFastOnVadPause": True,
|
|
"deferredInterruptionMinAudioMs": 1350,
|
|
"deferredInterruptionEnabled": False,
|
|
},
|
|
"vadLogging": {
|
|
"logDecisions": True,
|
|
"logActivity": False,
|
|
"activityMinProbability": 0.03,
|
|
},
|
|
"ws": {
|
|
"outputGain": 1.2,
|
|
"audioInputBacklogShedEnabled": True,
|
|
"audioInputBacklogShedThresholdMs": 700,
|
|
"audioInputBacklogShedKeepMs": 250,
|
|
"audioInputLatencyMetricsEnabled": True,
|
|
"audioInputLatencyAlertMs": 900,
|
|
"audioInputLatencyLogIntervalS": 10.5,
|
|
"livekitAudioSourceQueueSizeMs": 400,
|
|
"livekitAudioSourceClearOnShed": False,
|
|
"audioInputBacklogEnergyShedEnabled": True,
|
|
"audioInputBacklogEnergyShedMaxExcessMs": 600,
|
|
"audioInputBacklogSilenceDbfs": -60,
|
|
},
|
|
"agentFake": {
|
|
"delayMs": 2500,
|
|
"responses": (
|
|
"Esta e a primeira resposta simulada com tamanho intermediario;"
|
|
"Esta e a resposta final simulada encerrando o atendimento"
|
|
),
|
|
},
|
|
}
|
|
|
|
built = build_call_config(payload)
|
|
normalized = normalize_call_config(payload)
|
|
|
|
self.assertEqual(built, normalized)
|
|
self.assertEqual(resolve_agent_backend_name(payload, "remote_ws"), "remote_ws_fake")
|
|
self.assertEqual(resolve_stt_overrides(payload)["language"], "pt-BR")
|
|
self.assertEqual(resolve_stt_overrides(payload)["disable_vosk"], "True")
|
|
self.assertEqual(
|
|
resolve_stt_overrides(payload)["config_override"],
|
|
"{\"processor\":{\"strategy\":\"faster_default\"}}",
|
|
)
|
|
self.assertEqual(resolve_tts_overrides(payload)["voice_id"], "voice-123")
|
|
self.assertEqual(resolve_vad_overrides(payload)["min_speech_duration"], "0.04")
|
|
self.assertEqual(resolve_vad_overrides(payload)["activation_threshold"], "0.18")
|
|
self.assertEqual(
|
|
resolve_vad_overrides(payload)["pre_backend_wait_notice_fast_on_vad_pause"],
|
|
"True",
|
|
)
|
|
self.assertEqual(
|
|
resolve_vad_overrides(payload)["deferred_interruption_min_audio_ms"],
|
|
"1350",
|
|
)
|
|
self.assertEqual(
|
|
resolve_vad_overrides(payload)["deferred_interruption_enabled"],
|
|
"False",
|
|
)
|
|
self.assertEqual(resolve_vad_logging_overrides(payload)["log_decisions"], "True")
|
|
self.assertEqual(resolve_vad_logging_overrides(payload)["log_activity"], "False")
|
|
self.assertEqual(resolve_ws_overrides(payload)["output_gain"], "1.2")
|
|
self.assertEqual(resolve_ws_overrides(payload)["audio_in_backlog_shed_enabled"], "True")
|
|
self.assertEqual(resolve_ws_overrides(payload)["audio_in_backlog_shed_threshold_ms"], "700")
|
|
self.assertEqual(resolve_ws_overrides(payload)["audio_in_backlog_shed_keep_ms"], "250")
|
|
self.assertEqual(resolve_ws_overrides(payload)["audio_in_latency_metrics_enabled"], "True")
|
|
self.assertEqual(resolve_ws_overrides(payload)["audio_in_latency_alert_ms"], "900")
|
|
self.assertEqual(resolve_ws_overrides(payload)["audio_in_latency_log_interval_s"], "10.5")
|
|
self.assertEqual(resolve_ws_overrides(payload)["livekit_audio_source_queue_size_ms"], "400")
|
|
self.assertEqual(resolve_ws_overrides(payload)["livekit_audio_source_clear_on_shed"], "False")
|
|
self.assertEqual(resolve_ws_overrides(payload)["audio_in_backlog_energy_shed_enabled"], "True")
|
|
self.assertEqual(resolve_ws_overrides(payload)["audio_in_backlog_energy_shed_max_excess_ms"], "600")
|
|
self.assertEqual(resolve_ws_overrides(payload)["audio_in_backlog_silence_dbfs"], "-60")
|
|
fake = resolve_fake_agent_overrides(payload)
|
|
self.assertEqual(fake["delay_ms"], 2500)
|
|
self.assertEqual(len(fake["responses"]), 2)
|
|
|
|
def test_empty_payload_falls_back_to_defaults(self) -> None:
|
|
payload = {}
|
|
|
|
self.assertEqual(
|
|
build_call_config(payload),
|
|
{
|
|
"agent_backend": "",
|
|
"stt": {
|
|
"provider": "",
|
|
"language": "",
|
|
"api_key": "",
|
|
"initial_prompt": "",
|
|
"config_override": "",
|
|
"min_prob_single_word": "",
|
|
"disable_vosk": "",
|
|
},
|
|
"tts": {
|
|
"provider": "",
|
|
"voice_id": "",
|
|
"model_id": "",
|
|
"language": "",
|
|
},
|
|
"vad": {
|
|
"min_speech_duration": "",
|
|
"activation_threshold": "",
|
|
"deactivation_threshold": "",
|
|
"min_silence_duration": "",
|
|
"prefix_padding_duration": "",
|
|
"pre_backend_wait_notice_fast_on_vad_pause": "",
|
|
"deferred_interruption_min_audio_ms": "",
|
|
"deferred_interruption_enabled": "",
|
|
},
|
|
"vad_logging": {
|
|
"log_decisions": "",
|
|
"log_activity": "",
|
|
"activity_min_probability": "",
|
|
},
|
|
"ws": {
|
|
"output_gain": "",
|
|
"audio_in_backlog_shed_enabled": "",
|
|
"audio_in_backlog_shed_threshold_ms": "",
|
|
"audio_in_backlog_shed_keep_ms": "",
|
|
"audio_in_latency_metrics_enabled": "",
|
|
"audio_in_latency_alert_ms": "",
|
|
"audio_in_latency_log_interval_s": "",
|
|
"livekit_audio_source_queue_size_ms": "",
|
|
"livekit_audio_source_clear_on_shed": "",
|
|
"audio_in_backlog_energy_shed_enabled": "",
|
|
"audio_in_backlog_energy_shed_max_excess_ms": "",
|
|
"audio_in_backlog_silence_dbfs": "",
|
|
},
|
|
"agent_fake": {"delay_ms": "", "responses": ""},
|
|
},
|
|
)
|
|
self.assertEqual(resolve_agent_backend_name(payload, "remote_ws"), "remote_ws")
|
|
|
|
def test_fake_responses_are_trimmed_and_default_delay_is_applied(self) -> None:
|
|
payload = {
|
|
"agentBackend": "remote_ws_fake",
|
|
"agentFake": {
|
|
"responses": (
|
|
" Primeira resposta simulada com comprimento intermediario ;"
|
|
" Segunda resposta simulada encerrando corretamente a chamada "
|
|
)
|
|
},
|
|
}
|
|
|
|
fake = resolve_fake_agent_overrides(payload)
|
|
|
|
self.assertEqual(fake["delay_ms"], 2500)
|
|
self.assertEqual(
|
|
fake["responses"],
|
|
[
|
|
"Primeira resposta simulada com comprimento intermediario",
|
|
"Segunda resposta simulada encerrando corretamente a chamada",
|
|
],
|
|
)
|
|
|
|
def test_fake_responses_reject_invalid_contract(self) -> None:
|
|
valid = "Esta resposta simulada possui tamanho intermediario adequado"
|
|
invalid_cases = [
|
|
{"agentBackend": "remote_ws", "agentFake": {"responses": f"{valid};{valid}"}},
|
|
{"agentBackend": "remote_ws_fake", "agentFake": {"responses": valid}},
|
|
{"agentBackend": "remote_ws_fake", "agentFake": {"responses": f"{valid};;{valid}"}},
|
|
{"agentBackend": "remote_ws_fake", "agentFake": {"responses": f"curta;{valid}"}},
|
|
{
|
|
"agentBackend": "remote_ws_fake",
|
|
"agentFake": {"responses": f"{valid};{valid}", "delayMs": 180001},
|
|
},
|
|
]
|
|
|
|
for payload in invalid_cases:
|
|
with self.subTest(payload=payload), self.assertRaises(ValueError):
|
|
resolve_fake_agent_overrides(payload)
|
|
|
|
def test_disable_vosk_and_fake_backend_are_normalized_per_call(self) -> None:
|
|
payload = {
|
|
"agentBackend": "remote_ws_fake",
|
|
"stt": {"provider": "internal_http", "disableVosk": True},
|
|
}
|
|
|
|
self.assertEqual(resolve_agent_backend_name(payload, "remote_ws"), "remote_ws_fake")
|
|
self.assertEqual(resolve_stt_overrides(payload)["disable_vosk"], "True")
|
|
|
|
def test_fast_vad_pause_override_lives_with_vad_overrides(self) -> None:
|
|
payload = {"vad": {"preBackendWaitNoticeFastOnVadPause": True}}
|
|
|
|
self.assertEqual(
|
|
resolve_vad_overrides(payload)["pre_backend_wait_notice_fast_on_vad_pause"],
|
|
"True",
|
|
)
|
|
|
|
def test_deferred_interruption_min_audio_override_lives_with_vad_overrides(self) -> None:
|
|
payload = {"vad": {"deferredInterruptionMinAudioMs": 1250}}
|
|
|
|
self.assertEqual(
|
|
resolve_vad_overrides(payload)["deferred_interruption_min_audio_ms"],
|
|
"1250",
|
|
)
|
|
|
|
def test_deferred_interruption_min_audio_accepts_root_env_style_key(self) -> None:
|
|
payload = {"DEFERRED_INTERRUPTION_MIN_AUDIO_MS": 1400}
|
|
|
|
self.assertEqual(
|
|
resolve_vad_overrides(payload)["deferred_interruption_min_audio_ms"],
|
|
"1400",
|
|
)
|
|
|
|
def test_deferred_interruption_enabled_accepts_root_env_style_key(self) -> None:
|
|
payload = {"DEFERRED_INTERRUPTION_ENABLED": False}
|
|
|
|
self.assertEqual(
|
|
resolve_vad_overrides(payload)["deferred_interruption_enabled"],
|
|
"False",
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|