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

@ -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");

View file

@ -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<void>;
}
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<string>(workflowName);
const [ambientNoiseConfig, setAmbientNoiseConfig] = useState<AmbientNoiseConfiguration>(
workflowConfigurations?.ambient_noise_configuration || DEFAULT_AMBIENT_NOISE_CONFIG
resolvedWorkflowConfigurations.ambient_noise_configuration
);
const [maxCallDuration, setMaxCallDuration] = useState<number>(
workflowConfigurations?.max_call_duration || 600 // Default 10 minutes
resolvedWorkflowConfigurations.max_call_duration
);
const [maxUserIdleTimeout, setMaxUserIdleTimeout] = useState<number>(
workflowConfigurations?.max_user_idle_timeout || 10 // Default 10 seconds
resolvedWorkflowConfigurations.max_user_idle_timeout
);
const [smartTurnStopSecs, setSmartTurnStopSecs] = useState<number>(
workflowConfigurations?.smart_turn_stop_secs || 2 // Default 2 seconds
resolvedWorkflowConfigurations.smart_turn_stop_secs
);
const [turnStartStrategy, setTurnStartStrategy] = useState<TurnStartStrategy>(
workflowConfigurations?.turn_start_strategy || 'default'
resolvedWorkflowConfigurations.turn_start_strategy
);
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>(
workflowConfigurations?.provisional_vad_pause_secs || DEFAULT_PROVISIONAL_VAD_PAUSE_SECS
resolvedWorkflowConfigurations.provisional_vad_pause_secs
);
const [turnStopStrategy, setTurnStopStrategy] = useState<TurnStopStrategy>(
workflowConfigurations?.turn_stop_strategy || 'transcription'
resolvedWorkflowConfigurations.turn_stop_strategy
);
const [contextCompactionEnabled, setContextCompactionEnabled] = useState<boolean>(
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 = ({
</p>
</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">
<Label htmlFor="turn_start_strategy" className="text-xs">

View file

@ -9,12 +9,13 @@ import {
import { EdgeChange, NodeChange } from "@xyflow/system";
import { useRouter } from "next/navigation";
import posthog from "posthog-js";
import { useCallback, useEffect, useRef } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { useWorkflowStore } from "@/app/workflow/[workflowId]/stores/workflowStore";
import {
createWorkflowRunApiV1WorkflowWorkflowIdRunsPost,
getDefaultConfigurationsApiV1UserConfigurationsDefaultsGet,
updateWorkflowApiV1WorkflowWorkflowIdPut,
validateWorkflowApiV1WorkflowWorkflowIdValidatePost
} from "@/client";
@ -24,7 +25,11 @@ import { FlowEdge, FlowNode, FlowNodeData, NodeType } from "@/components/flow/ty
import { PostHogEvent } from "@/constants/posthog-events";
import logger from '@/lib/logger';
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
// the body is the raw `{ is_valid, errors }` (validate success-with-errors)
@ -111,6 +116,10 @@ export const useWorkflowState = ({
}: UseWorkflowStateProps) => {
const router = useRouter();
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
// creation looks up per-type schemas through it.
@ -149,10 +158,44 @@ export const useWorkflowState = ({
const canUndo = useWorkflowStore((state) => state.canUndo());
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
// (allow_interrupt, prompt placeholders, etc.) come from one source.
useEffect(() => {
if (specsLoading) return;
if (specsLoading || !workflowConfigurationDefaultsLoaded) return;
const startSpec = bySpecName.get(NodeType.START_CALL);
const fallbackStartNodes: FlowNode[] = startSpec
@ -176,16 +219,21 @@ export const useWorkflowState = ({
})
: fallbackStartNodes;
const resolvedInitialWorkflowConfigurations = resolveWorkflowConfigurations(
initialWorkflowConfigurations,
workflowConfigurationDefaults,
);
initializeWorkflow(
workflowId,
initialWorkflowName,
initialNodes,
initialFlow?.edges ?? [],
initialTemplateContextVariables,
initialWorkflowConfigurations,
initialWorkflowConfigurations?.dictionary ?? ''
resolvedInitialWorkflowConfigurations,
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
useEffect(() => {
@ -515,9 +563,12 @@ export const useWorkflowState = ({
throw new Error(msg);
}
const savedConfigurations = response.data?.workflow_configurations
? (response.data.workflow_configurations as WorkflowConfigurations)
: configurationsWithDictionary;
const savedConfigurations = resolveWorkflowConfigurations(
response.data?.workflow_configurations
? (response.data.workflow_configurations as Partial<WorkflowConfigurations>)
: configurationsWithDictionary,
workflowConfigurationDefaults,
);
setWorkflowConfigurations(savedConfigurations);
// Set name directly in the store to avoid setWorkflowName which marks isDirty: true
useWorkflowStore.setState({ workflowName: newWorkflowName });
@ -526,12 +577,14 @@ export const useWorkflowState = ({
logger.error(`Error saving workflow configurations: ${error}`);
throw error;
}
}, [workflowId, user, setWorkflowConfigurations]);
}, [workflowId, user, setWorkflowConfigurations, workflowConfigurationDefaults]);
// Save dictionary
const saveDictionary = useCallback(async (newDictionary: string) => {
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 };
try {
await updateWorkflowApiV1WorkflowWorkflowIdPut({
@ -550,7 +603,7 @@ export const useWorkflowState = ({
logger.error(`Error saving dictionary: ${error}`);
throw error;
}
}, [workflowId, workflowName, user, setDictionary, setWorkflowConfigurations]);
}, [workflowId, workflowName, user, setDictionary, setWorkflowConfigurations, workflowConfigurationDefaults]);
// Update rfInstance when it changes
useEffect(() => {

View file

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

View file

@ -48,7 +48,6 @@ import {
DEFAULT_PROVISIONAL_VAD_PAUSE_SECS,
DEFAULT_TURN_START_MIN_WORDS,
DEFAULT_VOICEMAIL_DETECTION_CONFIGURATION,
DEFAULT_WORKFLOW_CONFIGURATIONS,
TURN_START_STRATEGY_OPTIONS,
type TurnStartStrategy,
type TurnStopStrategy,
@ -63,11 +62,6 @@ import { useWorkflowState } from "../hooks/useWorkflowState";
// 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.
HUMAN ANSWERED - LIVE CONVERSATION (respond "CONVERSATION"):
@ -279,25 +273,25 @@ function GeneralSection({
}) {
const [name, setName] = useState(workflowName);
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 [maxUserIdleTimeout, setMaxUserIdleTimeout] = useState(workflowConfigurations.max_user_idle_timeout || 10);
const [smartTurnStopSecs, setSmartTurnStopSecs] = useState(workflowConfigurations.smart_turn_stop_secs || 2);
const [maxCallDuration, setMaxCallDuration] = useState(workflowConfigurations.max_call_duration);
const [maxUserIdleTimeout, setMaxUserIdleTimeout] = useState(workflowConfigurations.max_user_idle_timeout);
const [smartTurnStopSecs, setSmartTurnStopSecs] = useState(workflowConfigurations.smart_turn_stop_secs);
const [turnStartStrategy, setTurnStartStrategy] = useState<TurnStartStrategy>(
workflowConfigurations.turn_start_strategy || "default",
workflowConfigurations.turn_start_strategy,
);
const [turnStartMinWords, setTurnStartMinWords] = useState(
workflowConfigurations.turn_start_min_words || DEFAULT_TURN_START_MIN_WORDS,
workflowConfigurations.turn_start_min_words,
);
const [provisionalVadPauseSecs, setProvisionalVadPauseSecs] = useState(
workflowConfigurations.provisional_vad_pause_secs || DEFAULT_PROVISIONAL_VAD_PAUSE_SECS,
workflowConfigurations.provisional_vad_pause_secs,
);
const [turnStopStrategy, setTurnStopStrategy] = useState<TurnStopStrategy>(
workflowConfigurations.turn_stop_strategy || "transcription",
workflowConfigurations.turn_stop_strategy,
);
const [contextCompactionEnabled, setContextCompactionEnabled] = useState(
workflowConfigurations.context_compaction_enabled ?? false,
workflowConfigurations.context_compaction_enabled,
);
const [isSaving, setIsSaving] = useState(false);
const [isUploadingAudio, setIsUploadingAudio] = useState(false);
@ -309,18 +303,18 @@ function GeneralSection({
);
const isDirty = useMemo(() => {
const initAmbient = workflowConfigurations.ambient_noise_configuration || DEFAULT_AMBIENT_NOISE_CONFIG;
const initAmbient = workflowConfigurations.ambient_noise_configuration;
return (
name !== workflowName ||
JSON.stringify(ambientNoiseConfig) !== JSON.stringify(initAmbient) ||
maxCallDuration !== (workflowConfigurations.max_call_duration || 600) ||
maxUserIdleTimeout !== (workflowConfigurations.max_user_idle_timeout || 10) ||
smartTurnStopSecs !== (workflowConfigurations.smart_turn_stop_secs || 2) ||
turnStartStrategy !== (workflowConfigurations.turn_start_strategy || "default") ||
turnStartMinWords !== (workflowConfigurations.turn_start_min_words || DEFAULT_TURN_START_MIN_WORDS) ||
provisionalVadPauseSecs !== (workflowConfigurations.provisional_vad_pause_secs || DEFAULT_PROVISIONAL_VAD_PAUSE_SECS) ||
turnStopStrategy !== (workflowConfigurations.turn_stop_strategy || "transcription") ||
contextCompactionEnabled !== (workflowConfigurations.context_compaction_enabled ?? false)
maxCallDuration !== workflowConfigurations.max_call_duration ||
maxUserIdleTimeout !== workflowConfigurations.max_user_idle_timeout ||
smartTurnStopSecs !== workflowConfigurations.smart_turn_stop_secs ||
turnStartStrategy !== workflowConfigurations.turn_start_strategy ||
turnStartMinWords !== workflowConfigurations.turn_start_min_words ||
provisionalVadPauseSecs !== workflowConfigurations.provisional_vad_pause_secs ||
turnStopStrategy !== workflowConfigurations.turn_stop_strategy ||
contextCompactionEnabled !== workflowConfigurations.context_compaction_enabled
);
}, [name, workflowName, ambientNoiseConfig, maxCallDuration, maxUserIdleTimeout, smartTurnStopSecs, turnStartStrategy, turnStartMinWords, provisionalVadPauseSecs, turnStopStrategy, contextCompactionEnabled, workflowConfigurations]);
@ -611,6 +605,18 @@ function GeneralSection({
</p>
</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">
<Label htmlFor="turn_start_strategy" className="text-xs">Interruption Strategy</Label>
<Select
@ -1428,7 +1434,11 @@ function WorkflowSettingsInner({
);
const initialWorkflowConfigurations = useMemo(
() => (workflow.workflow_configurations as WorkflowConfigurations) || DEFAULT_WORKFLOW_CONFIGURATIONS,
() => (
workflow.workflow_configurations
? (workflow.workflow_configurations as WorkflowConfigurations)
: undefined
),
[workflow],
);

View file

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

File diff suppressed because one or more lines are too long

View file

@ -186,6 +186,21 @@ export type ActiveCallsResponse = {
active_calls: number;
};
/**
* AmbientNoiseConfigurationDefaults
*/
export type AmbientNoiseConfigurationDefaults = {
/**
* Enabled
*/
enabled?: boolean;
/**
* Volume
*/
volume?: number;
[key: string]: unknown;
};
/**
* AmbientNoiseUploadRequest
*/
@ -1878,6 +1893,7 @@ export type DefaultConfigurationsResponse = {
default_providers: {
[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';
/**
* 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
*

View file

@ -227,7 +227,7 @@ export function ServiceConfigurationForm({
console.error("Failed to fetch configurations");
return;
}
defaultsData = response.data as ServiceConfigurationDefaults;
defaultsData = response.data as unknown as ServiceConfigurationDefaults;
}
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;
volume: number;
storage_key?: string;
storage_backend?: string;
original_filename?: string;
}
};
export type TurnStopStrategy = 'transcription' | 'turn_analyzer';
export type TurnStartStrategy = 'default' | 'min_words' | 'provisional_vad';
export type TurnStopStrategy = NonNullable<GeneratedWorkflowConfigurationDefaults["turn_stop_strategy"]>;
export type TurnStartStrategy = NonNullable<GeneratedWorkflowConfigurationDefaults["turn_start_strategy"]>;
export const DEFAULT_TURN_START_MIN_WORDS = 3;
export const DEFAULT_PROVISIONAL_VAD_PAUSE_SECS = 1.5;
@ -81,7 +90,21 @@ export interface ModelOverrides {
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;
max_call_duration: number; // Maximum call duration 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
dictionary?: string; // Comma-separated words for voice agent to listen for
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_configuration_v2_override?: OrganizationAiModelConfigurationV2; // Full v2 model configuration override
[key: string]: unknown; // Allow additional properties for future configurations
}
};
export const DEFAULT_WORKFLOW_CONFIGURATIONS: WorkflowConfigurations = {
const FALLBACK_WORKFLOW_CONFIGURATIONS: WorkflowConfigurations = {
ambient_noise_configuration: {
enabled: false,
volume: 0.3
},
max_call_duration: 600, // 10 minutes
max_call_duration: 300,
max_user_idle_timeout: 10, // 10 seconds
smart_turn_stop_secs: 2, // 2 seconds
turn_start_strategy: 'default', // Default to platform-chosen user turn start detection
turn_start_min_words: DEFAULT_TURN_START_MIN_WORDS,
provisional_vad_pause_secs: DEFAULT_PROVISIONAL_VAD_PAUSE_SECS,
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,
};
}