Use FastGPT app opener for greetings

This commit is contained in:
Xin Wang
2026-05-29 15:34:33 +08:00
parent ce3c384b00
commit 15c98f21fe
9 changed files with 30 additions and 50 deletions

View File

@@ -45,9 +45,7 @@
"user_speech_timeout_sec": 0.2 "user_speech_timeout_sec": 0.2
}, },
"agent": { "agent": {
"system_prompt": "FastGPT app owns the system prompt when send_system_prompt is false.", "greeting_mode": "fastgpt_opener"
"greeting": "您好,这里是无锡交警,我将为您远程处理交通事故。请将人员撤离至路侧安全区域,开启危险报警双闪灯、放置三角警告牌、做好安全防护,谨防二次事故伤害。若您已经准备好了,请点击继续办理,如需人工服务,请说转人工。",
"greeting_mode": "fixed"
}, },
"services": { "services": {
"stt": { "stt": {
@@ -72,8 +70,7 @@
"chat_id": null, "chat_id": null,
"variables": {}, "variables": {},
"detail": false, "detail": false,
"timeout_sec": 60.0, "timeout_sec": 60.0
"send_system_prompt": false
}, },
"tts": { "tts": {
"provider": "xfyun", "provider": "xfyun",

View File

@@ -48,9 +48,7 @@
"idle_prompt_text": "你好,请问还在吗?" "idle_prompt_text": "你好,请问还在吗?"
}, },
"agent": { "agent": {
"system_prompt": "FastGPT app owns the system prompt when send_system_prompt is false.", "greeting_mode": "fastgpt_opener",
"greeting": "您好,这里是无锡交警,我将为您远程处理交通事故。请将人员撤离至路侧安全区域,开启危险报警双闪灯、放置三角警告牌、做好安全防护,谨防二次事故伤害。若您已经准备好了,请点击继续办理,如需人工服务,请说转人工。",
"greeting_mode": "fixed",
"response_state": { "response_state": {
"enabled": true, "enabled": true,
"tag": "state", "tag": "state",
@@ -81,8 +79,7 @@
"chat_id": null, "chat_id": null,
"variables": {}, "variables": {},
"detail": false, "detail": false,
"timeout_sec": 60.0, "timeout_sec": 60.0
"send_system_prompt": false
}, },
"tts": { "tts": {
"provider": "xfyun_super", "provider": "xfyun_super",

View File

@@ -55,9 +55,7 @@
"user_speech_timeout_sec": 0.2 "user_speech_timeout_sec": 0.2
}, },
"agent": { "agent": {
"system_prompt": "FastGPT app owns the system prompt when send_system_prompt is false.", "greeting_mode": "fastgpt_opener",
"greeting": "您好,这里是无锡交警,我将为您远程处理交通事故。请将人员撤离至路侧安全区域,开启危险报警双闪灯、放置三角警告牌、做好安全防护,谨防二次事故伤害。若您已经准备好了,请点击继续办理,如需人工服务,请说转人工。",
"greeting_mode": "fixed",
"response_state": { "response_state": {
"enabled": false, "enabled": false,
"tag": "state", "tag": "state",
@@ -88,8 +86,7 @@
"chat_id": null, "chat_id": null,
"variables": {}, "variables": {},
"detail": false, "detail": false,
"timeout_sec": 60.0, "timeout_sec": 60.0
"send_system_prompt": false
}, },
"tts": { "tts": {
"provider": "xfyun", "provider": "xfyun",

View File

@@ -45,9 +45,7 @@
"user_speech_timeout_sec": 0.2 "user_speech_timeout_sec": 0.2
}, },
"agent": { "agent": {
"system_prompt": "FastGPT app owns the system prompt when send_system_prompt is false.", "greeting_mode": "fastgpt_opener",
"greeting": "您好,这里是无锡交警,我将为您远程处理交通事故。请将人员撤离至路侧安全区域,开启危险报警双闪灯、放置三角警告牌、做好安全防护,谨防二次事故伤害。若您已经准备好了,请点击继续办理,如需人工服务,请说转人工。",
"greeting_mode": "fixed",
"response_state": { "response_state": {
"enabled": false, "enabled": false,
"tag": "state", "tag": "state",
@@ -78,8 +76,7 @@
"chat_id": null, "chat_id": null,
"variables": {}, "variables": {},
"detail": false, "detail": false,
"timeout_sec": 60.0, "timeout_sec": 60.0
"send_system_prompt": false
}, },
"tts": { "tts": {
"provider": "xfyun", "provider": "xfyun",

View File

@@ -48,9 +48,7 @@
"idle_prompt_text": "你好,请问还在吗?" "idle_prompt_text": "你好,请问还在吗?"
}, },
"agent": { "agent": {
"system_prompt": "FastGPT app owns the system prompt when send_system_prompt is false.", "greeting_mode": "fastgpt_opener",
"greeting": "您好,这里是无锡交警,我将为您远程处理交通事故。请将人员撤离至路侧安全区域,开启危险报警双闪灯、放置三角警告牌、做好安全防护,谨防二次事故伤害。若您已经准备好了,请点击继续办理,如需人工服务,请说转人工。",
"greeting_mode": "fixed",
"response_state": { "response_state": {
"enabled": false, "enabled": false,
"tag": "state", "tag": "state",
@@ -81,8 +79,7 @@
"chat_id": null, "chat_id": null,
"variables": {}, "variables": {},
"detail": false, "detail": false,
"timeout_sec": 60.0, "timeout_sec": 60.0
"send_system_prompt": false
}, },
"tts": { "tts": {
"provider": "xfyun_super", "provider": "xfyun_super",

View File

@@ -147,7 +147,6 @@ class LLMConfig:
variables: dict[str, str] = field(default_factory=dict) variables: dict[str, str] = field(default_factory=dict)
detail: bool = False detail: bool = False
timeout_sec: float = 60.0 timeout_sec: float = 60.0
send_system_prompt: bool = False
@property @property
def is_fastgpt(self) -> bool: def is_fastgpt(self) -> bool:
@@ -160,7 +159,7 @@ class LLMConfig:
@property @property
def uses_local_context_history(self) -> bool: def uses_local_context_history(self) -> bool:
"""Whether the pipeline should seed and maintain local LLM context history.""" """Whether the pipeline should seed and maintain local LLM context history."""
return not self.is_fastgpt or self.send_system_prompt return not self.is_fastgpt
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -233,9 +232,11 @@ def config_from_dict(data: dict) -> EngineConfig:
agent = _dict(data.get("agent")) agent = _dict(data.get("agent"))
if agent.get("greeting") == "": if agent.get("greeting") == "":
agent["greeting"] = None agent["greeting"] = None
if agent.get("greeting_mode") not in (None, "generated", "fixed", "off"): if agent.get("greeting_mode") not in (None, "generated", "fixed", "off", "fastgpt_opener"):
raise ValueError("agent.greeting_mode must be one of: generated, fixed, off") raise ValueError(
response_state = ResponseStateConfig(**_dict(agent.pop("response_state"))) "agent.greeting_mode must be one of: generated, fixed, off, fastgpt_opener"
)
response_state = ResponseStateConfig(**_dict(agent.pop("response_state", None)))
if response_state.max_prefix_chars < 1: if response_state.max_prefix_chars < 1:
raise ValueError("agent.response_state.max_prefix_chars must be greater than 0") raise ValueError("agent.response_state.max_prefix_chars must be greater than 0")
if not response_state.tag: if not response_state.tag:
@@ -255,6 +256,10 @@ def config_from_dict(data: dict) -> EngineConfig:
llm["app_id"] = None llm["app_id"] = None
if not isinstance(llm.get("variables"), dict): if not isinstance(llm.get("variables"), dict):
llm["variables"] = {} llm["variables"] = {}
if agent.get("greeting_mode") == "fastgpt_opener" and llm["provider"] != "fastgpt":
raise ValueError(
"agent.greeting_mode='fastgpt_opener' requires services.llm.provider='fastgpt'"
)
turn = _dict(data.get("turn")) turn = _dict(data.get("turn"))
vad = _dict(turn.get("vad")) vad = _dict(turn.get("vad"))

View File

@@ -170,7 +170,6 @@ class FastGPTLLMService(LLMService):
base_url: str, base_url: str,
chat_id: str | None = None, chat_id: str | None = None,
app_id: str | None = None, app_id: str | None = None,
send_system_prompt: bool = False,
greeting_prompt: str | None = None, greeting_prompt: str | None = None,
timeout: float = 60.0, timeout: float = 60.0,
settings: FastGPTLLMSettings | None = None, settings: FastGPTLLMSettings | None = None,
@@ -183,7 +182,6 @@ class FastGPTLLMService(LLMService):
self._chat_id = chat_id or f"voice_{uuid.uuid4().hex[:16]}" self._chat_id = chat_id or f"voice_{uuid.uuid4().hex[:16]}"
self._app_id = (app_id or "").strip() self._app_id = (app_id or "").strip()
self._send_system_prompt = send_system_prompt
self._greeting_prompt = (greeting_prompt or "你好").strip() or "你好" self._greeting_prompt = (greeting_prompt or "你好").strip() or "你好"
self._client = AsyncChatClient( self._client = AsyncChatClient(
api_key=api_key, api_key=api_key,
@@ -246,6 +244,8 @@ class FastGPTLLMService(LLMService):
return _first_nonempty_text( return _first_nonempty_text(
chat_config.get("welcomeText"), chat_config.get("welcomeText"),
app_payload.get("welcomeText"), app_payload.get("welcomeText"),
app_payload.get("opener"),
app_payload.get("intro"),
) )
async def fetch_welcome_text(self) -> str | None: async def fetch_welcome_text(self) -> str | None:
@@ -261,7 +261,7 @@ class FastGPTLLMService(LLMService):
response.raise_for_status() response.raise_for_status()
text = self._welcome_text_from_init_payload(response.json()) text = self._welcome_text_from_init_payload(response.json())
if text: if text:
logger.info(f"FastGPT welcomeText loaded for appId={self._app_id}") logger.info(f"FastGPT app opener loaded for appId={self._app_id}")
return text or None return text or None
except FastGPTError as exc: except FastGPTError as exc:
logger.warning(f"FastGPT chat init failed: {exc}") logger.warning(f"FastGPT chat init failed: {exc}")
@@ -279,26 +279,15 @@ class FastGPTLLMService(LLMService):
def _build_fastgpt_messages(self, context: LLMContext) -> list[dict[str, str]]: def _build_fastgpt_messages(self, context: LLMContext) -> list[dict[str, str]]:
raw_messages = context.get_messages() raw_messages = context.get_messages()
messages: list[dict[str, str]] = []
if self._send_system_prompt:
for message in raw_messages:
if not isinstance(message, dict) or message.get("role") != "system":
continue
text = _message_text(message)
if text:
messages.append({"role": "system", "content": text})
for message in reversed(raw_messages): for message in reversed(raw_messages):
if not isinstance(message, dict) or message.get("role") != "user": if not isinstance(message, dict) or message.get("role") != "user":
continue continue
text = _message_text(message) text = _message_text(message)
if text: if text:
messages.append({"role": "user", "content": text}) return [{"role": "user", "content": text}]
return messages
messages.append({"role": "user", "content": self._greeting_prompt}) return [{"role": "user", "content": self._greeting_prompt}]
return messages
async def _process_context(self, context: LLMContext) -> None: async def _process_context(self, context: LLMContext) -> None:
messages = self._build_fastgpt_messages(context) messages = self._build_fastgpt_messages(context)

View File

@@ -196,15 +196,17 @@ async def run_pipeline_with_serializer(
logger.info(f"{client_label} websocket client connected") logger.info(f"{client_label} websocket client connected")
if config.agent.greeting_mode == "fixed" and config.agent.greeting: if config.agent.greeting_mode == "fixed" and config.agent.greeting:
await task.queue_frames([TTSSpeakFrame(config.agent.greeting)]) await task.queue_frames([TTSSpeakFrame(config.agent.greeting)])
elif config.agent.greeting_mode == "generated": elif config.agent.greeting_mode == "fastgpt_opener":
if isinstance(llm, FastGPTLLMService): if isinstance(llm, FastGPTLLMService):
welcome = await llm.fetch_welcome_text() welcome = await llm.fetch_welcome_text()
if welcome: if welcome:
await task.queue_frames([TTSSpeakFrame(welcome)]) await task.queue_frames([TTSSpeakFrame(welcome)])
else: else:
await task.queue_frames([LLMRunFrame()]) logger.warning("FastGPT opener requested but no opener text was returned")
else: else:
await task.queue_frames([LLMRunFrame()]) raise RuntimeError("agent.greeting_mode='fastgpt_opener' requires FastGPT LLM service")
elif config.agent.greeting_mode == "generated":
await task.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected") @transport.event_handler("on_client_disconnected")
async def on_client_disconnected(_transport, _client): async def on_client_disconnected(_transport, _client):

View File

@@ -63,7 +63,6 @@ def create_llm_service(
base_url=config.base_url or "http://localhost:3000", base_url=config.base_url or "http://localhost:3000",
chat_id=chat_id or config.chat_id, chat_id=chat_id or config.chat_id,
app_id=config.app_id, app_id=config.app_id,
send_system_prompt=config.send_system_prompt,
greeting_prompt=greeting_prompt, greeting_prompt=greeting_prompt,
timeout=config.timeout_sec, timeout=config.timeout_sec,
settings=FastGPTLLMSettings( settings=FastGPTLLMSettings(