feat: add AWS Bedrock support

This commit is contained in:
Abhishek Kumar 2026-03-19 15:06:59 +05:30
parent 1604e306ec
commit fe84f086ba
30 changed files with 546 additions and 195 deletions

View file

@ -125,6 +125,7 @@ export default function CampaignsPage() {
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead>Name</TableHead>
<TableHead>Workflow</TableHead>
<TableHead>State</TableHead>
@ -139,6 +140,7 @@ export default function CampaignsPage() {
className="cursor-pointer hover:bg-muted/50"
onClick={() => handleRowClick(campaign.id)}
>
<TableCell>{campaign.id}</TableCell>
<TableCell className="font-medium">{campaign.name}</TableCell>
<TableCell>{campaign.workflow_name}</TableCell>
<TableCell>

View file

@ -6,7 +6,7 @@ import {
Panel,
ReactFlow,
} from "@xyflow/react";
import { BookA, BrushCleaning, Maximize2, Mic, Minus, Plus, Rocket, Settings, Variable } from 'lucide-react';
import { BookA, BrushCleaning, Maximize2, Mic, Minus, PhoneOff, Plus, Rocket, Settings, Variable } from 'lucide-react';
import React, { useEffect, useMemo, useState } from 'react';
import { listDocumentsApiV1KnowledgeBaseDocumentsGet, listRecordingsApiV1WorkflowRecordingsGet, listToolsApiV1ToolsGet } from '@/client';
@ -25,6 +25,7 @@ import { EmbedDialog } from './components/EmbedDialog';
import { PhoneCallDialog } from './components/PhoneCallDialog';
import { RecordingsDialog } from './components/RecordingsDialog';
import { TemplateContextVariablesDialog } from './components/TemplateContextVariablesDialog';
import { VoicemailDetectionDialog } from './components/VoicemailDetectionDialog';
import { WorkflowEditorHeader } from "./components/WorkflowEditorHeader";
import { WorkflowProvider } from "./contexts/WorkflowContext";
import { useWorkflowState } from "./hooks/useWorkflowState";
@ -69,6 +70,7 @@ function RenderWorkflow({ initialWorkflowName, workflowId, initialFlow, initialT
const [isEmbedDialogOpen, setIsEmbedDialogOpen] = useState(false);
const [isPhoneCallDialogOpen, setIsPhoneCallDialogOpen] = useState(false);
const [isRecordingsDialogOpen, setIsRecordingsDialogOpen] = useState(false);
const [isVoicemailDialogOpen, setIsVoicemailDialogOpen] = useState(false);
const [documents, setDocuments] = useState<DocumentResponseSchema[] | undefined>(undefined);
const [tools, setTools] = useState<ToolResponse[] | undefined>(undefined);
const [recordings, setRecordings] = useState<RecordingResponseSchema[]>([]);
@ -283,6 +285,22 @@ function RenderWorkflow({ initialWorkflowName, workflowId, initialFlow, initialT
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
onClick={() => setIsVoicemailDialogOpen(true)}
className="bg-white shadow-sm hover:shadow-md"
>
<PhoneOff className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="left">
<p>Voicemail Detection</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
@ -428,6 +446,13 @@ function RenderWorkflow({ initialWorkflowName, workflowId, initialFlow, initialT
workflowId={workflowId}
onRecordingsChange={setRecordings}
/>
<VoicemailDetectionDialog
open={isVoicemailDialogOpen}
onOpenChange={setIsVoicemailDialogOpen}
workflowConfigurations={workflowConfigurations}
onSave={saveWorkflowConfigurations}
/>
</div>
</WorkflowProvider>
);

View file

@ -0,0 +1,205 @@
import { useEffect, useState } from "react";
import { LLMConfigSelector } from "@/components/LLMConfigSelector";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import {
DEFAULT_VOICEMAIL_DETECTION_CONFIGURATION,
type VoicemailDetectionConfiguration,
type WorkflowConfigurations,
} from "@/types/workflow-configurations";
// Must match VoicemailDetector.DEFAULT_SYSTEM_PROMPT in pipecat
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"):
- Personal greetings: "Hello?", "Hi", "Yeah?", "John speaking"
- Interactive responses: "Who is this?", "What do you want?", "Can I help you?"
- Conversational tone expecting back-and-forth dialogue
- Questions directed at the caller: "Hello? Anyone there?"
- Informal responses: "Yep", "What's up?", "Speaking"
- Natural, spontaneous speech patterns
- Immediate acknowledgment of the call
VOICEMAIL SYSTEM (respond "VOICEMAIL"):
- Automated voicemail greetings: "Hi, you've reached [name], please leave a message"
- Phone carrier messages: "The number you have dialed is not in service", "Please leave a message", "All circuits are busy"
- Professional voicemail: "This is [name], I'm not available right now"
- Instructions about leaving messages: "leave a message", "leave your name and number"
- References to callback or messaging: "call me back", "I'll get back to you"
- Carrier system messages: "mailbox is full", "has not been set up"
- Business hours messages: "our office is currently closed"
Respond with ONLY "CONVERSATION" if a person answered, or "VOICEMAIL" if it's voicemail/recording.`;
interface VoicemailDetectionDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
workflowConfigurations: WorkflowConfigurations;
onSave: (configurations: WorkflowConfigurations) => void;
}
export const VoicemailDetectionDialog = ({
open,
onOpenChange,
workflowConfigurations,
onSave,
}: VoicemailDetectionDialogProps) => {
const getConfig = (): VoicemailDetectionConfiguration => ({
...DEFAULT_VOICEMAIL_DETECTION_CONFIGURATION,
...workflowConfigurations.voicemail_detection,
});
const [enabled, setEnabled] = useState(getConfig().enabled);
const [useWorkflowLlm, setUseWorkflowLlm] = useState(getConfig().use_workflow_llm);
const [provider, setProvider] = useState(getConfig().provider || "openai");
const [model, setModel] = useState(getConfig().model || "gpt-4.1");
const [apiKey, setApiKey] = useState(getConfig().api_key || "");
const [systemPrompt, setSystemPrompt] = useState(getConfig().system_prompt || DEFAULT_VOICEMAIL_SYSTEM_PROMPT);
const [longSpeechTimeout, setLongSpeechTimeout] = useState(getConfig().long_speech_timeout);
// Sync state from props whenever the dialog opens
useEffect(() => {
if (open) {
const config = {
...DEFAULT_VOICEMAIL_DETECTION_CONFIGURATION,
...workflowConfigurations.voicemail_detection,
};
setEnabled(config.enabled);
setUseWorkflowLlm(config.use_workflow_llm);
setProvider(config.provider || "openai");
setModel(config.model || "gpt-4.1");
setApiKey(config.api_key || "");
setSystemPrompt(config.system_prompt || DEFAULT_VOICEMAIL_SYSTEM_PROMPT);
setLongSpeechTimeout(config.long_speech_timeout);
}
}, [open, workflowConfigurations]);
const handleOpenChange = (newOpen: boolean) => {
onOpenChange(newOpen);
};
const handleSave = () => {
const voicemailConfig: VoicemailDetectionConfiguration = {
enabled,
use_workflow_llm: useWorkflowLlm,
provider: useWorkflowLlm ? undefined : provider,
model: useWorkflowLlm ? undefined : model,
api_key: useWorkflowLlm ? undefined : apiKey,
system_prompt: systemPrompt && systemPrompt !== DEFAULT_VOICEMAIL_SYSTEM_PROMPT ? systemPrompt : undefined,
long_speech_timeout: longSpeechTimeout,
};
onSave({
...workflowConfigurations,
voicemail_detection: voicemailConfig,
});
onOpenChange(false);
};
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="max-w-lg max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Voicemail Detection</DialogTitle>
<DialogDescription>
Configure voicemail detection to automatically detect and end calls
when a voicemail system is reached.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="flex items-center space-x-2 p-2 border rounded-md bg-muted/20">
<Switch
id="voicemail-enabled"
checked={enabled}
onCheckedChange={setEnabled}
/>
<Label htmlFor="voicemail-enabled">Enable Voicemail Detection</Label>
</div>
{enabled && (
<>
{/* LLM Configuration */}
<div className="space-y-3">
<div className="flex items-center space-x-2 p-2 border rounded-md bg-muted/20">
<Switch
id="voicemail-use-workflow-llm"
checked={useWorkflowLlm}
onCheckedChange={setUseWorkflowLlm}
/>
<Label htmlFor="voicemail-use-workflow-llm">Use Workflow LLM</Label>
<Label className="text-xs text-muted-foreground ml-2">
Use the LLM configured in your account settings.
</Label>
</div>
{!useWorkflowLlm && (
<LLMConfigSelector
provider={provider}
onProviderChange={setProvider}
model={model}
onModelChange={setModel}
apiKey={apiKey}
onApiKeyChange={setApiKey}
/>
)}
</div>
{/* System Prompt */}
<div className="grid gap-2">
<Label>System Prompt</Label>
<Label className="text-xs text-muted-foreground">
Prompt for voicemail classification.
The LLM must respond with either &quot;CONVERSATION&quot; or &quot;VOICEMAIL&quot;.
</Label>
<Textarea
value={systemPrompt}
onChange={(e) => setSystemPrompt(e.target.value)}
className="min-h-[200px] font-mono text-xs"
/>
</div>
{/* Timing Configuration */}
<div className="grid gap-4 p-3 border rounded-md bg-muted/10">
<Label className="font-medium">Timing</Label>
<div className="space-y-2">
<Label className="text-sm">Speech Cutoff (seconds)</Label>
<Label className="text-xs text-muted-foreground">
Trigger classification early if first turn speech exceeds this duration.
</Label>
<Input
type="number"
step="0.5"
min="1"
max="30"
value={longSpeechTimeout}
onChange={(e) => setLongSpeechTimeout(parseFloat(e.target.value) || 8.0)}
/>
</div>
</div>
</>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button onClick={handleSave}>Save</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View file

@ -1242,7 +1242,10 @@ export const getCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGet = <Thr
/**
* Get Mps Credits
* Get usage and quota from MPS for the user's configured Dograh service keys.
* Get aggregated usage and quota from MPS.
*
* OSS users: queries by provider_id (created_by).
* Hosted users: queries by organization_id.
*/
export const getMpsCreditsApiV1OrganizationsUsageMpsCreditsGet = <ThrowOnError extends boolean = false>(options?: Options<GetMpsCreditsApiV1OrganizationsUsageMpsCreditsGetData, ThrowOnError>) => {
return (options?.client ?? _heyApiClient).get<GetMpsCreditsApiV1OrganizationsUsageMpsCreditsGetResponse, GetMpsCreditsApiV1OrganizationsUsageMpsCreditsGetError, ThrowOnError>({

View file

@ -1238,7 +1238,7 @@ export type UpdateToolRequest = {
};
export type UpdateWorkflowRequest = {
name: string;
name?: string | null;
workflow_definition?: {
[key: string]: unknown;
} | null;
@ -1266,16 +1266,16 @@ export type UsageHistoryResponse = {
export type UserConfigurationRequestResponseSchema = {
llm?: {
[key: string]: string | number | Array<string>;
[key: string]: string | number | Array<string> | null;
} | null;
tts?: {
[key: string]: string | number | Array<string>;
[key: string]: string | number | Array<string> | null;
} | null;
stt?: {
[key: string]: string | number | Array<string>;
[key: string]: string | number | Array<string> | null;
} | null;
embeddings?: {
[key: string]: string | number | Array<string>;
[key: string]: string | number | Array<string> | null;
} | null;
test_phone_number?: string | null;
timezone?: string | null;

View file

@ -24,6 +24,8 @@ import { useNodeHandlers } from "./common/useNodeHandlers";
interface StartCallEditFormProps {
nodeData: FlowNodeData;
greeting: string;
setGreeting: (value: string) => void;
prompt: string;
setPrompt: (value: string) => void;
name: string;
@ -32,8 +34,6 @@ interface StartCallEditFormProps {
setAllowInterrupt: (value: boolean) => void;
addGlobalPrompt: boolean;
setAddGlobalPrompt: (value: boolean) => void;
detectVoicemail: boolean;
setDetectVoicemail: (value: boolean) => void;
delayedStart: boolean;
setDelayedStart: (value: boolean) => void;
delayedStartDuration: number;
@ -65,11 +65,11 @@ export const StartCall = memo(({ data, selected, id }: StartCallNodeProps) => {
const { saveWorkflow, tools, documents, recordings } = useWorkflow();
// Form state
const [greeting, setGreeting] = useState(data.greeting ?? "");
const [prompt, setPrompt] = useState(data.prompt ?? "");
const [name, setName] = useState(data.name);
const [allowInterrupt, setAllowInterrupt] = useState(data.allow_interrupt ?? true);
const [addGlobalPrompt, setAddGlobalPrompt] = useState(data.add_global_prompt ?? true);
const [detectVoicemail, setDetectVoicemail] = useState(data.detect_voicemail ?? false);
const [delayedStart, setDelayedStart] = useState(data.delayed_start ?? false);
const [delayedStartDuration, setDelayedStartDuration] = useState(data.delayed_start_duration ?? 2);
const [extractionEnabled, setExtractionEnabled] = useState(data.extraction_enabled ?? false);
@ -78,22 +78,23 @@ export const StartCall = memo(({ data, selected, id }: StartCallNodeProps) => {
const [toolUuids, setToolUuids] = useState<string[]>(data.tool_uuids ?? []);
const [documentUuids, setDocumentUuids] = useState<string[]>(data.document_uuids ?? []);
// Compute if form has unsaved changes (only check prompt, name)
// Compute if form has unsaved changes (only check prompt, name, greeting)
const isDirty = useMemo(() => {
return (
greeting !== (data.greeting ?? "") ||
prompt !== (data.prompt ?? "") ||
name !== (data.name ?? "")
);
}, [prompt, name, data]);
}, [greeting, prompt, name, data]);
const handleSave = async () => {
handleSaveNodeData({
...data,
greeting: greeting || undefined,
prompt,
name,
allow_interrupt: allowInterrupt,
add_global_prompt: addGlobalPrompt,
detect_voicemail: detectVoicemail,
delayed_start: delayedStart,
delayed_start_duration: delayedStart ? delayedStartDuration : undefined,
extraction_enabled: extractionEnabled,
@ -112,11 +113,11 @@ export const StartCall = memo(({ data, selected, id }: StartCallNodeProps) => {
// Reset form state when dialog opens
const handleOpenChange = (newOpen: boolean) => {
if (newOpen) {
setGreeting(data.greeting ?? "");
setPrompt(data.prompt ?? "");
setName(data.name);
setAllowInterrupt(data.allow_interrupt ?? true);
setAddGlobalPrompt(data.add_global_prompt ?? true);
setDetectVoicemail(data.detect_voicemail ?? false);
setDelayedStart(data.delayed_start ?? false);
setDelayedStartDuration(data.delayed_start_duration ?? 3);
setExtractionEnabled(data.extraction_enabled ?? false);
@ -131,11 +132,11 @@ export const StartCall = memo(({ data, selected, id }: StartCallNodeProps) => {
// Update form state when data changes (e.g., from undo/redo)
useEffect(() => {
if (open) {
setGreeting(data.greeting ?? "");
setPrompt(data.prompt ?? "");
setName(data.name);
setAllowInterrupt(data.allow_interrupt ?? true);
setAddGlobalPrompt(data.add_global_prompt ?? true);
setDetectVoicemail(data.detect_voicemail ?? false);
setDelayedStart(data.delayed_start ?? false);
setDelayedStartDuration(data.delayed_start_duration ?? 3);
setExtractionEnabled(data.extraction_enabled ?? false);
@ -225,6 +226,8 @@ export const StartCall = memo(({ data, selected, id }: StartCallNodeProps) => {
{open && (
<StartCallEditForm
nodeData={data}
greeting={greeting}
setGreeting={setGreeting}
prompt={prompt}
setPrompt={setPrompt}
name={name}
@ -233,8 +236,6 @@ export const StartCall = memo(({ data, selected, id }: StartCallNodeProps) => {
setAllowInterrupt={setAllowInterrupt}
addGlobalPrompt={addGlobalPrompt}
setAddGlobalPrompt={setAddGlobalPrompt}
detectVoicemail={detectVoicemail}
setDetectVoicemail={setDetectVoicemail}
delayedStart={delayedStart}
setDelayedStart={setDelayedStart}
delayedStartDuration={delayedStartDuration}
@ -260,6 +261,8 @@ export const StartCall = memo(({ data, selected, id }: StartCallNodeProps) => {
});
const StartCallEditForm = ({
greeting,
setGreeting,
prompt,
setPrompt,
name,
@ -268,8 +271,6 @@ const StartCallEditForm = ({
setAllowInterrupt,
addGlobalPrompt,
setAddGlobalPrompt,
detectVoicemail,
setDetectVoicemail,
delayedStart,
setDelayedStart,
delayedStartDuration,
@ -326,6 +327,18 @@ const StartCallEditForm = ({
onChange={(e) => setName(e.target.value)}
/>
<Label>Greeting</Label>
<Label className="text-xs text-muted-foreground">
Optional greeting message played via TTS when the call starts. If set, this will be spoken directly instead of generating a response from the LLM. Supports template variables like {"{{variable_name}}"}.
</Label>
<MentionTextarea
value={greeting}
onChange={setGreeting}
className="min-h-[60px] max-h-[200px] resize-none overflow-y-auto"
placeholder="e.g. Hello {{first_name}}, this is Sarah calling from Acme Corp."
recordings={recordings}
/>
<Label>Prompt</Label>
<Label className="text-xs text-muted-foreground">
Enter the prompt for the agent. This will be used to generate the agent&apos;s response. Prompt engineering&apos;s best practices apply.
@ -354,19 +367,6 @@ const StartCallEditForm = ({
Add Global Prompt
</Label>
</div>
<div className="flex items-center space-x-2">
<Switch
id="detect-voicemail"
checked={detectVoicemail}
onCheckedChange={setDetectVoicemail}
/>
<Label htmlFor="detect-voicemail">
Detect Voicemail
</Label>
<Label className="text-xs text-muted-foreground">
Automatically detect and end call if voicemail is reached.
</Label>
</div>
<div className="flex flex-col space-y-2">
<div className="flex items-center space-x-2">
<Switch

View file

@ -23,6 +23,7 @@ export type FlowNodeData = {
extraction_prompt?: string;
extraction_variables?: ExtractionVariable[];
add_global_prompt?: boolean;
greeting?: string;
wait_for_user_greeting?: boolean;
detect_voicemail?: boolean;
delayed_start?: boolean;

View file

@ -12,6 +12,22 @@ export interface AmbientNoiseConfiguration {
export type TurnStopStrategy = 'transcription' | 'turn_analyzer';
export interface VoicemailDetectionConfiguration {
enabled: boolean;
use_workflow_llm: boolean;
provider?: string;
model?: string;
api_key?: string;
system_prompt?: string;
long_speech_timeout: number; // seconds cutoff for long speech detection
}
export const DEFAULT_VOICEMAIL_DETECTION_CONFIGURATION: VoicemailDetectionConfiguration = {
enabled: false,
use_workflow_llm: true,
long_speech_timeout: 8.0,
};
export interface WorkflowConfigurations {
vad_configuration?: VADConfiguration;
ambient_noise_configuration: AmbientNoiseConfiguration;
@ -20,6 +36,7 @@ export interface WorkflowConfigurations {
smart_turn_stop_secs: number; // Timeout in seconds for incomplete turn detection
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;
[key: string]: unknown; // Allow additional properties for future configurations
}