[持续对话判定]:完成恢复待机延迟修复,包含本地规则收敛和分类器跳过回归测试
This commit is contained in:
@@ -35,28 +35,27 @@ class ContinuationDecisionProvider(Protocol):
|
|||||||
|
|
||||||
class RuleContinuationDecisionProvider:
|
class RuleContinuationDecisionProvider:
|
||||||
_continue_patterns = (
|
_continue_patterns = (
|
||||||
"你想",
|
|
||||||
"你要",
|
|
||||||
"你需要",
|
|
||||||
"需要我",
|
|
||||||
"要不要",
|
|
||||||
"是否需要",
|
|
||||||
"可以告诉我",
|
"可以告诉我",
|
||||||
"告诉我",
|
|
||||||
"请告诉我",
|
"请告诉我",
|
||||||
"你希望",
|
|
||||||
"你更想",
|
|
||||||
"哪一个",
|
|
||||||
"哪一部分",
|
|
||||||
"哪种",
|
|
||||||
"哪个",
|
|
||||||
"请选择",
|
"请选择",
|
||||||
"选一个",
|
"选一个",
|
||||||
"请补充",
|
"请补充",
|
||||||
"需要补充",
|
|
||||||
"补充一下",
|
"补充一下",
|
||||||
"继续吗",
|
"继续吗",
|
||||||
)
|
)
|
||||||
|
_ambiguous_continue_patterns = (
|
||||||
|
"我还可以",
|
||||||
|
"我可以继续",
|
||||||
|
"还可以继续",
|
||||||
|
"可以继续",
|
||||||
|
"继续展开",
|
||||||
|
"继续讲",
|
||||||
|
"继续聊",
|
||||||
|
"两个方向",
|
||||||
|
"三个方向",
|
||||||
|
"几个方向",
|
||||||
|
"几个部分",
|
||||||
|
)
|
||||||
_standby_patterns = (
|
_standby_patterns = (
|
||||||
"这是",
|
"这是",
|
||||||
"已经",
|
"已经",
|
||||||
@@ -70,6 +69,11 @@ class RuleContinuationDecisionProvider:
|
|||||||
"抱歉",
|
"抱歉",
|
||||||
"出错",
|
"出错",
|
||||||
"失败",
|
"失败",
|
||||||
|
"先这样",
|
||||||
|
"到这里",
|
||||||
|
"随时叫我",
|
||||||
|
"有需要再叫我",
|
||||||
|
"需要时再叫我",
|
||||||
)
|
)
|
||||||
|
|
||||||
def decide(
|
def decide(
|
||||||
@@ -88,7 +92,9 @@ class RuleContinuationDecisionProvider:
|
|||||||
return ContinuationDecision("standby", 0.82, "assistant appears to finish the answer", "rule")
|
return ContinuationDecision("standby", 0.82, "assistant appears to finish the answer", "rule")
|
||||||
if len(text) <= 18 and not text.endswith(("?", "?", "吗", "呢")):
|
if len(text) <= 18 and not text.endswith(("?", "?", "吗", "呢")):
|
||||||
return ContinuationDecision("standby", 0.72, "short non-question reply", "rule")
|
return ContinuationDecision("standby", 0.72, "short non-question reply", "rule")
|
||||||
return ContinuationDecision("unknown", 0.0, "rule uncertain", "rule")
|
if any(pattern in text for pattern in self._ambiguous_continue_patterns):
|
||||||
|
return ContinuationDecision("unknown", 0.0, "ambiguous continuation offer", "rule")
|
||||||
|
return ContinuationDecision("standby", 0.78, "assistant did not ask for immediate input", "rule")
|
||||||
|
|
||||||
|
|
||||||
class LlmContinuationDecisionProvider:
|
class LlmContinuationDecisionProvider:
|
||||||
|
|||||||
@@ -58,6 +58,24 @@ class ContinuationDecisionTests(unittest.TestCase):
|
|||||||
self.assertEqual(decision.action, "standby")
|
self.assertEqual(decision.action, "standby")
|
||||||
self.assertEqual(len(llm.calls), 1)
|
self.assertEqual(len(llm.calls), 1)
|
||||||
|
|
||||||
|
def test_hybrid_generic_completed_reply_does_not_call_llm_classifier(self) -> None:
|
||||||
|
llm = FakeClassifierLlm('{"action":"continue","confidence":0.99,"reason":"不应调用"}')
|
||||||
|
provider = HybridContinuationDecisionProvider(
|
||||||
|
RuleContinuationDecisionProvider(),
|
||||||
|
LlmContinuationDecisionProvider(llm, threshold=0.65),
|
||||||
|
threshold=0.65,
|
||||||
|
)
|
||||||
|
|
||||||
|
decision = provider.decide(
|
||||||
|
user_text="没有呢",
|
||||||
|
assistant_text="明白了,我先保持待机。有需要再叫我就行。",
|
||||||
|
history=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(decision.action, "standby")
|
||||||
|
self.assertEqual(decision.provider, "rule")
|
||||||
|
self.assertEqual(llm.calls, [])
|
||||||
|
|
||||||
def test_hybrid_high_confidence_llm_can_continue(self) -> None:
|
def test_hybrid_high_confidence_llm_can_continue(self) -> None:
|
||||||
llm = FakeClassifierLlm('{"action":"continue","confidence":0.91,"reason":"等待用户选择"}')
|
llm = FakeClassifierLlm('{"action":"continue","confidence":0.91,"reason":"等待用户选择"}')
|
||||||
provider = HybridContinuationDecisionProvider(
|
provider = HybridContinuationDecisionProvider(
|
||||||
|
|||||||
@@ -594,6 +594,38 @@ class LiveRuntimeTests(unittest.TestCase):
|
|||||||
self.assertEqual(event_types[-1], STANDBY_RESUMED)
|
self.assertEqual(event_types[-1], STANDBY_RESUMED)
|
||||||
self.assertEqual(len(llm.calls), 1)
|
self.assertEqual(len(llm.calls), 1)
|
||||||
|
|
||||||
|
def test_completed_reply_returns_to_standby_without_cloud_classifier_delay(self) -> None:
|
||||||
|
frames = [wake_frame(0, 0)]
|
||||||
|
frames.extend(segment_frames(1, 20, partials=["没有呢", "没有呢"]))
|
||||||
|
transport = MemoryAudioTransport(frames, flush_clears_input=False)
|
||||||
|
llm = QueueLlmProvider([["明白了,我先保持待机。有需要再叫我就行。"]])
|
||||||
|
runtime = VoiceAssistantPipeline(
|
||||||
|
config=AppConfig(
|
||||||
|
llm_api_key="secret",
|
||||||
|
speech_provider="cloud",
|
||||||
|
wake_ack_text="",
|
||||||
|
followup_listen_timeout_ms=3000,
|
||||||
|
),
|
||||||
|
transport=transport,
|
||||||
|
wakeword=KeywordWakeWordProvider(),
|
||||||
|
vad_recorder=VadRecorder(EnergyVadProvider(), min_duration_ms=40, end_silence_ms=40),
|
||||||
|
stt=QueueSttProvider(["没有呢"]),
|
||||||
|
realtime_stt=MetadataSttProvider(),
|
||||||
|
llm=llm,
|
||||||
|
tts=SineTtsProvider(),
|
||||||
|
context=ConversationContext(),
|
||||||
|
reporter=RecordingReporter(),
|
||||||
|
event_bus=PipelineEventBus(),
|
||||||
|
)
|
||||||
|
|
||||||
|
summary = runtime.run(max_turns=1)
|
||||||
|
event_types = [event.type for event in runtime.event_bus.events]
|
||||||
|
|
||||||
|
self.assertEqual(summary.completed_turns, 1)
|
||||||
|
self.assertNotIn(FOLLOWUP_LISTENING, event_types)
|
||||||
|
self.assertEqual(event_types[-1], STANDBY_RESUMED)
|
||||||
|
self.assertEqual(len(llm.calls), 1)
|
||||||
|
|
||||||
def test_barge_in_interrupts_playback_and_keeps_only_spoken_context(self) -> None:
|
def test_barge_in_interrupts_playback_and_keeps_only_spoken_context(self) -> None:
|
||||||
first_question_frames = [wake_frame(0, 0)]
|
first_question_frames = [wake_frame(0, 0)]
|
||||||
first_question_frames.extend(segment_frames(1, 20, partials=["第一问", "第一问"]))
|
first_question_frames.extend(segment_frames(1, 20, partials=["第一问", "第一问"]))
|
||||||
|
|||||||
Reference in New Issue
Block a user