Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enable choice trace (COR-000) #735

Closed
wants to merge 12 commits into from
3 changes: 2 additions & 1 deletion lib/services/runtime/handlers/state/preliminary/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { VoiceflowNode } from '@voiceflow/voiceflow-types';

import { Action, Handler, HandlerFactory, IfV2Handler } from '@/runtime';
import { Action, FunctionHandler, Handler, HandlerFactory, IfV2Handler } from '@/runtime';

import { isAlexaEventIntentRequest } from '../../../types';
import _V1Handler from '../../_v1';
Expand All @@ -14,6 +14,7 @@ import InteractionHandler from '../../interaction';

const _v1Handler = _V1Handler();
export const eventHandlers = [
FunctionHandler(),
...GoToHandler(),
CaptureHandler(),
...CaptureV2Handler(),
Expand Down
4 changes: 2 additions & 2 deletions lib/services/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { performance } from 'perf_hooks';

import { executeFunction } from '@/runtime/lib/Handlers/function/lib/execute-function/execute-function';
import { createFunctionExceptionDebugTrace } from '@/runtime/lib/Handlers/function/lib/function-exception/function.exception';
import { Trace } from '@/runtime/lib/Handlers/function/runtime-command/trace-command.dto';
import { UnknownTrace } from '@/runtime/lib/Handlers/function/runtime-command/trace/base.dto';

import { AbstractManager } from '../utils';
import { TestFunctionResponse } from './interface';
Expand Down Expand Up @@ -43,7 +43,7 @@ export class TestService extends AbstractManager {

const executionTime = endTime - startTime;

const debugTrace: Trace = createFunctionExceptionDebugTrace(err);
const debugTrace: UnknownTrace = createFunctionExceptionDebugTrace(err);

return {
success: false,
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"@voiceflow/utils-designer": "1.9.3",
"@voiceflow/verror": "^1.1.3",
"@voiceflow/voice-types": "2.9.83",
"@voiceflow/voiceflow-types": "3.27.0",
"@voiceflow/voiceflow-types": "3.28.0",
"ajv": "6.12.3",
"axios": "^0.21.1",
"compression": "^1.7.4",
Expand Down Expand Up @@ -66,6 +66,7 @@
"slate": "0.72.3",
"talisman": "^1.1.3",
"ts-pattern": "^4.0.5",
"underscore-query": "^3.3.2",
"unleash-client": "5.0.0",
"validator": "^13.7.0",
"words-to-numbers": "^1.5.1",
Expand Down
92 changes: 84 additions & 8 deletions runtime/lib/Handlers/function/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,20 @@ import {
FunctionCompiledNode,
NodeType,
} from '@voiceflow/dtos';
import { VoiceflowConstants } from '@voiceflow/voiceflow-types';
import _query from 'utils/underscore-query';

import { HandlerFactory } from '@/runtime/lib/Handler';

import Runtime from '../../Runtime';
import Store from '../../Runtime/Store';
import { executeFunction } from './lib/execute-function/execute-function';
import { createFunctionExceptionDebugTrace } from './lib/function-exception/function.exception';
import { NextCommand } from './runtime-command/next-command.dto';
import { createFunctionRequestContext, FunctionRequestContext } from './lib/request-context/request-context';
import { NextBranches, NextBranchesDTO, NextCommand } from './runtime-command/next-command.dto';
import { OutputVarsCommand } from './runtime-command/output-vars-command.dto';
import { TraceCommand } from './runtime-command/trace-command.dto';
import { Transfer, TransferType } from './runtime-command/transfer/transfer.dto';

const utilsObj = {
replaceVariables,
Expand All @@ -25,9 +29,15 @@ const utilsObj = {
function applyOutputCommand(
command: OutputVarsCommand,
runtime: Runtime,
variables: Store,
outputVarDeclarations: FunctionCompiledDefinition['outputVars'],
outputVarAssignments: FunctionCompiledInvocation['outputVars']
{
variables,
outputVarDeclarations,
outputVarAssignments,
}: {
variables: Store;
outputVarDeclarations: FunctionCompiledDefinition['outputVars'];
outputVarAssignments: FunctionCompiledInvocation['outputVars'];
}
): void {
Object.keys(outputVarDeclarations).forEach((functionVarName) => {
const diagramVariableName = outputVarAssignments[functionVarName];
Expand All @@ -45,20 +55,83 @@ function applyTraceCommand(command: TraceCommand, runtime: Runtime): void {
});
}

function applyNextCommand(command: NextCommand, paths: FunctionCompiledInvocation['paths']): string | null {
function applyNextCommand(
command: NextCommand,
runtime: Runtime,
{ nodeId, paths }: { nodeId: string; paths: FunctionCompiledInvocation['paths'] }
): string | null {
if ('listen' in command) {
if (!command.listen) return null;

const { defaultTo, to } = command;
runtime.variables.set(VoiceflowConstants.BuiltInVariable.FUNCTION_CONDITIONAL_TRANSFERS, { defaultTo, to });

return nodeId;
}
if ('path' in command) {
return paths[command.path] ?? null;
}
return null;
}

function applyTransfer(transfer: string | Transfer, paths: FunctionCompiledInvocation['paths']) {
// Case 1 - `transfer` is a path string that must be mapped
if (typeof transfer === 'string') {
return paths[transfer];
}

// Case 2 - `transfer` is a Transfer object that can be anything such as a PathTransfer
if (transfer.type === TransferType.PATH) {
return paths[transfer.path];
}

throw new Error(`Function produced a transfer object with an unexpected type '${transfer.type}'`);
}

function handleListenResponse(
conditionalTransfers: NextBranches,
requestContext: FunctionRequestContext,
paths: FunctionCompiledInvocation['paths']
): string {
const firstMatchingTransfer = conditionalTransfers.to.find((item) => _query([requestContext], item.on).length > 0);

if (!firstMatchingTransfer) {
return applyTransfer(conditionalTransfers.defaultTo, paths);
}

return applyTransfer(firstMatchingTransfer.dest, paths);
}

export const FunctionHandler: HandlerFactory<FunctionCompiledNode, typeof utilsObj> = (utils) => ({
canHandle: (node) => node.type === NodeType.FUNCTION,

handle: async (node, runtime, variables): Promise<string | null> => {
const { definition, invocation } = node.data;

try {
const parsedTransfers = NextBranchesDTO.safeParse(
runtime.variables.get(VoiceflowConstants.BuiltInVariable.FUNCTION_CONDITIONAL_TRANSFERS)
);

/**
* Case 1 - If there is a `parsedTransfers`, then we are resuming Function step execution after
* obtaining user input
*/
if (parsedTransfers.success) {
const conditionalTransfers = parsedTransfers.data;
const requestContext = createFunctionRequestContext(runtime);

const nextId = handleListenResponse(conditionalTransfers, requestContext, invocation.paths);

runtime.variables.set(VoiceflowConstants.BuiltInVariable.FUNCTION_CONDITIONAL_TRANSFERS, null);

return nextId;
}

/**
* Case 2 - If there are no `parsedTransfers`, then we are hitting this Function step for the
* first time
*/
const resolvedInputMapping = Object.entries(invocation.inputVars).reduce((acc, [varName, value]) => {
return {
...acc,
Expand All @@ -77,18 +150,21 @@ export const FunctionHandler: HandlerFactory<FunctionCompiledNode, typeof utilsO
});

if (outputVars) {
applyOutputCommand(outputVars, runtime, variables, definition.outputVars, invocation.outputVars);
applyOutputCommand(outputVars, runtime, {
variables,
outputVarDeclarations: definition.outputVars,
outputVarAssignments: invocation.outputVars,
});
}

if (trace) {
applyTraceCommand(trace, runtime);
}

if (definition.pathCodes.length === 0) {
return invocation.paths.__vf__default ?? null;
}
if (next) {
return applyNextCommand(next, invocation.paths);
return applyNextCommand(next, runtime, { nodeId: node.id, paths: invocation.paths });
}
return null;
} catch (err) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
import { BaseNode, BaseRequest, BaseTrace } from '@voiceflow/base-types';
import { Utils } from '@voiceflow/common';

import {
SimpleAction,
SimpleAudioTrace,
SimpleButton,
SimpleCard,
SimpleCardV2Trace,
SimpleCarouselTrace,
SimpleSpeakTrace,
SimpleTextTrace,
SimpleTraceType,
SimpleVisualTrace,
Trace,
} from '../../../runtime-command/trace-command.dto';
import { SimpleAction, SimpleActionButton } from '../../../runtime-command/button/action-button.dto';
import { SimpleGeneralButton } from '../../../runtime-command/button/general-button.dto';
import { SimpleCard } from '../../../runtime-command/card/card.dto';
import { SimpleAudioTrace } from '../../../runtime-command/trace/audio.dto';
import { UnknownTrace } from '../../../runtime-command/trace/base.dto';
import { SimpleCardV2Trace } from '../../../runtime-command/trace/cardV2.dto';
import { SimpleCarouselTrace } from '../../../runtime-command/trace/carousel.dto';
import { SimpleChoiceTrace } from '../../../runtime-command/trace/choice.dto';
import { SimpleTraceType } from '../../../runtime-command/trace/simple-trace-type.enum';
import { SimpleSpeakTrace } from '../../../runtime-command/trace/speak.dto';
import { SimpleTextTrace } from '../../../runtime-command/trace/text.dto';
import { SimpleVisualTrace } from '../../../runtime-command/trace/visual.dto';
import { isSimpleTrace } from './is-simple-trace';

const { cuid } = Utils.id;
const { TraceType } = BaseTrace;

const adaptTextTrace = (trace: SimpleTextTrace): Trace => {
const adaptTextTrace = (trace: SimpleTextTrace): UnknownTrace => {
return {
...trace,
type: TraceType.TEXT,
Expand All @@ -33,7 +32,7 @@ const adaptTextTrace = (trace: SimpleTextTrace): Trace => {
} satisfies BaseTrace.TextTrace;
};

const adaptSpeakTrace = (trace: SimpleSpeakTrace): Trace => {
const adaptSpeakTrace = (trace: SimpleSpeakTrace): UnknownTrace => {
return {
...trace,
type: TraceType.SPEAK,
Expand All @@ -44,7 +43,7 @@ const adaptSpeakTrace = (trace: SimpleSpeakTrace): Trace => {
} satisfies BaseTrace.SpeakTrace;
};

const adaptAudioTrace = (trace: SimpleAudioTrace): Trace => {
const adaptAudioTrace = (trace: SimpleAudioTrace): UnknownTrace => {
return {
...trace,
type: TraceType.SPEAK,
Expand All @@ -55,7 +54,7 @@ const adaptAudioTrace = (trace: SimpleAudioTrace): Trace => {
} satisfies BaseTrace.SpeakTrace;
};

const adaptVisualTrace = (trace: SimpleVisualTrace): Trace => {
const adaptVisualTrace = (trace: SimpleVisualTrace): UnknownTrace => {
return {
...trace,
type: TraceType.VISUAL,
Expand Down Expand Up @@ -85,7 +84,7 @@ const adaptAction = (action: SimpleAction): BaseRequest.Action.BaseAction => {
};
};

const adaptButton = (button: SimpleButton): BaseRequest.ActionRequestButton => {
const adaptActionButton = (button: SimpleActionButton): BaseRequest.ActionRequestButton => {
return {
name: button.name,
request: {
Expand All @@ -98,15 +97,27 @@ const adaptButton = (button: SimpleButton): BaseRequest.ActionRequestButton => {
};
};

const adaptGeneralButton = (button: SimpleGeneralButton): BaseRequest.GeneralRequestButton => {
return {
name: button.name,
request: {
type: button.request.type,
payload: {
label: button.name,
},
},
};
};

const adaptCard = (card: SimpleCard): BaseNode.Carousel.TraceCarouselCard => {
return {
...card,
id: cuid.slug(),
buttons: (card.buttons ?? []).map((but) => adaptButton(but)),
buttons: (card.buttons ?? []).map((but) => adaptActionButton(but)),
};
};

const adaptCarouselTrace = (trace: SimpleCarouselTrace): Trace => {
const adaptCarouselTrace = (trace: SimpleCarouselTrace): UnknownTrace => {
return {
...trace,
type: TraceType.CAROUSEL,
Expand All @@ -118,18 +129,29 @@ const adaptCarouselTrace = (trace: SimpleCarouselTrace): Trace => {
} satisfies BaseTrace.CarouselTrace;
};

const adaptCardV2Trace = (trace: SimpleCardV2Trace): Trace => {
const adaptCardV2Trace = (trace: SimpleCardV2Trace): UnknownTrace => {
return {
...trace,
type: TraceType.CARD_V2,
payload: {
...trace.payload,
buttons: (trace.payload.buttons ?? []).map((but) => adaptButton(but)),
buttons: (trace.payload.buttons ?? []).map((but) => adaptActionButton(but)),
},
} satisfies BaseTrace.CardV2;
};

export function adaptTrace(trace: Trace): Trace {
const adaptChoiceTrace = (trace: SimpleChoiceTrace): UnknownTrace => {
return {
...trace,
type: TraceType.CHOICE,
payload: {
...trace.payload,
buttons: (trace.payload.buttons ?? []).map((but) => adaptGeneralButton(but)),
},
} satisfies BaseTrace.Choice;
};

export function adaptTrace(trace: UnknownTrace): UnknownTrace {
if (!isSimpleTrace(trace)) return trace;

switch (trace.type) {
Expand All @@ -145,6 +167,8 @@ export function adaptTrace(trace: Trace): Trace {
return adaptCarouselTrace(trace);
case SimpleTraceType.CardV2:
return adaptCardV2Trace(trace);
case SimpleTraceType.Choice:
return adaptChoiceTrace(trace);
default:
return trace;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { SimpleTrace, SimpleTraceDTO, SimpleTraceType, Trace } from '../../../runtime-command/trace-command.dto';
import { UnknownTrace } from '../../../runtime-command/trace/base.dto';
import { SimpleTraceType } from '../../../runtime-command/trace/simple-trace-type.enum';
import { SimpleTrace, SimpleTraceDTO } from '../../../runtime-command/trace-command.dto';

export const isSimpleTrace = (trace: Trace): trace is SimpleTrace => SimpleTraceDTO.safeParse(trace).success;
export const isSimpleTrace = (trace: UnknownTrace): trace is SimpleTrace => SimpleTraceDTO.safeParse(trace).success;

const simpleTraceTypes = new Set<string>(Object.values(SimpleTraceType));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { isNextPath, NextCommand } from '../../../runtime-command/next-command.dto';
import { NextCommand, NextPathDTO } from '../../../runtime-command/next-command.dto';
import { FunctionPathException } from '../exceptions/function-path.exception';

export function validateNext(next: NextCommand, expectedPathCodes: Array<string>) {
if (isNextPath(next) && !expectedPathCodes.includes(next.path)) {
throw new FunctionPathException(next.path, expectedPathCodes);
const parsedNextPath = NextPathDTO.safeParse(next);
if (parsedNextPath.success && !expectedPathCodes.includes(parsedNextPath.data.path)) {
throw new FunctionPathException(parsedNextPath.data.path, expectedPathCodes);
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { InvalidRuntimeCommandException } from '@/runtime/lib/HTTPClient/function-lambda/exceptions/invalid-runtime-command.exception';

import { SimpleTraceDTO, Trace } from '../../../runtime-command/trace-command.dto';
import { UnknownTrace } from '../../../runtime-command/trace/base.dto';
import { SimpleTraceDTO } from '../../../runtime-command/trace-command.dto';
import { isSimpleTraceType } from './is-simple-trace';

export function parseTrace(trace: Trace): Trace {
export function parseTrace(trace: UnknownTrace): UnknownTrace {
if (isSimpleTraceType(trace.type)) {
try {
return SimpleTraceDTO.parse(trace);
Expand Down