diff --git a/api/routes/user.py b/api/routes/user.py index 4a2caa4b..c9c443b3 100644 --- a/api/routes/user.py +++ b/api/routes/user.py @@ -10,6 +10,10 @@ from api.db.models import ( UserModel, ) from api.schemas.onboarding_state import OnboardingState, OnboardingStateUpdate +from api.schemas.workflow_configurations import ( + WorkflowConfigurationDefaults, + get_default_workflow_configurations, +) from api.services.auth.depends import get_user from api.services.configuration.ai_model_configuration import ( get_resolved_ai_model_configuration, @@ -40,13 +44,14 @@ class AuthUserResponse(TypedDict): is_superuser: bool -class DefaultConfigurationsResponse(TypedDict): +class DefaultConfigurationsResponse(BaseModel): llm: dict[str, dict] tts: dict[str, dict] stt: dict[str, dict] embeddings: dict[str, dict] realtime: dict[str, dict] default_providers: dict[str, str] + workflow_configurations: WorkflowConfigurationDefaults @router.get("/configurations/defaults") @@ -73,8 +78,9 @@ async def get_default_configurations() -> DefaultConfigurationsResponse: for provider, model_cls in REGISTRY[ServiceType.REALTIME].items() }, "default_providers": DEFAULT_SERVICE_PROVIDERS, + "workflow_configurations": get_default_workflow_configurations(), } - return configurations + return DefaultConfigurationsResponse(**configurations) @router.get("/auth/user") diff --git a/api/schemas/workflow_configurations.py b/api/schemas/workflow_configurations.py new file mode 100644 index 00000000..ae974c19 --- /dev/null +++ b/api/schemas/workflow_configurations.py @@ -0,0 +1,44 @@ +from typing import Literal + +from pydantic import BaseModel, ConfigDict, Field + +DEFAULT_MAX_CALL_DURATION_SECONDS = 300 +DEFAULT_MAX_USER_IDLE_TIMEOUT_SECONDS = 10.0 +DEFAULT_SMART_TURN_STOP_SECS = 2.0 +DEFAULT_TURN_START_STRATEGY = "default" +DEFAULT_TURN_START_MIN_WORDS = 3 +DEFAULT_PROVISIONAL_VAD_PAUSE_SECS = 1.5 +DEFAULT_TURN_STOP_STRATEGY = "transcription" +DEFAULT_CONTEXT_COMPACTION_ENABLED = False + + +class AmbientNoiseConfigurationDefaults(BaseModel): + model_config = ConfigDict(extra="allow") + + enabled: bool = False + volume: float = 0.3 + + +class WorkflowConfigurationDefaults(BaseModel): + model_config = ConfigDict(extra="allow") + + ambient_noise_configuration: AmbientNoiseConfigurationDefaults = Field( + default_factory=AmbientNoiseConfigurationDefaults + ) + max_call_duration: int = DEFAULT_MAX_CALL_DURATION_SECONDS + max_user_idle_timeout: float = DEFAULT_MAX_USER_IDLE_TIMEOUT_SECONDS + smart_turn_stop_secs: float = DEFAULT_SMART_TURN_STOP_SECS + turn_start_strategy: Literal["default", "min_words", "provisional_vad"] = ( + DEFAULT_TURN_START_STRATEGY + ) + turn_start_min_words: int = DEFAULT_TURN_START_MIN_WORDS + provisional_vad_pause_secs: float = DEFAULT_PROVISIONAL_VAD_PAUSE_SECS + turn_stop_strategy: Literal["transcription", "turn_analyzer"] = ( + DEFAULT_TURN_STOP_STRATEGY + ) + dictionary: str = "" + context_compaction_enabled: bool = DEFAULT_CONTEXT_COMPACTION_ENABLED + + +def get_default_workflow_configurations() -> WorkflowConfigurationDefaults: + return WorkflowConfigurationDefaults() diff --git a/api/services/pipecat/pipeline_engine_callbacks_processor.py b/api/services/pipecat/pipeline_engine_callbacks_processor.py index 8c048809..71fbe28b 100644 --- a/api/services/pipecat/pipeline_engine_callbacks_processor.py +++ b/api/services/pipecat/pipeline_engine_callbacks_processor.py @@ -3,6 +3,7 @@ from typing import Awaitable, Callable, Optional from loguru import logger +from api.schemas.workflow_configurations import DEFAULT_MAX_CALL_DURATION_SECONDS from pipecat.frames.frames import ( Frame, HeartbeatFrame, @@ -23,7 +24,7 @@ class PipelineEngineCallbacksProcessor(FrameProcessor): def __init__( self, - max_call_duration_seconds: int = 300, + max_call_duration_seconds: int = DEFAULT_MAX_CALL_DURATION_SECONDS, max_duration_end_task_callback: Optional[Callable[[], Awaitable[None]]] = None, generation_started_callback: Optional[Callable[[], Awaitable[None]]] = None, llm_text_frame_callback: Optional[Callable[[str], Awaitable[None]]] = None, diff --git a/api/services/pipecat/run_pipeline.py b/api/services/pipecat/run_pipeline.py index 18dbeaa3..4bfedc7c 100644 --- a/api/services/pipecat/run_pipeline.py +++ b/api/services/pipecat/run_pipeline.py @@ -6,6 +6,14 @@ from loguru import logger from api.db import db_client from api.enums import WorkflowRunMode +from api.schemas.workflow_configurations import ( + DEFAULT_MAX_CALL_DURATION_SECONDS, + DEFAULT_MAX_USER_IDLE_TIMEOUT_SECONDS, + DEFAULT_PROVISIONAL_VAD_PAUSE_SECS, + DEFAULT_SMART_TURN_STOP_SECS, + DEFAULT_TURN_START_MIN_WORDS, + DEFAULT_TURN_START_STRATEGY, +) from api.services.configuration.registry import ServiceProviders from api.services.integrations import ( IntegrationRuntimeContext, @@ -100,10 +108,6 @@ ensure_tracing() DEFAULT_USER_TURN_STOP_TIMEOUT = 5.0 EXTERNAL_TURN_USER_STOP_TIMEOUT = 30.0 -DEFAULT_TURN_START_STRATEGY = "default" -DEFAULT_TURN_START_MIN_WORDS = 3 -DEFAULT_PROVISIONAL_VAD_PAUSE_SECS = 1.5 -DEFAULT_SMART_TURN_STOP_SECS = 2.0 def _resolve_user_turn_stop_timeout( @@ -538,8 +542,8 @@ async def _run_pipeline_impl( run_configs = run_definition.workflow_configurations or {} # Extract configurations from the version's workflow_configurations - max_call_duration_seconds = 300 # Default 5 minutes - max_user_idle_timeout = 10.0 # Default 10 seconds + max_call_duration_seconds = DEFAULT_MAX_CALL_DURATION_SECONDS + max_user_idle_timeout = DEFAULT_MAX_USER_IDLE_TIMEOUT_SECONDS keyterms = None # Dictionary words for STT boosting if run_configs: diff --git a/ui/src/app/workflow/[workflowId]/RenderWorkflow.tsx b/ui/src/app/workflow/[workflowId]/RenderWorkflow.tsx index eed634b9..55638dc2 100644 --- a/ui/src/app/workflow/[workflowId]/RenderWorkflow.tsx +++ b/ui/src/app/workflow/[workflowId]/RenderWorkflow.tsx @@ -443,7 +443,7 @@ function RenderWorkflow({ const renameWorkflow = useCallback(async (newName: string) => { // The header doesn't render the pencil until the page has mounted with // initial data, so workflowConfigurations is non-null by the time this - // runs. Throw rather than silently sending DEFAULT_WORKFLOW_CONFIGURATIONS, + // runs. Throw rather than silently sending fallback workflow configurations, // which would overwrite the saved server-side config. if (!workflowConfigurations) { throw new Error("Workflow configurations not loaded"); diff --git a/ui/src/app/workflow/[workflowId]/components/ConfigurationsDialog.tsx b/ui/src/app/workflow/[workflowId]/components/ConfigurationsDialog.tsx index 4ee6a7db..2d288a61 100644 --- a/ui/src/app/workflow/[workflowId]/components/ConfigurationsDialog.tsx +++ b/ui/src/app/workflow/[workflowId]/components/ConfigurationsDialog.tsx @@ -10,6 +10,7 @@ import { AmbientNoiseConfiguration, DEFAULT_PROVISIONAL_VAD_PAUSE_SECS, DEFAULT_TURN_START_MIN_WORDS, + resolveWorkflowConfigurations, TURN_START_STRATEGY_OPTIONS, TurnStartStrategy, TurnStopStrategy, @@ -24,11 +25,6 @@ interface ConfigurationsDialogProps { onSave: (configurations: WorkflowConfigurations, workflowName: string) => Promise; } -const DEFAULT_AMBIENT_NOISE_CONFIG: AmbientNoiseConfiguration = { - enabled: false, - volume: 0.3, -}; - export const ConfigurationsDialog = ({ open, onOpenChange, @@ -36,33 +32,34 @@ export const ConfigurationsDialog = ({ workflowName, onSave }: ConfigurationsDialogProps) => { + const resolvedWorkflowConfigurations = resolveWorkflowConfigurations(workflowConfigurations); const [name, setName] = useState(workflowName); const [ambientNoiseConfig, setAmbientNoiseConfig] = useState( - workflowConfigurations?.ambient_noise_configuration || DEFAULT_AMBIENT_NOISE_CONFIG + resolvedWorkflowConfigurations.ambient_noise_configuration ); const [maxCallDuration, setMaxCallDuration] = useState( - workflowConfigurations?.max_call_duration || 600 // Default 10 minutes + resolvedWorkflowConfigurations.max_call_duration ); const [maxUserIdleTimeout, setMaxUserIdleTimeout] = useState( - workflowConfigurations?.max_user_idle_timeout || 10 // Default 10 seconds + resolvedWorkflowConfigurations.max_user_idle_timeout ); const [smartTurnStopSecs, setSmartTurnStopSecs] = useState( - workflowConfigurations?.smart_turn_stop_secs || 2 // Default 2 seconds + resolvedWorkflowConfigurations.smart_turn_stop_secs ); const [turnStartStrategy, setTurnStartStrategy] = useState( - workflowConfigurations?.turn_start_strategy || 'default' + resolvedWorkflowConfigurations.turn_start_strategy ); const [turnStartMinWords, setTurnStartMinWords] = useState( - workflowConfigurations?.turn_start_min_words || DEFAULT_TURN_START_MIN_WORDS + resolvedWorkflowConfigurations.turn_start_min_words ); const [provisionalVadPauseSecs, setProvisionalVadPauseSecs] = useState( - workflowConfigurations?.provisional_vad_pause_secs || DEFAULT_PROVISIONAL_VAD_PAUSE_SECS + resolvedWorkflowConfigurations.provisional_vad_pause_secs ); const [turnStopStrategy, setTurnStopStrategy] = useState( - workflowConfigurations?.turn_stop_strategy || 'transcription' + resolvedWorkflowConfigurations.turn_stop_strategy ); const [contextCompactionEnabled, setContextCompactionEnabled] = useState( - workflowConfigurations?.context_compaction_enabled ?? false + resolvedWorkflowConfigurations.context_compaction_enabled ); const [isSaving, setIsSaving] = useState(false); const selectedTurnStartStrategy = TURN_START_STRATEGY_OPTIONS.find( @@ -94,16 +91,17 @@ export const ConfigurationsDialog = ({ // Sync state with props when dialog opens useEffect(() => { if (open) { + const nextWorkflowConfigurations = resolveWorkflowConfigurations(workflowConfigurations); setName(workflowName); - setAmbientNoiseConfig(workflowConfigurations?.ambient_noise_configuration || DEFAULT_AMBIENT_NOISE_CONFIG); - setMaxCallDuration(workflowConfigurations?.max_call_duration || 600); - setMaxUserIdleTimeout(workflowConfigurations?.max_user_idle_timeout || 10); - setSmartTurnStopSecs(workflowConfigurations?.smart_turn_stop_secs || 2); - setTurnStartStrategy(workflowConfigurations?.turn_start_strategy || 'default'); - setTurnStartMinWords(workflowConfigurations?.turn_start_min_words || DEFAULT_TURN_START_MIN_WORDS); - setProvisionalVadPauseSecs(workflowConfigurations?.provisional_vad_pause_secs || DEFAULT_PROVISIONAL_VAD_PAUSE_SECS); - setTurnStopStrategy(workflowConfigurations?.turn_stop_strategy || 'transcription'); - setContextCompactionEnabled(workflowConfigurations?.context_compaction_enabled ?? false); + setAmbientNoiseConfig(nextWorkflowConfigurations.ambient_noise_configuration); + setMaxCallDuration(nextWorkflowConfigurations.max_call_duration); + setMaxUserIdleTimeout(nextWorkflowConfigurations.max_user_idle_timeout); + setSmartTurnStopSecs(nextWorkflowConfigurations.smart_turn_stop_secs); + setTurnStartStrategy(nextWorkflowConfigurations.turn_start_strategy); + setTurnStartMinWords(nextWorkflowConfigurations.turn_start_min_words); + setProvisionalVadPauseSecs(nextWorkflowConfigurations.provisional_vad_pause_secs); + setTurnStopStrategy(nextWorkflowConfigurations.turn_stop_strategy); + setContextCompactionEnabled(nextWorkflowConfigurations.context_compaction_enabled); } }, [open, workflowName, workflowConfigurations]); @@ -244,6 +242,16 @@ export const ConfigurationsDialog = ({

)} + + + {/* Interruption Section */} +
+
+

Interruption

+

+ Configure when user speech should interrupt the agent while it is speaking. +

+
)} +
+ + + + {/* Interruption */} +
+
+

Interruption

+

+ Configure when user speech should interrupt the agent while it is speaking. +

+