Add turn configuration support for assistants
- Introduce a new `turnConfig` field in `AssistantConfig` and `Assistant` models to manage user interaction settings. - Implement `TurnConfig`, `BargeInConfig`, `VadConfig`, and `TurnDetectionConfig` schemas to define turn management strategies. - Update the backend to handle turn configuration in the database and during assistant operations. - Enhance frontend components with a `TurnConfigEditor` for configuring turn settings, including VAD and barge-in strategies. - Modify existing pages to integrate turn configuration, improving user experience and interaction capabilities.
This commit is contained in:
@@ -108,6 +108,7 @@ class Assistant(Base):
|
||||
runtime_mode: Mapped[str] = mapped_column(String(16), default="pipeline")
|
||||
greeting: Mapped[str] = mapped_column(String(2048), default="")
|
||||
enable_interrupt: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
turn_config: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||
vision_enabled: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
vision_model_resource_id: Mapped[str | None] = mapped_column(
|
||||
String(40),
|
||||
|
||||
25
backend/migrations/versions/20260712_0004_add_turn_config.py
Normal file
25
backend/migrations/versions/20260712_0004_add_turn_config.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""add assistant turn configuration
|
||||
|
||||
Revision ID: 20260712_0004
|
||||
Revises: 20260710_0003
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = "20260712_0004"
|
||||
down_revision = "20260710_0003"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"assistants",
|
||||
sa.Column("turn_config", sa.JSON(), server_default=sa.text("'{}'"), nullable=False),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("assistants", "turn_config")
|
||||
@@ -71,6 +71,7 @@ class AssistantConfig(BaseModel):
|
||||
tts_secrets: dict = {}
|
||||
|
||||
enableInterrupt: bool = True
|
||||
turnConfig: dict = Field(default_factory=dict)
|
||||
|
||||
# Prompt assistant reusable tools. Execution remains type-specific in the pipeline.
|
||||
tools: list[RuntimeTool] = Field(default_factory=list)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# webrtc -> SmallWebRTCTransport / SmallWebRTCConnection + aiortc
|
||||
# silero -> 本地 VAD(判断用户说话起止),语音必备
|
||||
# openai -> OpenAI 兼容的 LLM/STT/TTS 客户端(DeepSeek、SenseVoice、CosyVoice 都走它)
|
||||
pipecat-ai[webrtc,websocket,silero,openai]==1.3.0
|
||||
pipecat-ai[webrtc,websocket,silero,openai]==1.4.0
|
||||
Pillow>=11.1.0,<13
|
||||
|
||||
# FastGPT 类型助手:本地 SDK(包 /api/v1/chat/completions 流式 + chatId 会话)
|
||||
|
||||
@@ -137,6 +137,7 @@ async def _to_out(session: AsyncSession, assistant: Assistant) -> AssistantOut:
|
||||
runtime_mode=assistant.runtime_mode, # type: ignore[arg-type]
|
||||
greeting=assistant.greeting,
|
||||
enable_interrupt=assistant.enable_interrupt,
|
||||
turn_config=assistant.turn_config or {},
|
||||
vision_enabled=assistant.vision_enabled,
|
||||
vision_model_resource_id=assistant.vision_model_resource_id,
|
||||
model_resource_ids=await _resource_ids(session, assistant.id),
|
||||
@@ -202,6 +203,7 @@ async def duplicate_assistant(
|
||||
runtime_mode=source.runtime_mode,
|
||||
greeting=source.greeting,
|
||||
enable_interrupt=source.enable_interrupt,
|
||||
turn_config=dict(source.turn_config or {}),
|
||||
vision_enabled=source.vision_enabled,
|
||||
vision_model_resource_id=source.vision_model_resource_id,
|
||||
knowledge_base_id=source.knowledge_base_id,
|
||||
|
||||
@@ -16,6 +16,7 @@ from pydantic.alias_generators import to_camel
|
||||
RuntimeMode = Literal["pipeline", "realtime"]
|
||||
ModelType = Literal["LLM", "ASR", "TTS", "Realtime", "Embedding", "Agent"]
|
||||
AssistantType = Literal["prompt", "workflow", "dify", "fastgpt", "opencode"]
|
||||
TurnEndStrategy = Literal["silence", "smart_turn"]
|
||||
ToolType = Literal["end_call", "http"]
|
||||
ToolStatus = Literal["active", "archived", "draft"]
|
||||
ToolParameterType = Literal["string", "number", "integer", "boolean", "object", "array"]
|
||||
@@ -40,6 +41,27 @@ class CamelModel(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class BargeInConfig(CamelModel):
|
||||
strategy: Literal["vad", "transcription"] = "vad"
|
||||
|
||||
|
||||
class VadConfig(CamelModel):
|
||||
confidence: float = Field(default=0.7, ge=0.0, le=1.0)
|
||||
start_secs: float = Field(default=0.2, ge=0.05, le=2.0)
|
||||
stop_secs: float = Field(default=0.2, ge=0.05, le=3.0)
|
||||
min_volume: float = Field(default=0.6, ge=0.0, le=1.0)
|
||||
|
||||
class TurnDetectionConfig(CamelModel):
|
||||
strategy: TurnEndStrategy = "silence"
|
||||
silence_timeout_secs: float = Field(default=0.6, ge=0.2, le=5.0)
|
||||
|
||||
|
||||
class TurnConfig(CamelModel):
|
||||
barge_in: BargeInConfig = Field(default_factory=BargeInConfig)
|
||||
vad: VadConfig = Field(default_factory=VadConfig)
|
||||
turn_detection: TurnDetectionConfig = Field(default_factory=TurnDetectionConfig)
|
||||
|
||||
|
||||
# 各 type 允许的瘦字段(其余字段写入时清零,防止跨类型脏数据)
|
||||
ALLOWED_FIELDS: dict[str, set[str]] = {
|
||||
"prompt": {"prompt"},
|
||||
@@ -57,6 +79,7 @@ class AssistantUpsert(CamelModel):
|
||||
runtime_mode: RuntimeMode = "pipeline"
|
||||
greeting: str = ""
|
||||
enable_interrupt: bool = True
|
||||
turn_config: TurnConfig = Field(default_factory=TurnConfig)
|
||||
vision_enabled: bool = False
|
||||
vision_model_resource_id: str | None = None
|
||||
|
||||
|
||||
@@ -135,6 +135,7 @@ async def resolve_runtime_config(
|
||||
prompt=assistant.prompt or "你是一个有帮助的助手。",
|
||||
runtimeMode=assistant.runtime_mode, # type: ignore[arg-type]
|
||||
enableInterrupt=assistant.enable_interrupt,
|
||||
turnConfig=assistant.turn_config or {},
|
||||
tools=await _tools_for(session, assistant),
|
||||
# workflow 图:仅 workflow 类型非空,引擎据此启用图驱动对话
|
||||
graph=(assistant.graph or {}) if assistant.type == "workflow" else {},
|
||||
|
||||
@@ -30,7 +30,6 @@ from services.pipecat.service_factory import (
|
||||
|
||||
from pipecat.adapters.schemas.function_schema import FunctionSchema
|
||||
from pipecat.adapters.schemas.tools_schema import ToolsSchema
|
||||
from pipecat.audio.vad.silero import SileroVADAnalyzer
|
||||
from pipecat.frames.frames import (
|
||||
EndFrame,
|
||||
InputTransportMessageFrame,
|
||||
@@ -59,15 +58,14 @@ from pipecat.runner.utils import (
|
||||
maybe_capture_participant_camera,
|
||||
)
|
||||
from pipecat.services.llm_service import FunctionCallParams
|
||||
from pipecat.turns.user_start import (
|
||||
TranscriptionUserTurnStartStrategy,
|
||||
VADUserTurnStartStrategy,
|
||||
)
|
||||
from pipecat.turns.user_mute.base_user_mute_strategy import BaseUserMuteStrategy
|
||||
from pipecat.turns.user_mute.function_call_user_mute_strategy import (
|
||||
FunctionCallUserMuteStrategy,
|
||||
)
|
||||
from pipecat.turns.user_turn_strategies import UserTurnStrategies
|
||||
from services.pipecat.turn_config import (
|
||||
create_user_turn_strategies,
|
||||
create_vad_analyzer,
|
||||
)
|
||||
from pipecat.utils.time import time_now_iso8601
|
||||
from pipecat.workers.runner import WorkerRunner
|
||||
|
||||
@@ -448,18 +446,14 @@ async def run_pipeline(
|
||||
user_aggregator = LLMUserAggregator(
|
||||
context,
|
||||
params=LLMUserAggregatorParams(
|
||||
vad_analyzer=SileroVADAnalyzer(),
|
||||
vad_analyzer=create_vad_analyzer(cfg.turnConfig),
|
||||
user_mute_strategies=[
|
||||
FunctionCallUserMuteStrategy(),
|
||||
CallEndingUserMuteStrategy(lambda: call_end.ending),
|
||||
],
|
||||
user_turn_strategies=UserTurnStrategies(
|
||||
start=[
|
||||
VADUserTurnStartStrategy(enable_interruptions=cfg.enableInterrupt),
|
||||
TranscriptionUserTurnStartStrategy(
|
||||
enable_interruptions=cfg.enableInterrupt
|
||||
),
|
||||
]
|
||||
user_turn_strategies=create_user_turn_strategies(
|
||||
cfg.turnConfig,
|
||||
enable_interruptions=cfg.enableInterrupt,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
89
backend/services/pipecat/turn_config.py
Normal file
89
backend/services/pipecat/turn_config.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""把稳定的产品配置映射为 Pipecat 用户轮次策略。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pipecat.audio.turn.smart_turn.local_smart_turn_v3 import LocalSmartTurnAnalyzerV3
|
||||
from pipecat.audio.vad.silero import SileroVADAnalyzer
|
||||
from pipecat.audio.vad.vad_analyzer import VADParams
|
||||
from pipecat.turns.user_start import (
|
||||
TranscriptionUserTurnStartStrategy,
|
||||
VADUserTurnStartStrategy,
|
||||
)
|
||||
from pipecat.turns.user_stop import (
|
||||
SpeechTimeoutUserTurnStopStrategy,
|
||||
TurnAnalyzerUserTurnStopStrategy,
|
||||
)
|
||||
from pipecat.turns.user_turn_strategies import UserTurnStrategies
|
||||
|
||||
|
||||
DEFAULT_VAD = {
|
||||
"confidence": 0.7,
|
||||
"start_secs": 0.2,
|
||||
"stop_secs": 0.2,
|
||||
"min_volume": 0.6,
|
||||
}
|
||||
DEFAULT_TURN_DETECTION = {
|
||||
"strategy": "silence",
|
||||
"silence_timeout_secs": 0.6,
|
||||
}
|
||||
|
||||
|
||||
def _section(config: dict[str, Any], snake: str, camel: str) -> dict[str, Any]:
|
||||
value = config.get(snake, config.get(camel, {}))
|
||||
return value if isinstance(value, dict) else {}
|
||||
|
||||
|
||||
def _value(config: dict[str, Any], snake: str, camel: str, default: Any) -> Any:
|
||||
return config.get(snake, config.get(camel, default))
|
||||
|
||||
|
||||
def create_vad_analyzer(turn_config: dict[str, Any]) -> SileroVADAnalyzer:
|
||||
vad = _section(turn_config, "vad", "vad")
|
||||
return SileroVADAnalyzer(
|
||||
params=VADParams(
|
||||
confidence=float(vad.get("confidence", DEFAULT_VAD["confidence"])),
|
||||
start_secs=float(_value(vad, "start_secs", "startSecs", 0.2)),
|
||||
stop_secs=float(_value(vad, "stop_secs", "stopSecs", 0.2)),
|
||||
min_volume=float(_value(vad, "min_volume", "minVolume", 0.6)),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_user_turn_strategies(
|
||||
turn_config: dict[str, Any], *, enable_interruptions: bool
|
||||
) -> UserTurnStrategies:
|
||||
barge_in = _section(turn_config, "barge_in", "bargeIn")
|
||||
start = []
|
||||
strategy = barge_in.get("strategy", "vad")
|
||||
if strategy == "vad":
|
||||
start.append(VADUserTurnStartStrategy(enable_interruptions=enable_interruptions))
|
||||
else:
|
||||
start.append(
|
||||
TranscriptionUserTurnStartStrategy(enable_interruptions=enable_interruptions)
|
||||
)
|
||||
|
||||
detection = _section(turn_config, "turn_detection", "turnDetection")
|
||||
if detection.get("strategy", DEFAULT_TURN_DETECTION["strategy"]) == "smart_turn":
|
||||
stop = [
|
||||
TurnAnalyzerUserTurnStopStrategy(
|
||||
turn_analyzer=LocalSmartTurnAnalyzerV3(),
|
||||
wait_for_transcript=True,
|
||||
)
|
||||
]
|
||||
else:
|
||||
stop = [
|
||||
SpeechTimeoutUserTurnStopStrategy(
|
||||
user_speech_timeout=float(
|
||||
_value(
|
||||
detection,
|
||||
"silence_timeout_secs",
|
||||
"silenceTimeoutSecs",
|
||||
0.6,
|
||||
)
|
||||
),
|
||||
wait_for_transcript=True,
|
||||
)
|
||||
]
|
||||
return UserTurnStrategies(start=start, stop=stop)
|
||||
Reference in New Issue
Block a user