From 69c5941139b9f91512c401808f5306c3a49d024e Mon Sep 17 00:00:00 2001 From: Xin Wang Date: Fri, 29 May 2026 11:01:24 +0800 Subject: [PATCH] Update idle prompt --- config.json | 5 ++++- config/fastgpt.state.xfyun.json | 5 ++++- engine/config.py | 15 +++++++++++++++ engine/pipeline.py | 15 +++++++++++++++ 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/config.json b/config.json index aab655b..60c7db6 100644 --- a/config.json +++ b/config.json @@ -52,7 +52,10 @@ "没有", "否" ], - "user_speech_timeout_sec": 0.8 + "user_speech_timeout_sec": 0.8, + "idle_prompt_timeout_sec": 8.0, + "idle_prompt_max_count": 1, + "idle_prompt_text": "我先停在这里。你可以继续说你的想法,或者让我根据刚才的内容帮你整理下一步。" }, "agent": { "system_prompt": "You are a helpful, friendly voice assistant. Keep responses concise and natural for spoken conversation.", diff --git a/config/fastgpt.state.xfyun.json b/config/fastgpt.state.xfyun.json index f25ce21..f3a89f2 100644 --- a/config/fastgpt.state.xfyun.json +++ b/config/fastgpt.state.xfyun.json @@ -42,7 +42,10 @@ "你好", "在吗" ], - "user_speech_timeout_sec": 0.2 + "user_speech_timeout_sec": 0.8, + "idle_prompt_timeout_sec": 8.0, + "idle_prompt_max_count": 3, + "idle_prompt_text": "我先停在这里。你可以继续说你的想法,或者让我根据刚才的内容帮你整理下一步。" }, "agent": { "system_prompt": "FastGPT app owns the system prompt when send_system_prompt is false.", diff --git a/engine/config.py b/engine/config.py index 1d32c4b..42bf3d8 100644 --- a/engine/config.py +++ b/engine/config.py @@ -78,6 +78,12 @@ class TurnConfig: vad: VADConfig = field(default_factory=VADConfig) user_speech_timeout_sec: float = 1.0 + idle_prompt_timeout_sec: float = 0.0 + idle_prompt_max_count: int = 1 + idle_prompt_text: str = ( + "我先停在这里。你可以继续说你的想法," + "或者让我根据刚才的内容帮你整理下一步。" + ) interruption_min_chars: int = 3 interruption_use_interim: bool = True interruption_short_replies: list[str] = field( @@ -263,6 +269,15 @@ def config_from_dict(data: dict) -> EngineConfig: user_speech_timeout_sec=float( turn.get("user_speech_timeout_sec", TurnConfig().user_speech_timeout_sec) ), + idle_prompt_timeout_sec=float( + turn.get("idle_prompt_timeout_sec", TurnConfig().idle_prompt_timeout_sec) + ), + idle_prompt_max_count=int( + turn.get("idle_prompt_max_count", TurnConfig().idle_prompt_max_count) + ), + idle_prompt_text=str( + turn.get("idle_prompt_text", TurnConfig().idle_prompt_text) + ), interruption_min_chars=int( turn.get("interruption_min_chars", TurnConfig().interruption_min_chars) ), diff --git a/engine/pipeline.py b/engine/pipeline.py index aa62c49..75361dd 100644 --- a/engine/pipeline.py +++ b/engine/pipeline.py @@ -146,6 +146,7 @@ async def run_pipeline_with_serializer( user_params=LLMUserAggregatorParams( vad_analyzer=SileroVADAnalyzer(params=vad_params), user_turn_strategies=user_turn_strategies, + user_idle_timeout=config.turn.idle_prompt_timeout_sec, ), ) @@ -188,6 +189,7 @@ async def run_pipeline_with_serializer( ), idle_timeout_secs=config.session.inactivity_timeout_sec, ) + idle_prompt_count = 0 @transport.event_handler("on_client_connected") async def on_client_connected(_transport, _client): @@ -216,6 +218,8 @@ async def run_pipeline_with_serializer( @user_aggregator.event_handler("on_user_turn_stopped") async def on_user_turn_stopped(_aggregator, _strategy, message: UserTurnStoppedMessage): + nonlocal idle_prompt_count + idle_prompt_count = 0 logger.info(f"User: {message.content}") text = (message.content or "").strip() if not text: @@ -231,6 +235,17 @@ async def run_pipeline_with_serializer( ) ) + @user_aggregator.event_handler("on_user_turn_idle") + async def on_user_turn_idle(_aggregator): + nonlocal idle_prompt_count + text = config.turn.idle_prompt_text.strip() + if not text or idle_prompt_count >= config.turn.idle_prompt_max_count: + return + + idle_prompt_count += 1 + logger.info("User idle prompt triggered") + await task.queue_frames([TTSSpeakFrame(text)]) + # NOTE: assistant turn started/final events are emitted by # ProductTextStreamProcessor, upstream of TTS, so text streams to the # client ahead of audio. This logger is kept for server-side visibility.