diff --git a/api/routes/organization.py b/api/routes/organization.py index 3728ddb5..a35eb292 100644 --- a/api/routes/organization.py +++ b/api/routes/organization.py @@ -536,18 +536,6 @@ async def get_model_configuration_preferences_legacy( return await get_preferences(user=user) -@router.put( - "/model-configurations/preferences", - response_model=OrganizationPreferences, - include_in_schema=False, -) -async def save_model_configuration_preferences_legacy( - request: OrganizationPreferences, - user: UserModel = Depends(get_user_with_selected_organization), -): - return await save_preferences(request=request, user=user) - - def preserve_masked_fields(provider: str, request_dict: dict, existing: dict): """If the client re-submitted a masked sensitive field, restore the original.""" for field_name in _sensitive_fields(provider): diff --git a/api/services/workflow/tools/transfer_resolver.py b/api/services/workflow/tools/transfer_resolver.py index d76f2cdf..ce354f5a 100644 --- a/api/services/workflow/tools/transfer_resolver.py +++ b/api/services/workflow/tools/transfer_resolver.py @@ -356,7 +356,7 @@ async def resolve_transfer_config( logger.info( "Transfer destination resolved from context mapping " f"context_path={resolved.metadata.get('context_path')} " - f"resolved={resolved}" + f"source={resolved.source} destination={resolved.destination}" ) return resolved diff --git a/ui/src/app/workflow/[workflowId]/settings/page.tsx b/ui/src/app/workflow/[workflowId]/settings/page.tsx index 65a33c56..bfc1fc2e 100644 --- a/ui/src/app/workflow/[workflowId]/settings/page.tsx +++ b/ui/src/app/workflow/[workflowId]/settings/page.tsx @@ -1,7 +1,7 @@ "use client"; import { format } from "date-fns"; -import { ArrowLeft, BookA, Brain, CalendarIcon, Clipboard, Download, ExternalLink, FileDown, Fingerprint, Loader2, Mic, Pause, PhoneOff, Play, Rocket, Settings, Trash2Icon, Upload, Variable, X } from "lucide-react"; +import { ArrowLeft, BookA, Brain, CalendarIcon, Clipboard, Download, ExternalLink, FileDown, Fingerprint, Loader2, Mic, Pause, PhoneOff, Play, Plus, Rocket, Settings, Trash2Icon, Upload, Variable, X } from "lucide-react"; import Link from "next/link"; import { useParams, useRouter } from "next/navigation"; import { useEffect, useMemo, useRef, useState } from "react"; @@ -38,6 +38,7 @@ import { Separator } from "@/components/ui/separator"; import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; import { SETTINGS_DOCUMENTATION_URLS } from "@/constants/documentation"; +import { useOrgConfig } from "@/context/OrgConfigContext"; import { UnsavedChangesProvider, useUnsavedChanges, useUnsavedChangesContext } from "@/context/UnsavedChangesContext"; import { useAudioPlayback } from "@/hooks/useAudioPlayback"; import { detailFromError } from "@/lib/apiError"; @@ -49,6 +50,7 @@ import { DEFAULT_PROVISIONAL_VAD_PAUSE_SECS, DEFAULT_TURN_START_MIN_WORDS, DEFAULT_VOICEMAIL_DETECTION_CONFIGURATION, + type ExternalPBXFieldMapping, resolveWorkflowConfigurations, TURN_START_STRATEGY_OPTIONS, type TurnStartStrategy, @@ -273,6 +275,7 @@ function GeneralSection({ workflowId: number; onSave: (configurations: WorkflowConfigurations, workflowName: string) => Promise; }) { + const { externalPbxIntegrationsEnabled } = useOrgConfig(); const [name, setName] = useState(workflowName); const [ambientNoiseConfig, setAmbientNoiseConfig] = useState( workflowConfigurations.ambient_noise_configuration, @@ -298,6 +301,9 @@ function GeneralSection({ const [includeTranscriptEndTimestamps, setIncludeTranscriptEndTimestamps] = useState( workflowConfigurations.transcript_configuration?.include_end_timestamps ?? false, ); + const [externalPbxFieldMappings, setExternalPbxFieldMappings] = useState( + workflowConfigurations.external_pbx_field_mappings, + ); const [isSaving, setIsSaving] = useState(false); const [isUploadingAudio, setIsUploadingAudio] = useState(false); const [audioUploadError, setAudioUploadError] = useState(null); @@ -306,6 +312,11 @@ function GeneralSection({ const selectedTurnStartStrategy = TURN_START_STRATEGY_OPTIONS.find( (option) => option.value === turnStartStrategy, ); + const externalPbxFieldMappingsValid = externalPbxFieldMappings.every( + (mapping) => + Boolean(mapping.context_path.trim()) && + /^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(mapping.destination_field.trim()), + ); const isDirty = useMemo(() => { const initAmbient = workflowConfigurations.ambient_noise_configuration; @@ -321,9 +332,11 @@ function GeneralSection({ turnStopStrategy !== workflowConfigurations.turn_stop_strategy || contextCompactionEnabled !== workflowConfigurations.context_compaction_enabled || includeTranscriptEndTimestamps !== - (workflowConfigurations.transcript_configuration?.include_end_timestamps ?? false) + (workflowConfigurations.transcript_configuration?.include_end_timestamps ?? false) || + JSON.stringify(externalPbxFieldMappings) !== + JSON.stringify(workflowConfigurations.external_pbx_field_mappings) ); - }, [name, workflowName, ambientNoiseConfig, maxCallDuration, maxUserIdleTimeout, smartTurnStopSecs, turnStartStrategy, turnStartMinWords, provisionalVadPauseSecs, turnStopStrategy, contextCompactionEnabled, includeTranscriptEndTimestamps, workflowConfigurations]); + }, [name, workflowName, ambientNoiseConfig, maxCallDuration, maxUserIdleTimeout, smartTurnStopSecs, turnStartStrategy, turnStartMinWords, provisionalVadPauseSecs, turnStopStrategy, contextCompactionEnabled, includeTranscriptEndTimestamps, externalPbxFieldMappings, workflowConfigurations]); useUnsavedChanges("general", isDirty); @@ -404,6 +417,7 @@ function GeneralSection({ ...(workflowConfigurations.transcript_configuration ?? {}), include_end_timestamps: includeTranscriptEndTimestamps, }, + external_pbx_field_mappings: externalPbxFieldMappings, }, name, ); @@ -788,10 +802,94 @@ function GeneralSection({ + + {externalPbxIntegrationsEnabled && ( + <> + + + {/* External PBX Field Updates */} +
+
+

External PBX Field Updates

+

+ Optionally copy final gathered-context values into provider-native fields before transfer or hangup. +

+
+
+ + +
+
+ {externalPbxFieldMappings.map((mapping, index) => ( +
+ setExternalPbxFieldMappings((current) => + current.map((item, itemIndex) => + itemIndex === index + ? { ...item, context_path: event.target.value } + : item, + ) + )} + placeholder="qualified" + /> + setExternalPbxFieldMappings((current) => + current.map((item, itemIndex) => + itemIndex === index + ? { ...item, destination_field: event.target.value } + : item, + ) + )} + placeholder="address3" + /> + +
+ ))} + {externalPbxFieldMappings.length === 0 && ( +

+ No external fields will be updated. Context names may be direct extracted-variable names or paths such as extracted_variables.qualified. +

+ )} + {!externalPbxFieldMappingsValid && ( +

+ Each mapping needs a context field and a destination field containing only letters, numbers, and underscores. +

+ )} +
+
+ + )} {isDirty && Unsaved changes} -