feat: client gen default configurations (#499)

This commit is contained in:
Abhishek 2026-07-04 18:37:50 +05:30 committed by GitHub
parent a9947cec04
commit ceb01a16a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 353 additions and 87 deletions

View file

@ -10,6 +10,10 @@ from api.db.models import (
UserModel, UserModel,
) )
from api.schemas.onboarding_state import OnboardingState, OnboardingStateUpdate 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.auth.depends import get_user
from api.services.configuration.ai_model_configuration import ( from api.services.configuration.ai_model_configuration import (
get_resolved_ai_model_configuration, get_resolved_ai_model_configuration,
@ -40,13 +44,14 @@ class AuthUserResponse(TypedDict):
is_superuser: bool is_superuser: bool
class DefaultConfigurationsResponse(TypedDict): class DefaultConfigurationsResponse(BaseModel):
llm: dict[str, dict] llm: dict[str, dict]
tts: dict[str, dict] tts: dict[str, dict]
stt: dict[str, dict] stt: dict[str, dict]
embeddings: dict[str, dict] embeddings: dict[str, dict]
realtime: dict[str, dict] realtime: dict[str, dict]
default_providers: dict[str, str] default_providers: dict[str, str]
workflow_configurations: WorkflowConfigurationDefaults
@router.get("/configurations/defaults") @router.get("/configurations/defaults")
@ -73,8 +78,9 @@ async def get_default_configurations() -> DefaultConfigurationsResponse:
for provider, model_cls in REGISTRY[ServiceType.REALTIME].items() for provider, model_cls in REGISTRY[ServiceType.REALTIME].items()
}, },
"default_providers": DEFAULT_SERVICE_PROVIDERS, "default_providers": DEFAULT_SERVICE_PROVIDERS,
"workflow_configurations": get_default_workflow_configurations(),
} }
return configurations return DefaultConfigurationsResponse(**configurations)
@router.get("/auth/user") @router.get("/auth/user")

View file

@ -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()

View file

@ -3,6 +3,7 @@ from typing import Awaitable, Callable, Optional
from loguru import logger from loguru import logger
from api.schemas.workflow_configurations import DEFAULT_MAX_CALL_DURATION_SECONDS
from pipecat.frames.frames import ( from pipecat.frames.frames import (
Frame, Frame,
HeartbeatFrame, HeartbeatFrame,
@ -23,7 +24,7 @@ class PipelineEngineCallbacksProcessor(FrameProcessor):
def __init__( def __init__(
self, 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, max_duration_end_task_callback: Optional[Callable[[], Awaitable[None]]] = None,
generation_started_callback: Optional[Callable[[], Awaitable[None]]] = None, generation_started_callback: Optional[Callable[[], Awaitable[None]]] = None,
llm_text_frame_callback: Optional[Callable[[str], Awaitable[None]]] = None, llm_text_frame_callback: Optional[Callable[[str], Awaitable[None]]] = None,

View file

@ -6,6 +6,14 @@ from loguru import logger
from api.db import db_client from api.db import db_client
from api.enums import WorkflowRunMode 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.configuration.registry import ServiceProviders
from api.services.integrations import ( from api.services.integrations import (
IntegrationRuntimeContext, IntegrationRuntimeContext,
@ -100,10 +108,6 @@ ensure_tracing()
DEFAULT_USER_TURN_STOP_TIMEOUT = 5.0 DEFAULT_USER_TURN_STOP_TIMEOUT = 5.0
EXTERNAL_TURN_USER_STOP_TIMEOUT = 30.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( def _resolve_user_turn_stop_timeout(
@ -538,8 +542,8 @@ async def _run_pipeline_impl(
run_configs = run_definition.workflow_configurations or {} run_configs = run_definition.workflow_configurations or {}
# Extract configurations from the version's workflow_configurations # Extract configurations from the version's workflow_configurations
max_call_duration_seconds = 300 # Default 5 minutes max_call_duration_seconds = DEFAULT_MAX_CALL_DURATION_SECONDS
max_user_idle_timeout = 10.0 # Default 10 seconds max_user_idle_timeout = DEFAULT_MAX_USER_IDLE_TIMEOUT_SECONDS
keyterms = None # Dictionary words for STT boosting keyterms = None # Dictionary words for STT boosting
if run_configs: if run_configs:

View file

@ -443,7 +443,7 @@ function RenderWorkflow({
const renameWorkflow = useCallback(async (newName: string) => { const renameWorkflow = useCallback(async (newName: string) => {
// The header doesn't render the pencil until the page has mounted with // The header doesn't render the pencil until the page has mounted with
// initial data, so workflowConfigurations is non-null by the time this // 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. // which would overwrite the saved server-side config.
if (!workflowConfigurations) { if (!workflowConfigurations) {
throw new Error("Workflow configurations not loaded"); throw new Error("Workflow configurations not loaded");

View file

@ -10,6 +10,7 @@ import {
AmbientNoiseConfiguration, AmbientNoiseConfiguration,
DEFAULT_PROVISIONAL_VAD_PAUSE_SECS, DEFAULT_PROVISIONAL_VAD_PAUSE_SECS,
DEFAULT_TURN_START_MIN_WORDS, DEFAULT_TURN_START_MIN_WORDS,
resolveWorkflowConfigurations,
TURN_START_STRATEGY_OPTIONS, TURN_START_STRATEGY_OPTIONS,
TurnStartStrategy, TurnStartStrategy,
TurnStopStrategy, TurnStopStrategy,
@ -24,11 +25,6 @@ interface ConfigurationsDialogProps {
onSave: (configurations: WorkflowConfigurations, workflowName: string) => Promise<void>; onSave: (configurations: WorkflowConfigurations, workflowName: string) => Promise<void>;
} }
const DEFAULT_AMBIENT_NOISE_CONFIG: AmbientNoiseConfiguration = {
enabled: false,
volume: 0.3,
};
export const ConfigurationsDialog = ({ export const ConfigurationsDialog = ({
open, open,
onOpenChange, onOpenChange,
@ -36,33 +32,34 @@ export const ConfigurationsDialog = ({
workflowName, workflowName,
onSave onSave
}: ConfigurationsDialogProps) => { }: ConfigurationsDialogProps) => {
const resolvedWorkflowConfigurations = resolveWorkflowConfigurations(workflowConfigurations);
const [name, setName] = useState<string>(workflowName); const [name, setName] = useState<string>(workflowName);
const [ambientNoiseConfig, setAmbientNoiseConfig] = useState<AmbientNoiseConfiguration>( const [ambientNoiseConfig, setAmbientNoiseConfig] = useState<AmbientNoiseConfiguration>(
workflowConfigurations?.ambient_noise_configuration || DEFAULT_AMBIENT_NOISE_CONFIG resolvedWorkflowConfigurations.ambient_noise_configuration
); );
const [maxCallDuration, setMaxCallDuration] = useState<number>( const [maxCallDuration, setMaxCallDuration] = useState<number>(
workflowConfigurations?.max_call_duration || 600 // Default 10 minutes resolvedWorkflowConfigurations.max_call_duration
); );
const [maxUserIdleTimeout, setMaxUserIdleTimeout] = useState<number>( const [maxUserIdleTimeout, setMaxUserIdleTimeout] = useState<number>(
workflowConfigurations?.max_user_idle_timeout || 10 // Default 10 seconds resolvedWorkflowConfigurations.max_user_idle_timeout
); );
const [smartTurnStopSecs, setSmartTurnStopSecs] = useState<number>( const [smartTurnStopSecs, setSmartTurnStopSecs] = useState<number>(
workflowConfigurations?.smart_turn_stop_secs || 2 // Default 2 seconds resolvedWorkflowConfigurations.smart_turn_stop_secs
); );
const [turnStartStrategy, setTurnStartStrategy] = useState<TurnStartStrategy>( const [turnStartStrategy, setTurnStartStrategy] = useState<TurnStartStrategy>(
workflowConfigurations?.turn_start_strategy || 'default' resolvedWorkflowConfigurations.turn_start_strategy
); );
const [turnStartMinWords, setTurnStartMinWords] = useState<number>( const [turnStartMinWords, setTurnStartMinWords] = useState<number>(
workflowConfigurations?.turn_start_min_words || DEFAULT_TURN_START_MIN_WORDS resolvedWorkflowConfigurations.turn_start_min_words
); );
const [provisionalVadPauseSecs, setProvisionalVadPauseSecs] = useState<number>( const [provisionalVadPauseSecs, setProvisionalVadPauseSecs] = useState<number>(
workflowConfigurations?.provisional_vad_pause_secs || DEFAULT_PROVISIONAL_VAD_PAUSE_SECS resolvedWorkflowConfigurations.provisional_vad_pause_secs
); );
const [turnStopStrategy, setTurnStopStrategy] = useState<TurnStopStrategy>( const [turnStopStrategy, setTurnStopStrategy] = useState<TurnStopStrategy>(
workflowConfigurations?.turn_stop_strategy || 'transcription' resolvedWorkflowConfigurations.turn_stop_strategy
); );
const [contextCompactionEnabled, setContextCompactionEnabled] = useState<boolean>( const [contextCompactionEnabled, setContextCompactionEnabled] = useState<boolean>(
workflowConfigurations?.context_compaction_enabled ?? false resolvedWorkflowConfigurations.context_compaction_enabled
); );
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const selectedTurnStartStrategy = TURN_START_STRATEGY_OPTIONS.find( const selectedTurnStartStrategy = TURN_START_STRATEGY_OPTIONS.find(
@ -94,16 +91,17 @@ export const ConfigurationsDialog = ({
// Sync state with props when dialog opens // Sync state with props when dialog opens
useEffect(() => { useEffect(() => {
if (open) { if (open) {
const nextWorkflowConfigurations = resolveWorkflowConfigurations(workflowConfigurations);
setName(workflowName); setName(workflowName);
setAmbientNoiseConfig(workflowConfigurations?.ambient_noise_configuration || DEFAULT_AMBIENT_NOISE_CONFIG); setAmbientNoiseConfig(nextWorkflowConfigurations.ambient_noise_configuration);
setMaxCallDuration(workflowConfigurations?.max_call_duration || 600); setMaxCallDuration(nextWorkflowConfigurations.max_call_duration);
setMaxUserIdleTimeout(workflowConfigurations?.max_user_idle_timeout || 10); setMaxUserIdleTimeout(nextWorkflowConfigurations.max_user_idle_timeout);
setSmartTurnStopSecs(workflowConfigurations?.smart_turn_stop_secs || 2); setSmartTurnStopSecs(nextWorkflowConfigurations.smart_turn_stop_secs);
setTurnStartStrategy(workflowConfigurations?.turn_start_strategy || 'default'); setTurnStartStrategy(nextWorkflowConfigurations.turn_start_strategy);
setTurnStartMinWords(workflowConfigurations?.turn_start_min_words || DEFAULT_TURN_START_MIN_WORDS); setTurnStartMinWords(nextWorkflowConfigurations.turn_start_min_words);
setProvisionalVadPauseSecs(workflowConfigurations?.provisional_vad_pause_secs || DEFAULT_PROVISIONAL_VAD_PAUSE_SECS); setProvisionalVadPauseSecs(nextWorkflowConfigurations.provisional_vad_pause_secs);
setTurnStopStrategy(workflowConfigurations?.turn_stop_strategy || 'transcription'); setTurnStopStrategy(nextWorkflowConfigurations.turn_stop_strategy);
setContextCompactionEnabled(workflowConfigurations?.context_compaction_enabled ?? false); setContextCompactionEnabled(nextWorkflowConfigurations.context_compaction_enabled);
} }
}, [open, workflowName, workflowConfigurations]); }, [open, workflowName, workflowConfigurations]);
@ -244,6 +242,16 @@ export const ConfigurationsDialog = ({
</p> </p>
</div> </div>
)} )}
</div>
{/* Interruption Section */}
<div className="space-y-4">
<div>
<h3 className="text-sm font-semibold mb-1">Interruption</h3>
<p className="text-xs text-muted-foreground">
Configure when user speech should interrupt the agent while it is speaking.
</p>
</div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="turn_start_strategy" className="text-xs"> <Label htmlFor="turn_start_strategy" className="text-xs">

View file

@ -9,12 +9,13 @@ import {
import { EdgeChange, NodeChange } from "@xyflow/system"; import { EdgeChange, NodeChange } from "@xyflow/system";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import posthog from "posthog-js"; import posthog from "posthog-js";
import { useCallback, useEffect, useRef } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { useWorkflowStore } from "@/app/workflow/[workflowId]/stores/workflowStore"; import { useWorkflowStore } from "@/app/workflow/[workflowId]/stores/workflowStore";
import { import {
createWorkflowRunApiV1WorkflowWorkflowIdRunsPost, createWorkflowRunApiV1WorkflowWorkflowIdRunsPost,
getDefaultConfigurationsApiV1UserConfigurationsDefaultsGet,
updateWorkflowApiV1WorkflowWorkflowIdPut, updateWorkflowApiV1WorkflowWorkflowIdPut,
validateWorkflowApiV1WorkflowWorkflowIdValidatePost validateWorkflowApiV1WorkflowWorkflowIdValidatePost
} from "@/client"; } from "@/client";
@ -24,7 +25,11 @@ import { FlowEdge, FlowNode, FlowNodeData, NodeType } from "@/components/flow/ty
import { PostHogEvent } from "@/constants/posthog-events"; import { PostHogEvent } from "@/constants/posthog-events";
import logger from '@/lib/logger'; import logger from '@/lib/logger';
import { getNextNodeId, getRandomId } from "@/lib/utils"; import { getNextNodeId, getRandomId } from "@/lib/utils";
import { DEFAULT_WORKFLOW_CONFIGURATIONS, WorkflowConfigurations } from "@/types/workflow-configurations"; import {
resolveWorkflowConfigurations,
type WorkflowConfigurationDefaults,
type WorkflowConfigurations,
} from "@/types/workflow-configurations";
// Pull a WorkflowError[] out of any validate-shaped payload — works whether // Pull a WorkflowError[] out of any validate-shaped payload — works whether
// the body is the raw `{ is_valid, errors }` (validate success-with-errors) // the body is the raw `{ is_valid, errors }` (validate success-with-errors)
@ -111,6 +116,10 @@ export const useWorkflowState = ({
}: UseWorkflowStateProps) => { }: UseWorkflowStateProps) => {
const router = useRouter(); const router = useRouter();
const rfInstance = useRef<ReactFlowInstance<FlowNode, FlowEdge> | null>(null); const rfInstance = useRef<ReactFlowInstance<FlowNode, FlowEdge> | null>(null);
const [workflowConfigurationDefaults, setWorkflowConfigurationDefaults] =
useState<WorkflowConfigurationDefaults | null>(null);
const [workflowConfigurationDefaultsLoaded, setWorkflowConfigurationDefaultsLoaded] =
useState(false);
// Spec catalog. Workflow init waits on this to populate defaults; node // Spec catalog. Workflow init waits on this to populate defaults; node
// creation looks up per-type schemas through it. // creation looks up per-type schemas through it.
@ -149,10 +158,44 @@ export const useWorkflowState = ({
const canUndo = useWorkflowStore((state) => state.canUndo()); const canUndo = useWorkflowStore((state) => state.canUndo());
const canRedo = useWorkflowStore((state) => state.canRedo()); const canRedo = useWorkflowStore((state) => state.canRedo());
useEffect(() => {
let cancelled = false;
const loadWorkflowConfigurationDefaults = async () => {
try {
const response = await getDefaultConfigurationsApiV1UserConfigurationsDefaultsGet();
if (cancelled) return;
if (response.error || !response.data?.workflow_configurations) {
logger.error(
`Failed to load workflow configuration defaults: ${JSON.stringify(response.error)}`,
);
setWorkflowConfigurationDefaults(null);
} else {
setWorkflowConfigurationDefaults(response.data.workflow_configurations);
}
} catch (error) {
if (cancelled) return;
logger.error(`Failed to load workflow configuration defaults: ${error}`);
setWorkflowConfigurationDefaults(null);
} finally {
if (!cancelled) {
setWorkflowConfigurationDefaultsLoaded(true);
}
}
};
loadWorkflowConfigurationDefaults();
return () => {
cancelled = true;
};
}, []);
// Initialize workflow on mount. Waits for the spec catalog so defaults // Initialize workflow on mount. Waits for the spec catalog so defaults
// (allow_interrupt, prompt placeholders, etc.) come from one source. // (allow_interrupt, prompt placeholders, etc.) come from one source.
useEffect(() => { useEffect(() => {
if (specsLoading) return; if (specsLoading || !workflowConfigurationDefaultsLoaded) return;
const startSpec = bySpecName.get(NodeType.START_CALL); const startSpec = bySpecName.get(NodeType.START_CALL);
const fallbackStartNodes: FlowNode[] = startSpec const fallbackStartNodes: FlowNode[] = startSpec
@ -176,16 +219,21 @@ export const useWorkflowState = ({
}) })
: fallbackStartNodes; : fallbackStartNodes;
const resolvedInitialWorkflowConfigurations = resolveWorkflowConfigurations(
initialWorkflowConfigurations,
workflowConfigurationDefaults,
);
initializeWorkflow( initializeWorkflow(
workflowId, workflowId,
initialWorkflowName, initialWorkflowName,
initialNodes, initialNodes,
initialFlow?.edges ?? [], initialFlow?.edges ?? [],
initialTemplateContextVariables, initialTemplateContextVariables,
initialWorkflowConfigurations, resolvedInitialWorkflowConfigurations,
initialWorkflowConfigurations?.dictionary ?? '' resolvedInitialWorkflowConfigurations.dictionary ?? ''
); );
}, [workflowId, initialWorkflowName, initialFlow?.nodes, initialFlow?.edges, initialTemplateContextVariables, initialWorkflowConfigurations, initializeWorkflow, specsLoading, bySpecName]); }, [workflowId, initialWorkflowName, initialFlow?.nodes, initialFlow?.edges, initialTemplateContextVariables, initialWorkflowConfigurations, initializeWorkflow, specsLoading, bySpecName, workflowConfigurationDefaultsLoaded, workflowConfigurationDefaults]);
// Set up keyboard shortcuts for undo/redo // Set up keyboard shortcuts for undo/redo
useEffect(() => { useEffect(() => {
@ -515,9 +563,12 @@ export const useWorkflowState = ({
throw new Error(msg); throw new Error(msg);
} }
const savedConfigurations = response.data?.workflow_configurations const savedConfigurations = resolveWorkflowConfigurations(
? (response.data.workflow_configurations as WorkflowConfigurations) response.data?.workflow_configurations
: configurationsWithDictionary; ? (response.data.workflow_configurations as Partial<WorkflowConfigurations>)
: configurationsWithDictionary,
workflowConfigurationDefaults,
);
setWorkflowConfigurations(savedConfigurations); setWorkflowConfigurations(savedConfigurations);
// Set name directly in the store to avoid setWorkflowName which marks isDirty: true // Set name directly in the store to avoid setWorkflowName which marks isDirty: true
useWorkflowStore.setState({ workflowName: newWorkflowName }); useWorkflowStore.setState({ workflowName: newWorkflowName });
@ -526,12 +577,14 @@ export const useWorkflowState = ({
logger.error(`Error saving workflow configurations: ${error}`); logger.error(`Error saving workflow configurations: ${error}`);
throw error; throw error;
} }
}, [workflowId, user, setWorkflowConfigurations]); }, [workflowId, user, setWorkflowConfigurations, workflowConfigurationDefaults]);
// Save dictionary // Save dictionary
const saveDictionary = useCallback(async (newDictionary: string) => { const saveDictionary = useCallback(async (newDictionary: string) => {
if (!user) return; if (!user) return;
const currentConfigurations = useWorkflowStore.getState().workflowConfigurations ?? DEFAULT_WORKFLOW_CONFIGURATIONS; const currentConfigurations =
useWorkflowStore.getState().workflowConfigurations
?? resolveWorkflowConfigurations(null, workflowConfigurationDefaults);
const updatedConfigurations: WorkflowConfigurations = { ...currentConfigurations, dictionary: newDictionary }; const updatedConfigurations: WorkflowConfigurations = { ...currentConfigurations, dictionary: newDictionary };
try { try {
await updateWorkflowApiV1WorkflowWorkflowIdPut({ await updateWorkflowApiV1WorkflowWorkflowIdPut({
@ -550,7 +603,7 @@ export const useWorkflowState = ({
logger.error(`Error saving dictionary: ${error}`); logger.error(`Error saving dictionary: ${error}`);
throw error; throw error;
} }
}, [workflowId, workflowName, user, setDictionary, setWorkflowConfigurations]); }, [workflowId, workflowName, user, setDictionary, setWorkflowConfigurations, workflowConfigurationDefaults]);
// Update rfInstance when it changes // Update rfInstance when it changes
useEffect(() => { useEffect(() => {

View file

@ -12,7 +12,7 @@ import SpinLoader from '@/components/SpinLoader';
import { PostHogEvent } from '@/constants/posthog-events'; import { PostHogEvent } from '@/constants/posthog-events';
import { useAuth } from '@/lib/auth'; import { useAuth } from '@/lib/auth';
import logger from '@/lib/logger'; import logger from '@/lib/logger';
import { DEFAULT_WORKFLOW_CONFIGURATIONS, WorkflowConfigurations } from '@/types/workflow-configurations'; import { WorkflowConfigurations } from '@/types/workflow-configurations';
import WorkflowLayout from '../WorkflowLayout'; import WorkflowLayout from '../WorkflowLayout';
@ -92,7 +92,11 @@ export default function WorkflowDetailPage() {
viewport: { x: 0, y: 0, zoom: 0 } viewport: { x: 0, y: 0, zoom: 0 }
}} }}
initialTemplateContextVariables={workflow.template_context_variables as Record<string, string> || {}} initialTemplateContextVariables={workflow.template_context_variables as Record<string, string> || {}}
initialWorkflowConfigurations={(workflow.workflow_configurations as WorkflowConfigurations) || DEFAULT_WORKFLOW_CONFIGURATIONS} initialWorkflowConfigurations={
workflow.workflow_configurations
? (workflow.workflow_configurations as WorkflowConfigurations)
: undefined
}
initialVersionNumber={workflow.version_number ?? null} initialVersionNumber={workflow.version_number ?? null}
initialVersionStatus={workflow.version_status ?? null} initialVersionStatus={workflow.version_status ?? null}
user={stableUser} user={stableUser}

View file

@ -48,7 +48,6 @@ import {
DEFAULT_PROVISIONAL_VAD_PAUSE_SECS, DEFAULT_PROVISIONAL_VAD_PAUSE_SECS,
DEFAULT_TURN_START_MIN_WORDS, DEFAULT_TURN_START_MIN_WORDS,
DEFAULT_VOICEMAIL_DETECTION_CONFIGURATION, DEFAULT_VOICEMAIL_DETECTION_CONFIGURATION,
DEFAULT_WORKFLOW_CONFIGURATIONS,
TURN_START_STRATEGY_OPTIONS, TURN_START_STRATEGY_OPTIONS,
type TurnStartStrategy, type TurnStartStrategy,
type TurnStopStrategy, type TurnStopStrategy,
@ -63,11 +62,6 @@ import { useWorkflowState } from "../hooks/useWorkflowState";
// Constants // Constants
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const DEFAULT_AMBIENT_NOISE_CONFIG: AmbientNoiseConfiguration = {
enabled: false,
volume: 0.3,
};
const DEFAULT_VOICEMAIL_SYSTEM_PROMPT = `You are a voicemail detection classifier for an OUTBOUND calling system. A bot has called a phone number and you need to determine if a human answered or if the call went to voicemail based on the provided text. const DEFAULT_VOICEMAIL_SYSTEM_PROMPT = `You are a voicemail detection classifier for an OUTBOUND calling system. A bot has called a phone number and you need to determine if a human answered or if the call went to voicemail based on the provided text.
HUMAN ANSWERED - LIVE CONVERSATION (respond "CONVERSATION"): HUMAN ANSWERED - LIVE CONVERSATION (respond "CONVERSATION"):
@ -279,25 +273,25 @@ function GeneralSection({
}) { }) {
const [name, setName] = useState(workflowName); const [name, setName] = useState(workflowName);
const [ambientNoiseConfig, setAmbientNoiseConfig] = useState<AmbientNoiseConfiguration>( const [ambientNoiseConfig, setAmbientNoiseConfig] = useState<AmbientNoiseConfiguration>(
workflowConfigurations.ambient_noise_configuration || DEFAULT_AMBIENT_NOISE_CONFIG, workflowConfigurations.ambient_noise_configuration,
); );
const [maxCallDuration, setMaxCallDuration] = useState(workflowConfigurations.max_call_duration || 600); const [maxCallDuration, setMaxCallDuration] = useState(workflowConfigurations.max_call_duration);
const [maxUserIdleTimeout, setMaxUserIdleTimeout] = useState(workflowConfigurations.max_user_idle_timeout || 10); const [maxUserIdleTimeout, setMaxUserIdleTimeout] = useState(workflowConfigurations.max_user_idle_timeout);
const [smartTurnStopSecs, setSmartTurnStopSecs] = useState(workflowConfigurations.smart_turn_stop_secs || 2); const [smartTurnStopSecs, setSmartTurnStopSecs] = useState(workflowConfigurations.smart_turn_stop_secs);
const [turnStartStrategy, setTurnStartStrategy] = useState<TurnStartStrategy>( const [turnStartStrategy, setTurnStartStrategy] = useState<TurnStartStrategy>(
workflowConfigurations.turn_start_strategy || "default", workflowConfigurations.turn_start_strategy,
); );
const [turnStartMinWords, setTurnStartMinWords] = useState( const [turnStartMinWords, setTurnStartMinWords] = useState(
workflowConfigurations.turn_start_min_words || DEFAULT_TURN_START_MIN_WORDS, workflowConfigurations.turn_start_min_words,
); );
const [provisionalVadPauseSecs, setProvisionalVadPauseSecs] = useState( const [provisionalVadPauseSecs, setProvisionalVadPauseSecs] = useState(
workflowConfigurations.provisional_vad_pause_secs || DEFAULT_PROVISIONAL_VAD_PAUSE_SECS, workflowConfigurations.provisional_vad_pause_secs,
); );
const [turnStopStrategy, setTurnStopStrategy] = useState<TurnStopStrategy>( const [turnStopStrategy, setTurnStopStrategy] = useState<TurnStopStrategy>(
workflowConfigurations.turn_stop_strategy || "transcription", workflowConfigurations.turn_stop_strategy,
); );
const [contextCompactionEnabled, setContextCompactionEnabled] = useState( const [contextCompactionEnabled, setContextCompactionEnabled] = useState(
workflowConfigurations.context_compaction_enabled ?? false, workflowConfigurations.context_compaction_enabled,
); );
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [isUploadingAudio, setIsUploadingAudio] = useState(false); const [isUploadingAudio, setIsUploadingAudio] = useState(false);
@ -309,18 +303,18 @@ function GeneralSection({
); );
const isDirty = useMemo(() => { const isDirty = useMemo(() => {
const initAmbient = workflowConfigurations.ambient_noise_configuration || DEFAULT_AMBIENT_NOISE_CONFIG; const initAmbient = workflowConfigurations.ambient_noise_configuration;
return ( return (
name !== workflowName || name !== workflowName ||
JSON.stringify(ambientNoiseConfig) !== JSON.stringify(initAmbient) || JSON.stringify(ambientNoiseConfig) !== JSON.stringify(initAmbient) ||
maxCallDuration !== (workflowConfigurations.max_call_duration || 600) || maxCallDuration !== workflowConfigurations.max_call_duration ||
maxUserIdleTimeout !== (workflowConfigurations.max_user_idle_timeout || 10) || maxUserIdleTimeout !== workflowConfigurations.max_user_idle_timeout ||
smartTurnStopSecs !== (workflowConfigurations.smart_turn_stop_secs || 2) || smartTurnStopSecs !== workflowConfigurations.smart_turn_stop_secs ||
turnStartStrategy !== (workflowConfigurations.turn_start_strategy || "default") || turnStartStrategy !== workflowConfigurations.turn_start_strategy ||
turnStartMinWords !== (workflowConfigurations.turn_start_min_words || DEFAULT_TURN_START_MIN_WORDS) || turnStartMinWords !== workflowConfigurations.turn_start_min_words ||
provisionalVadPauseSecs !== (workflowConfigurations.provisional_vad_pause_secs || DEFAULT_PROVISIONAL_VAD_PAUSE_SECS) || provisionalVadPauseSecs !== workflowConfigurations.provisional_vad_pause_secs ||
turnStopStrategy !== (workflowConfigurations.turn_stop_strategy || "transcription") || turnStopStrategy !== workflowConfigurations.turn_stop_strategy ||
contextCompactionEnabled !== (workflowConfigurations.context_compaction_enabled ?? false) contextCompactionEnabled !== workflowConfigurations.context_compaction_enabled
); );
}, [name, workflowName, ambientNoiseConfig, maxCallDuration, maxUserIdleTimeout, smartTurnStopSecs, turnStartStrategy, turnStartMinWords, provisionalVadPauseSecs, turnStopStrategy, contextCompactionEnabled, workflowConfigurations]); }, [name, workflowName, ambientNoiseConfig, maxCallDuration, maxUserIdleTimeout, smartTurnStopSecs, turnStartStrategy, turnStartMinWords, provisionalVadPauseSecs, turnStopStrategy, contextCompactionEnabled, workflowConfigurations]);
@ -611,6 +605,18 @@ function GeneralSection({
</p> </p>
</div> </div>
)} )}
</div>
<Separator />
{/* Interruption */}
<div className="space-y-4">
<div>
<h3 className="text-sm font-medium">Interruption</h3>
<p className="text-xs text-muted-foreground mt-0.5">
Configure when user speech should interrupt the agent while it is speaking.
</p>
</div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="turn_start_strategy" className="text-xs">Interruption Strategy</Label> <Label htmlFor="turn_start_strategy" className="text-xs">Interruption Strategy</Label>
<Select <Select
@ -1428,7 +1434,11 @@ function WorkflowSettingsInner({
); );
const initialWorkflowConfigurations = useMemo( const initialWorkflowConfigurations = useMemo(
() => (workflow.workflow_configurations as WorkflowConfigurations) || DEFAULT_WORKFLOW_CONFIGURATIONS, () => (
workflow.workflow_configurations
? (workflow.workflow_configurations as WorkflowConfigurations)
: undefined
),
[workflow], [workflow],
); );

View file

@ -4,7 +4,7 @@ import { create } from 'zustand';
import { WorkflowError } from '@/client/types.gen'; import { WorkflowError } from '@/client/types.gen';
import { FlowEdge, FlowNode } from '@/components/flow/types'; import { FlowEdge, FlowNode } from '@/components/flow/types';
import { DEFAULT_WORKFLOW_CONFIGURATIONS, WorkflowConfigurations } from '@/types/workflow-configurations'; import { WorkflowConfigurations } from '@/types/workflow-configurations';
interface HistoryState { interface HistoryState {
nodes: FlowNode[]; nodes: FlowNode[];
@ -117,7 +117,7 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
rfInstance: null, rfInstance: null,
// Actions // Actions
initializeWorkflow: (workflowId, workflowName, nodes, edges, templateContextVariables = {}, workflowConfigurations = DEFAULT_WORKFLOW_CONFIGURATIONS, dictionary = '') => { initializeWorkflow: (workflowId, workflowName, nodes, edges, templateContextVariables = {}, workflowConfigurations = null, dictionary = '') => {
const initialHistory: HistoryState = { nodes, edges, workflowName }; const initialHistory: HistoryState = { nodes, edges, workflowName };
set({ set({
workflowId, workflowId,

File diff suppressed because one or more lines are too long

View file

@ -186,6 +186,21 @@ export type ActiveCallsResponse = {
active_calls: number; active_calls: number;
}; };
/**
* AmbientNoiseConfigurationDefaults
*/
export type AmbientNoiseConfigurationDefaults = {
/**
* Enabled
*/
enabled?: boolean;
/**
* Volume
*/
volume?: number;
[key: string]: unknown;
};
/** /**
* AmbientNoiseUploadRequest * AmbientNoiseUploadRequest
*/ */
@ -1878,6 +1893,7 @@ export type DefaultConfigurationsResponse = {
default_providers: { default_providers: {
[key: string]: string; [key: string]: string;
}; };
workflow_configurations: WorkflowConfigurationDefaults;
}; };
/** /**
@ -6523,6 +6539,50 @@ export type VonageConfigurationResponse = {
*/ */
export type WebhookCredentialType = 'none' | 'api_key' | 'bearer_token' | 'basic_auth' | 'custom_header'; export type WebhookCredentialType = 'none' | 'api_key' | 'bearer_token' | 'basic_auth' | 'custom_header';
/**
* WorkflowConfigurationDefaults
*/
export type WorkflowConfigurationDefaults = {
ambient_noise_configuration?: AmbientNoiseConfigurationDefaults;
/**
* Max Call Duration
*/
max_call_duration?: number;
/**
* Max User Idle Timeout
*/
max_user_idle_timeout?: number;
/**
* Smart Turn Stop Secs
*/
smart_turn_stop_secs?: number;
/**
* Turn Start Strategy
*/
turn_start_strategy?: 'default' | 'min_words' | 'provisional_vad';
/**
* Turn Start Min Words
*/
turn_start_min_words?: number;
/**
* Provisional Vad Pause Secs
*/
provisional_vad_pause_secs?: number;
/**
* Turn Stop Strategy
*/
turn_stop_strategy?: 'transcription' | 'turn_analyzer';
/**
* Dictionary
*/
dictionary?: string;
/**
* Context Compaction Enabled
*/
context_compaction_enabled?: boolean;
[key: string]: unknown;
};
/** /**
* WorkflowCountResponse * WorkflowCountResponse
* *

View file

@ -227,7 +227,7 @@ export function ServiceConfigurationForm({
console.error("Failed to fetch configurations"); console.error("Failed to fetch configurations");
return; return;
} }
defaultsData = response.data as ServiceConfigurationDefaults; defaultsData = response.data as unknown as ServiceConfigurationDefaults;
} }
const realtimeSchemas = (defaultsData.realtime || {}) as Record<string, ProviderSchema>; const realtimeSchemas = (defaultsData.realtime || {}) as Record<string, ProviderSchema>;

View file

@ -1,15 +1,24 @@
import type { OrganizationAiModelConfigurationV2 } from "@/client/types.gen"; import type {
AmbientNoiseConfigurationDefaults,
OrganizationAiModelConfigurationV2,
WorkflowConfigurationDefaults as GeneratedWorkflowConfigurationDefaults,
} from "@/client/types.gen";
export interface AmbientNoiseConfiguration { export type WorkflowConfigurationDefaults = GeneratedWorkflowConfigurationDefaults;
export type AmbientNoiseConfiguration = Omit<
AmbientNoiseConfigurationDefaults,
"enabled" | "volume"
> & {
enabled: boolean; enabled: boolean;
volume: number; volume: number;
storage_key?: string; storage_key?: string;
storage_backend?: string; storage_backend?: string;
original_filename?: string; original_filename?: string;
} };
export type TurnStopStrategy = 'transcription' | 'turn_analyzer'; export type TurnStopStrategy = NonNullable<GeneratedWorkflowConfigurationDefaults["turn_stop_strategy"]>;
export type TurnStartStrategy = 'default' | 'min_words' | 'provisional_vad'; export type TurnStartStrategy = NonNullable<GeneratedWorkflowConfigurationDefaults["turn_start_strategy"]>;
export const DEFAULT_TURN_START_MIN_WORDS = 3; export const DEFAULT_TURN_START_MIN_WORDS = 3;
export const DEFAULT_PROVISIONAL_VAD_PAUSE_SECS = 1.5; export const DEFAULT_PROVISIONAL_VAD_PAUSE_SECS = 1.5;
@ -81,7 +90,21 @@ export interface ModelOverrides {
is_realtime?: boolean; is_realtime?: boolean;
} }
export interface WorkflowConfigurations { type WorkflowConfigurationBase = Omit<
GeneratedWorkflowConfigurationDefaults,
| "ambient_noise_configuration"
| "max_call_duration"
| "max_user_idle_timeout"
| "smart_turn_stop_secs"
| "turn_start_strategy"
| "turn_start_min_words"
| "provisional_vad_pause_secs"
| "turn_stop_strategy"
| "dictionary"
| "context_compaction_enabled"
>;
export type WorkflowConfigurations = WorkflowConfigurationBase & {
ambient_noise_configuration: AmbientNoiseConfiguration; ambient_noise_configuration: AmbientNoiseConfiguration;
max_call_duration: number; // Maximum call duration in seconds max_call_duration: number; // Maximum call duration in seconds
max_user_idle_timeout: number; // Maximum user idle time in seconds max_user_idle_timeout: number; // Maximum user idle time in seconds
@ -92,23 +115,76 @@ export interface WorkflowConfigurations {
turn_stop_strategy: TurnStopStrategy; // Strategy for detecting end of user turn turn_stop_strategy: TurnStopStrategy; // Strategy for detecting end of user turn
dictionary?: string; // Comma-separated words for voice agent to listen for dictionary?: string; // Comma-separated words for voice agent to listen for
voicemail_detection?: VoicemailDetectionConfiguration; voicemail_detection?: VoicemailDetectionConfiguration;
context_compaction_enabled?: boolean; // Summarize context on node transitions to remove stale tool calls context_compaction_enabled: boolean; // Summarize context on node transitions to remove stale tool calls
model_overrides?: ModelOverrides; // Per-workflow model configuration overrides model_overrides?: ModelOverrides; // Per-workflow model configuration overrides
model_configuration_v2_override?: OrganizationAiModelConfigurationV2; // Full v2 model configuration override model_configuration_v2_override?: OrganizationAiModelConfigurationV2; // Full v2 model configuration override
[key: string]: unknown; // Allow additional properties for future configurations [key: string]: unknown; // Allow additional properties for future configurations
} };
export const DEFAULT_WORKFLOW_CONFIGURATIONS: WorkflowConfigurations = { const FALLBACK_WORKFLOW_CONFIGURATIONS: WorkflowConfigurations = {
ambient_noise_configuration: { ambient_noise_configuration: {
enabled: false, enabled: false,
volume: 0.3 volume: 0.3
}, },
max_call_duration: 600, // 10 minutes max_call_duration: 300,
max_user_idle_timeout: 10, // 10 seconds max_user_idle_timeout: 10, // 10 seconds
smart_turn_stop_secs: 2, // 2 seconds smart_turn_stop_secs: 2, // 2 seconds
turn_start_strategy: 'default', // Default to platform-chosen user turn start detection turn_start_strategy: 'default', // Default to platform-chosen user turn start detection
turn_start_min_words: DEFAULT_TURN_START_MIN_WORDS, turn_start_min_words: DEFAULT_TURN_START_MIN_WORDS,
provisional_vad_pause_secs: DEFAULT_PROVISIONAL_VAD_PAUSE_SECS, provisional_vad_pause_secs: DEFAULT_PROVISIONAL_VAD_PAUSE_SECS,
turn_stop_strategy: 'transcription', // Default to transcription-based detection turn_stop_strategy: 'transcription', // Default to transcription-based detection
dictionary: '' dictionary: '',
context_compaction_enabled: false,
}; };
export function resolveWorkflowConfigurations(
configurations?: Partial<WorkflowConfigurations> | null,
defaults?: WorkflowConfigurationDefaults | null,
): WorkflowConfigurations {
return {
...FALLBACK_WORKFLOW_CONFIGURATIONS,
...defaults,
...configurations,
ambient_noise_configuration: {
...FALLBACK_WORKFLOW_CONFIGURATIONS.ambient_noise_configuration,
...defaults?.ambient_noise_configuration,
...configurations?.ambient_noise_configuration,
},
max_call_duration:
configurations?.max_call_duration
?? defaults?.max_call_duration
?? FALLBACK_WORKFLOW_CONFIGURATIONS.max_call_duration,
max_user_idle_timeout:
configurations?.max_user_idle_timeout
?? defaults?.max_user_idle_timeout
?? FALLBACK_WORKFLOW_CONFIGURATIONS.max_user_idle_timeout,
smart_turn_stop_secs:
configurations?.smart_turn_stop_secs
?? defaults?.smart_turn_stop_secs
?? FALLBACK_WORKFLOW_CONFIGURATIONS.smart_turn_stop_secs,
turn_start_strategy:
configurations?.turn_start_strategy
?? defaults?.turn_start_strategy
?? FALLBACK_WORKFLOW_CONFIGURATIONS.turn_start_strategy,
turn_start_min_words:
configurations?.turn_start_min_words
?? defaults?.turn_start_min_words
?? FALLBACK_WORKFLOW_CONFIGURATIONS.turn_start_min_words,
provisional_vad_pause_secs:
configurations?.provisional_vad_pause_secs
?? defaults?.provisional_vad_pause_secs
?? FALLBACK_WORKFLOW_CONFIGURATIONS.provisional_vad_pause_secs,
turn_stop_strategy:
configurations?.turn_stop_strategy
?? defaults?.turn_stop_strategy
?? FALLBACK_WORKFLOW_CONFIGURATIONS.turn_stop_strategy,
dictionary:
configurations?.dictionary
?? defaults?.dictionary
?? FALLBACK_WORKFLOW_CONFIGURATIONS.dictionary,
context_compaction_enabled:
configurations?.context_compaction_enabled
?? defaults?.context_compaction_enabled
?? FALLBACK_WORKFLOW_CONFIGURATIONS.context_compaction_enabled,
};
}