diff --git a/api/app/routers/assistants.py b/api/app/routers/assistants.py index 6ce52de..d3a28e2 100644 --- a/api/app/routers/assistants.py +++ b/api/app/routers/assistants.py @@ -182,11 +182,13 @@ def _resolve_runtime_tools(db: Session, selected_tool_ids: List[str], warnings: def _resolve_runtime_metadata(db: Session, assistant: Assistant) -> tuple[Dict[str, Any], List[str]]: warnings: List[str] = [] + generated_opener_enabled = bool(assistant.generated_opener_enabled) metadata: Dict[str, Any] = { "systemPrompt": _compose_runtime_system_prompt(assistant.prompt), "firstTurnMode": assistant.first_turn_mode or "bot_first", - "greeting": assistant.opener or "", - "generatedOpenerEnabled": bool(assistant.generated_opener_enabled), + # Generated opener should rely on systemPrompt instead of fixed opener text. + "greeting": "" if generated_opener_enabled else (assistant.opener or ""), + "generatedOpenerEnabled": generated_opener_enabled, "output": {"mode": "audio" if assistant.voice_output_enabled else "text"}, "bargeIn": { "enabled": not bool(assistant.bot_cannot_be_interrupted), diff --git a/api/tests/test_assistants.py b/api/tests/test_assistants.py index d1fc52d..adb7d7c 100644 --- a/api/tests/test_assistants.py +++ b/api/tests/test_assistants.py @@ -331,5 +331,6 @@ class TestAssistantAPI: metadata = runtime_resp.json()["sessionStartMetadata"] assert metadata["firstTurnMode"] == "user_first" assert metadata["generatedOpenerEnabled"] is True + assert metadata["greeting"] == "" assert metadata["bargeIn"]["enabled"] is False assert metadata["bargeIn"]["minDurationMs"] == 900 diff --git a/engine/core/duplex_pipeline.py b/engine/core/duplex_pipeline.py index 037274a..6ee5747 100644 --- a/engine/core/duplex_pipeline.py +++ b/engine/core/duplex_pipeline.py @@ -780,7 +780,6 @@ class DuplexPipeline: if not self.llm_service: return None - prompt_hint = (self._runtime_greeting or "").strip() system_context = (self.conversation.system_prompt or self._runtime_system_prompt or "").strip() # Keep context concise to avoid overloading greeting generation. if len(system_context) > 1200: @@ -793,8 +792,6 @@ class DuplexPipeline: user_prompt = "请生成一句中文开场白(不超过25个汉字)。" if system_context: user_prompt += f"\n\n以下是该助手的系统提示词,请据此决定语气、角色和边界:\n{system_context}" - if prompt_hint: - user_prompt += f"\n\n额外风格提示:{prompt_hint}" try: generated = await self.llm_service.generate( diff --git a/engine/tests/test_tool_call_flow.py b/engine/tests/test_tool_call_flow.py index ac60de9..3a34193 100644 --- a/engine/tests/test_tool_call_flow.py +++ b/engine/tests/test_tool_call_flow.py @@ -61,6 +61,9 @@ class _FakeLLM: self._rounds = rounds self._call_index = 0 + async def generate(self, _messages, temperature=0.7, max_tokens=None): + return "" + async def generate_stream(self, _messages, temperature=0.7, max_tokens=None): idx = self._call_index self._call_index += 1 @@ -69,6 +72,19 @@ class _FakeLLM: yield event +class _CaptureGenerateLLM: + def __init__(self, response: str): + self.response = response + self.messages: List[Any] = [] + + async def generate(self, messages, temperature=0.7, max_tokens=None): + self.messages = list(messages) + return self.response + + async def generate_stream(self, _messages, temperature=0.7, max_tokens=None): + yield LLMStreamEvent(type="done") + + def _build_pipeline(monkeypatch, llm_rounds: List[List[LLMStreamEvent]]) -> tuple[DuplexPipeline, List[Dict[str, Any]]]: monkeypatch.setattr("core.duplex_pipeline.SileroVAD", _DummySileroVAD) monkeypatch.setattr("core.duplex_pipeline.VADProcessor", _DummyVADProcessor) @@ -203,6 +219,33 @@ async def test_pipeline_applies_default_args_to_tool_call(monkeypatch): assert args.get("unit") == "c" +@pytest.mark.asyncio +async def test_generated_opener_prompt_uses_system_prompt_only(monkeypatch): + monkeypatch.setattr("core.duplex_pipeline.SileroVAD", _DummySileroVAD) + monkeypatch.setattr("core.duplex_pipeline.VADProcessor", _DummyVADProcessor) + monkeypatch.setattr("core.duplex_pipeline.EouDetector", _DummyEouDetector) + + llm = _CaptureGenerateLLM("你好") + pipeline = DuplexPipeline( + transport=_FakeTransport(), + session_id="s_generated_opener", + llm_service=llm, + tts_service=_FakeTTS(), + asr_service=_FakeASR(), + ) + pipeline.conversation.system_prompt = "SYSTEM_PROMPT_ONLY" + pipeline._runtime_greeting = "DEV_HINT_SHOULD_NOT_BE_USED" + + generated = await pipeline._generate_runtime_greeting() + + assert generated == "你好" + assert len(llm.messages) == 2 + user_prompt = llm.messages[1].content + assert "SYSTEM_PROMPT_ONLY" in user_prompt + assert "DEV_HINT_SHOULD_NOT_BE_USED" not in user_prompt + assert "额外风格提示" not in user_prompt + + @pytest.mark.asyncio async def test_ws_message_parses_tool_call_results(): msg = parse_client_message( diff --git a/web/pages/Assistants.tsx b/web/pages/Assistants.tsx index d83e7eb..c85ada5 100644 --- a/web/pages/Assistants.tsx +++ b/web/pages/Assistants.tsx @@ -889,25 +889,26 @@ export const AssistantsPage: React.FC = () => { -
{selectedAssistant.generatedOpenerEnabled === true
? '通话接通后将根据提示词自动生成开场白。'
@@ -2139,10 +2139,13 @@ export const DebugDrawer: React.FC<{
|| textSessionStarted;
const requiredTemplateVariableKeys = useMemo(() => {
const keys = new Set
- Use placeholders like {'{{customer_name}}'} in prompt/opener.
+ Use placeholders like {'{{customer_name}}'} in prompt{assistant.generatedOpenerEnabled === true ? '' : '/opener'}.
Built-in system vars: {'{{system__time}}'}, {'{{system_utc}}'}, {'{{system_timezone}}'}.