mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-16 08:25:18 +02:00
feat: add dictionary support for STT boosting in voice agents (#136)
* feat: add dictionary support for voice agents Also fixes #132 * chore: add keyterms in evals
This commit is contained in:
parent
e3a1e0bf07
commit
db75d90535
23 changed files with 9666 additions and 53 deletions
|
|
@ -6,7 +6,7 @@ import {
|
|||
Panel,
|
||||
ReactFlow,
|
||||
} from "@xyflow/react";
|
||||
import { BrushCleaning, Maximize2, Minus, Plus, Rocket, Settings, Variable } from 'lucide-react';
|
||||
import { BookA, BrushCleaning, Maximize2, Minus, Plus, Rocket, Settings, Variable } from 'lucide-react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { listDocumentsApiV1KnowledgeBaseDocumentsGet, listToolsApiV1ToolsGet } from '@/client';
|
||||
|
|
@ -20,6 +20,7 @@ import AddNodePanel from "../../../components/flow/AddNodePanel";
|
|||
import CustomEdge from "../../../components/flow/edges/CustomEdge";
|
||||
import { AgentNode, EndCall, GlobalNode, StartCall, TriggerNode, WebhookNode } from "../../../components/flow/nodes";
|
||||
import { ConfigurationsDialog } from './components/ConfigurationsDialog';
|
||||
import { DictionaryDialog } from './components/DictionaryDialog';
|
||||
import { EmbedDialog } from './components/EmbedDialog';
|
||||
import { PhoneCallDialog } from './components/PhoneCallDialog';
|
||||
import { TemplateContextVariablesDialog } from './components/TemplateContextVariablesDialog';
|
||||
|
|
@ -63,6 +64,7 @@ interface RenderWorkflowProps {
|
|||
function RenderWorkflow({ initialWorkflowName, workflowId, initialFlow, initialTemplateContextVariables, initialWorkflowConfigurations, user, getAccessToken }: RenderWorkflowProps) {
|
||||
const [isContextVarsDialogOpen, setIsContextVarsDialogOpen] = useState(false);
|
||||
const [isConfigurationsDialogOpen, setIsConfigurationsDialogOpen] = useState(false);
|
||||
const [isDictionaryDialogOpen, setIsDictionaryDialogOpen] = useState(false);
|
||||
const [isEmbedDialogOpen, setIsEmbedDialogOpen] = useState(false);
|
||||
const [isPhoneCallDialogOpen, setIsPhoneCallDialogOpen] = useState(false);
|
||||
const [documents, setDocuments] = useState<DocumentResponseSchema[] | undefined>(undefined);
|
||||
|
|
@ -88,7 +90,9 @@ function RenderWorkflow({ initialWorkflowName, workflowId, initialFlow, initialT
|
|||
onNodesChange,
|
||||
onRun,
|
||||
saveTemplateContextVariables,
|
||||
saveWorkflowConfigurations
|
||||
saveWorkflowConfigurations,
|
||||
dictionary,
|
||||
saveDictionary
|
||||
} = useWorkflowState({
|
||||
initialWorkflowName,
|
||||
workflowId,
|
||||
|
|
@ -238,6 +242,22 @@ function RenderWorkflow({ initialWorkflowName, workflowId, initialFlow, initialT
|
|||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setIsDictionaryDialogOpen(true)}
|
||||
className="bg-white shadow-sm hover:shadow-md"
|
||||
>
|
||||
<BookA className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left">
|
||||
<p>Dictionary</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
|
|
@ -356,6 +376,13 @@ function RenderWorkflow({ initialWorkflowName, workflowId, initialFlow, initialT
|
|||
onSave={saveTemplateContextVariables}
|
||||
/>
|
||||
|
||||
<DictionaryDialog
|
||||
open={isDictionaryDialogOpen}
|
||||
onOpenChange={setIsDictionaryDialogOpen}
|
||||
dictionary={dictionary}
|
||||
onSave={saveDictionary}
|
||||
/>
|
||||
|
||||
<EmbedDialog
|
||||
open={isEmbedDialogOpen}
|
||||
onOpenChange={setIsEmbedDialogOpen}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
interface DictionaryDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
dictionary: string;
|
||||
onSave: (dictionary: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export const DictionaryDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
dictionary,
|
||||
onSave
|
||||
}: DictionaryDialogProps) => {
|
||||
const [dictionaryValue, setDictionaryValue] = useState(dictionary);
|
||||
|
||||
// Sync local state with prop when dialog opens
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setDictionaryValue(dictionary);
|
||||
}
|
||||
}, [open, dictionary]);
|
||||
|
||||
const handleSave = async () => {
|
||||
await onSave(dictionaryValue);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const handleDialogOpenChange = (isOpen: boolean) => {
|
||||
onOpenChange(isOpen);
|
||||
if (isOpen) {
|
||||
setDictionaryValue(dictionary);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleDialogOpenChange}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Dictionary</DialogTitle>
|
||||
<DialogDescription>
|
||||
Add any specific words that you would want the bot to actively listen for. The Voice Agent learns your
|
||||
unique words and names. Add expected words and phrases, company jargon, named entities, or industry-specific lingo. <br/>
|
||||
Example: billing department, tretinoin etc. <br/>
|
||||
(May incur extra cost depending on provider)
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dictionary" className="text-sm font-medium">Words</Label>
|
||||
<Textarea
|
||||
id="dictionary"
|
||||
placeholder="Enter words separated by comma"
|
||||
value={dictionaryValue}
|
||||
onChange={(e) => setDictionaryValue(e.target.value)}
|
||||
rows={4}
|
||||
className="resize-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave}>
|
||||
Save Dictionary
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { Trash2Icon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
|
|
@ -23,6 +23,15 @@ export const TemplateContextVariablesDialog = ({
|
|||
const [newKey, setNewKey] = useState("");
|
||||
const [newValue, setNewValue] = useState("");
|
||||
|
||||
// Sync local state with prop when dialog opens
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setContextVars(templateContextVariables);
|
||||
setNewKey("");
|
||||
setNewValue("");
|
||||
}
|
||||
}, [open, templateContextVariables]);
|
||||
|
||||
const handleAddContextVar = () => {
|
||||
if (newKey && newValue) {
|
||||
setContextVars(prev => ({ ...prev, [newKey]: newValue }));
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import { WorkflowError } from "@/client/types.gen";
|
|||
import { FlowEdge, FlowNode, NodeType } from "@/components/flow/types";
|
||||
import logger from '@/lib/logger';
|
||||
import { getNextNodeId, getRandomId } from "@/lib/utils";
|
||||
import { WorkflowConfigurations } from "@/types/workflow-configurations";
|
||||
import { DEFAULT_WORKFLOW_CONFIGURATIONS, WorkflowConfigurations } from "@/types/workflow-configurations";
|
||||
|
||||
export function getDefaultAllowInterrupt(type: string = NodeType.START_CALL): boolean {
|
||||
switch (type) {
|
||||
|
|
@ -141,6 +141,8 @@ export const useWorkflowState = ({
|
|||
setWorkflowValidationErrors,
|
||||
setTemplateContextVariables,
|
||||
setWorkflowConfigurations,
|
||||
setDictionary,
|
||||
dictionary,
|
||||
clearValidationErrors,
|
||||
markNodeAsInvalid,
|
||||
markEdgeAsInvalid,
|
||||
|
|
@ -174,7 +176,8 @@ export const useWorkflowState = ({
|
|||
initialNodes,
|
||||
initialFlow?.edges ?? [],
|
||||
initialTemplateContextVariables,
|
||||
initialWorkflowConfigurations
|
||||
initialWorkflowConfigurations,
|
||||
initialWorkflowConfigurations?.dictionary ?? ''
|
||||
);
|
||||
}, []);
|
||||
|
||||
|
|
@ -223,6 +226,11 @@ export const useWorkflowState = ({
|
|||
selected: true, // Mark the new node as selected
|
||||
};
|
||||
|
||||
// Deselect all existing nodes before adding the new one
|
||||
const currentNodes = rfInstance.current.getNodes();
|
||||
const deselectedNodes = currentNodes.map(node => ({ ...node, selected: false }));
|
||||
rfInstance.current.setNodes(deselectedNodes);
|
||||
|
||||
// Use addNodes from ReactFlow instance
|
||||
rfInstance.current.addNodes([newNode]);
|
||||
setIsAddNodePanelOpen(false);
|
||||
|
|
@ -326,6 +334,19 @@ export const useWorkflowState = ({
|
|||
await validateWorkflow();
|
||||
}, [workflowId, workflowName, setIsDirty, user, getAccessToken, validateWorkflow]);
|
||||
|
||||
// Set up keyboard shortcut for save (Cmd/Ctrl + S)
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
|
||||
e.preventDefault();
|
||||
saveWorkflow();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [saveWorkflow]);
|
||||
|
||||
const onConnect: OnConnect = useCallback((connection) => {
|
||||
if (!rfInstance.current) return;
|
||||
|
||||
|
|
@ -411,6 +432,9 @@ export const useWorkflowState = ({
|
|||
const saveWorkflowConfigurations = useCallback(async (configurations: WorkflowConfigurations, newWorkflowName: string) => {
|
||||
if (!user) return;
|
||||
const accessToken = await getAccessToken();
|
||||
// Preserve the current dictionary when saving other configurations
|
||||
const currentDictionary = useWorkflowStore.getState().dictionary;
|
||||
const configurationsWithDictionary: WorkflowConfigurations = { ...configurations, dictionary: currentDictionary };
|
||||
try {
|
||||
await updateWorkflowApiV1WorkflowWorkflowIdPut({
|
||||
path: {
|
||||
|
|
@ -419,13 +443,13 @@ export const useWorkflowState = ({
|
|||
body: {
|
||||
name: newWorkflowName,
|
||||
workflow_definition: null,
|
||||
workflow_configurations: configurations as Record<string, unknown>,
|
||||
workflow_configurations: configurationsWithDictionary as Record<string, unknown>,
|
||||
},
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
setWorkflowConfigurations(configurations);
|
||||
setWorkflowConfigurations(configurationsWithDictionary);
|
||||
setWorkflowName(newWorkflowName);
|
||||
logger.info('Workflow configurations saved successfully');
|
||||
} catch (error) {
|
||||
|
|
@ -434,6 +458,34 @@ export const useWorkflowState = ({
|
|||
}
|
||||
}, [workflowId, user, getAccessToken, setWorkflowConfigurations, setWorkflowName]);
|
||||
|
||||
// Save dictionary
|
||||
const saveDictionary = useCallback(async (newDictionary: string) => {
|
||||
if (!user) return;
|
||||
const accessToken = await getAccessToken();
|
||||
const currentConfigurations = useWorkflowStore.getState().workflowConfigurations ?? DEFAULT_WORKFLOW_CONFIGURATIONS;
|
||||
const updatedConfigurations: WorkflowConfigurations = { ...currentConfigurations, dictionary: newDictionary };
|
||||
try {
|
||||
await updateWorkflowApiV1WorkflowWorkflowIdPut({
|
||||
path: {
|
||||
workflow_id: workflowId,
|
||||
},
|
||||
body: {
|
||||
name: workflowName,
|
||||
workflow_definition: null,
|
||||
workflow_configurations: updatedConfigurations as Record<string, unknown>,
|
||||
},
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
setDictionary(newDictionary);
|
||||
setWorkflowConfigurations(updatedConfigurations);
|
||||
} catch (error) {
|
||||
logger.error(`Error saving dictionary: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}, [workflowId, workflowName, user, getAccessToken, setDictionary, setWorkflowConfigurations]);
|
||||
|
||||
// Update rfInstance when it changes
|
||||
useEffect(() => {
|
||||
if (rfInstance.current) {
|
||||
|
|
@ -456,6 +508,7 @@ export const useWorkflowState = ({
|
|||
workflowValidationErrors,
|
||||
templateContextVariables,
|
||||
workflowConfigurations,
|
||||
dictionary,
|
||||
setNodes,
|
||||
setIsDirty,
|
||||
setIsAddNodePanelOpen,
|
||||
|
|
@ -468,6 +521,7 @@ export const useWorkflowState = ({
|
|||
onRun,
|
||||
saveTemplateContextVariables,
|
||||
saveWorkflowConfigurations,
|
||||
saveDictionary,
|
||||
// Export undo/redo state
|
||||
undo,
|
||||
redo,
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ interface WorkflowState {
|
|||
// Configuration
|
||||
templateContextVariables: Record<string, string>;
|
||||
workflowConfigurations: WorkflowConfigurations | null;
|
||||
dictionary: string;
|
||||
|
||||
// ReactFlow instance reference
|
||||
rfInstance: ReactFlowInstance<FlowNode, FlowEdge> | null;
|
||||
|
|
@ -48,7 +49,8 @@ interface WorkflowActions {
|
|||
nodes: FlowNode[],
|
||||
edges: FlowEdge[],
|
||||
templateContextVariables?: Record<string, string>,
|
||||
workflowConfigurations?: WorkflowConfigurations | null
|
||||
workflowConfigurations?: WorkflowConfigurations | null,
|
||||
dictionary?: string
|
||||
) => void;
|
||||
|
||||
// History management
|
||||
|
|
@ -74,6 +76,7 @@ interface WorkflowActions {
|
|||
setWorkflowName: (name: string) => void;
|
||||
setTemplateContextVariables: (variables: Record<string, string>) => void;
|
||||
setWorkflowConfigurations: (configurations: WorkflowConfigurations) => void;
|
||||
setDictionary: (dictionary: string) => void;
|
||||
|
||||
// UI state
|
||||
setIsDirty: (isDirty: boolean) => void;
|
||||
|
|
@ -110,10 +113,11 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
|
|||
workflowValidationErrors: [],
|
||||
templateContextVariables: {},
|
||||
workflowConfigurations: DEFAULT_WORKFLOW_CONFIGURATIONS,
|
||||
dictionary: '',
|
||||
rfInstance: null,
|
||||
|
||||
// Actions
|
||||
initializeWorkflow: (workflowId, workflowName, nodes, edges, templateContextVariables = {}, workflowConfigurations = DEFAULT_WORKFLOW_CONFIGURATIONS) => {
|
||||
initializeWorkflow: (workflowId, workflowName, nodes, edges, templateContextVariables = {}, workflowConfigurations = DEFAULT_WORKFLOW_CONFIGURATIONS, dictionary = '') => {
|
||||
const initialHistory: HistoryState = { nodes, edges, workflowName };
|
||||
set({
|
||||
workflowId,
|
||||
|
|
@ -122,6 +126,7 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
|
|||
edges,
|
||||
templateContextVariables,
|
||||
workflowConfigurations,
|
||||
dictionary,
|
||||
isDirty: false,
|
||||
workflowValidationErrors: [],
|
||||
history: [initialHistory],
|
||||
|
|
@ -195,18 +200,28 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
|
|||
setNodes: (nodes, changes) => {
|
||||
// Determine whether to push to history and set isDirty based on change types
|
||||
if (changes && changes.length > 0) {
|
||||
// Check if any changes are user-initiated (not just selections or dimensions)
|
||||
const hasDirtyChanges = changes.some(change =>
|
||||
change.type === 'add' ||
|
||||
change.type === 'remove' ||
|
||||
(change.type === 'position' && change.dragging)
|
||||
// Check for add/remove changes (always push to history)
|
||||
const hasAddRemoveChanges = changes.some(change =>
|
||||
change.type === 'add' || change.type === 'remove'
|
||||
);
|
||||
|
||||
if (hasDirtyChanges) {
|
||||
// Check for position changes - only push to history when drag ENDS (dragging: false)
|
||||
// but still mark as dirty during dragging
|
||||
const hasDragEndChanges = changes.some(change =>
|
||||
change.type === 'position' && change.dragging === false
|
||||
);
|
||||
const isActiveDragging = changes.some(change =>
|
||||
change.type === 'position' && change.dragging === true
|
||||
);
|
||||
|
||||
if (hasAddRemoveChanges || hasDragEndChanges) {
|
||||
get().pushToHistory();
|
||||
set({ nodes, isDirty: true });
|
||||
} else if (isActiveDragging) {
|
||||
// During active dragging, update nodes but don't push to history
|
||||
set({ nodes, isDirty: true });
|
||||
} else {
|
||||
// For selection changes or dimension updates, don't push to history
|
||||
// For selection changes or dimension updates, don't push to history or set dirty
|
||||
set({ nodes });
|
||||
}
|
||||
} else {
|
||||
|
|
@ -312,6 +327,10 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
|
|||
set({ workflowConfigurations });
|
||||
},
|
||||
|
||||
setDictionary: (dictionary) => {
|
||||
set({ dictionary });
|
||||
},
|
||||
|
||||
setIsDirty: (isDirty) => {
|
||||
set({ isDirty });
|
||||
},
|
||||
|
|
@ -375,6 +394,7 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
|
|||
workflowValidationErrors: [],
|
||||
templateContextVariables: {},
|
||||
workflowConfigurations: DEFAULT_WORKFLOW_CONFIGURATIONS,
|
||||
dictionary: '',
|
||||
rfInstance: null,
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ export default function CustomEdge(props: CustomEdgeProps) {
|
|||
: isHovered
|
||||
? 'drop-shadow(0 0 6px rgba(96, 165, 250, 0.4))'
|
||||
: 'none',
|
||||
transition: 'all 0.2s ease',
|
||||
transition: 'stroke 0.2s ease, stroke-width 0.2s ease, filter 0.2s ease',
|
||||
}}
|
||||
interactionWidth={20}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export interface WorkflowConfigurations {
|
|||
ambient_noise_configuration: AmbientNoiseConfiguration;
|
||||
max_call_duration: number; // Maximum call duration in seconds
|
||||
max_user_idle_timeout: number; // Maximum user idle time in seconds
|
||||
dictionary?: string; // Comma-separated words for voice agent to listen for
|
||||
[key: string]: unknown; // Allow additional properties for future configurations
|
||||
}
|
||||
|
||||
|
|
@ -30,5 +31,6 @@ export const DEFAULT_WORKFLOW_CONFIGURATIONS: WorkflowConfigurations = {
|
|||
volume: 0.3
|
||||
},
|
||||
max_call_duration: 600, // 10 minutes
|
||||
max_user_idle_timeout: 10 // 10 seconds
|
||||
max_user_idle_timeout: 10, // 10 seconds
|
||||
dictionary: ''
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue