2
0

(setVariable) Add Transcription system var (#1507)

Closes #1484
This commit is contained in:
Baptiste Arnaud
2024-05-15 14:24:55 +02:00
committed by GitHub
parent ec7ff8d9ca
commit 40f21203b5
102 changed files with 2911 additions and 986 deletions

View File

@@ -1,12 +1,15 @@
import { executeCondition } from '@typebot.io/logic/executeCondition'
import { ChoiceInputBlock, Variable } from '@typebot.io/schemas'
import { executeCondition } from '../../logic/condition/executeCondition'
export const filterChoiceItems =
(variables: Variable[]) =>
(block: ChoiceInputBlock): ChoiceInputBlock => {
const filteredItems = block.items.filter((item) => {
if (item.displayCondition?.isEnabled && item.displayCondition?.condition)
return executeCondition(variables)(item.displayCondition.condition)
return executeCondition({
variables,
condition: item.displayCondition.condition,
})
return true
})

View File

@@ -41,7 +41,6 @@ const getVariableValue =
const [transformedVariable] = transformVariablesToList(variables)([
variable.id,
])
updateVariablesInSession(state)([transformedVariable])
return transformedVariable.value as string[]
}
return variable.value

View File

@@ -1,12 +1,15 @@
import { executeCondition } from '@typebot.io/logic/executeCondition'
import { PictureChoiceBlock, Variable } from '@typebot.io/schemas'
import { executeCondition } from '../../logic/condition/executeCondition'
export const filterPictureChoiceItems =
(variables: Variable[]) =>
(block: PictureChoiceBlock): PictureChoiceBlock => {
const filteredItems = block.items.filter((item) => {
if (item.displayCondition?.isEnabled && item.displayCondition?.condition)
return executeCondition(variables)(item.displayCondition.condition)
return executeCondition({
variables,
condition: item.displayCondition.condition,
})
return true
})

View File

@@ -25,6 +25,7 @@ export const executeGoogleSheetBlock = async (
})
case GoogleSheetsAction.GET:
return getRow(state, {
blockId: block.id,
options: block.options,
outgoingEdgeId: block.outgoingEdgeId,
})

View File

@@ -14,9 +14,14 @@ import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesI
export const getRow = async (
state: SessionState,
{
blockId,
outgoingEdgeId,
options,
}: { outgoingEdgeId?: string; options: GoogleSheetsGetOptions }
}: {
blockId: string
outgoingEdgeId?: string
options: GoogleSheetsGetOptions
}
): Promise<ExecuteIntegrationResponse> => {
const logs: ChatLog[] = []
const { variables } = state.typebotsQueue[0].typebot
@@ -79,10 +84,15 @@ export const getRow = async (
[]
)
if (!newVariables) return { outgoingEdgeId }
const newSessionState = updateVariablesInSession(state)(newVariables)
const { updatedState, newSetVariableHistory } = updateVariablesInSession({
state,
newVariables,
currentBlockId: blockId,
})
return {
outgoingEdgeId,
newSessionState,
newSessionState: updatedState,
newSetVariableHistory,
}
} catch (err) {
logs.push({

View File

@@ -107,12 +107,16 @@ export const createSpeechOpenAI = async (
mimeType: 'audio/mpeg',
})
newSessionState = updateVariablesInSession(newSessionState)([
{
...saveUrlInVariable,
value: url,
},
])
newSessionState = updateVariablesInSession({
newVariables: [
{
...saveUrlInVariable,
value: url,
},
],
state: newSessionState,
currentBlockId: undefined,
}).updatedState
return {
startTimeShouldBeUpdated: true,

View File

@@ -22,7 +22,6 @@ import {
defaultOpenAIOptions,
} from '@typebot.io/schemas/features/blocks/integrations/openai/constants'
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
import { isPlaneteScale } from '@typebot.io/lib/isPlanetScale'
export const createChatCompletionOpenAI = async (
state: SessionState,
@@ -68,9 +67,11 @@ export const createChatCompletionOpenAI = async (
typebot.variables
)(options.messages)
if (variablesTransformedToList.length > 0)
newSessionState = updateVariablesInSession(state)(
variablesTransformedToList
)
newSessionState = updateVariablesInSession({
state,
newVariables: variablesTransformedToList,
currentBlockId: undefined,
}).updatedState
const temperature = parseVariableNumber(typebot.variables)(
options.advancedSettings?.temperature

View File

@@ -42,7 +42,11 @@ export const resumeChatCompletion =
return newVariables
}, [])
if (newVariables && newVariables.length > 0)
newSessionState = updateVariablesInSession(newSessionState)(newVariables)
newSessionState = updateVariablesInSession({
newVariables,
state: newSessionState,
currentBlockId: undefined,
}).updatedState
return {
outgoingEdgeId,
newSessionState,

View File

@@ -70,10 +70,15 @@ export const resumeWebhookExecution = ({
}
}, [])
if (newVariables && newVariables.length > 0) {
const newSessionState = updateVariablesInSession(state)(newVariables)
const { updatedState, newSetVariableHistory } = updateVariablesInSession({
newVariables,
state,
currentBlockId: block.id,
})
return {
outgoingEdgeId: block.outgoingEdgeId,
newSessionState,
newSessionState: updatedState,
newSetVariableHistory,
logs,
}
}

View File

@@ -19,6 +19,7 @@ export const executeZemanticAiBlock = async (
block: ZemanticAiBlock
): Promise<ExecuteIntegrationResponse> => {
let newSessionState = state
let setVariableHistory = []
if (!block.options?.credentialsId)
return {
@@ -82,24 +83,34 @@ export const executeZemanticAiBlock = async (
for (const r of block.options.responseMapping || []) {
const variable = typebot.variables.find(byId(r.variableId))
let newVariables = []
switch (r.valueToExtract) {
case 'Summary':
if (isDefined(variable) && !isEmpty(res.summary)) {
newSessionState = updateVariablesInSession(newSessionState)([
{ ...variable, value: res.summary },
])
newVariables.push({ ...variable, value: res.summary })
}
break
case 'Results':
if (isDefined(variable) && res.results.length) {
newSessionState = updateVariablesInSession(newSessionState)([
{ ...variable, value: JSON.stringify(res.results) },
])
newVariables.push({
...variable,
value: JSON.stringify(res.results),
})
}
break
default:
break
}
if (newVariables.length > 0) {
const { newSetVariableHistory, updatedState } =
updateVariablesInSession({
newVariables,
state: newSessionState,
currentBlockId: block.id,
})
newSessionState = updatedState
setVariableHistory.push(...newSetVariableHistory)
}
}
} catch (e) {
console.error(e)
@@ -112,6 +123,7 @@ export const executeZemanticAiBlock = async (
description: 'Could not execute Zemantic AI request',
},
],
newSetVariableHistory: setVariableHistory,
}
}

View File

@@ -1,201 +0,0 @@
import { isNotDefined, isDefined } from '@typebot.io/lib'
import { Comparison, Condition, Variable } from '@typebot.io/schemas'
import { findUniqueVariableValue } from '@typebot.io/variables/findUniqueVariableValue'
import { parseVariables } from '@typebot.io/variables/parseVariables'
import {
LogicalOperator,
ComparisonOperators,
defaultConditionItemContent,
} from '@typebot.io/schemas/features/blocks/logic/condition/constants'
export const executeCondition =
(variables: Variable[]) =>
(condition: Condition): boolean => {
if (!condition.comparisons) return false
return (condition.logicalOperator ??
defaultConditionItemContent.logicalOperator) === LogicalOperator.AND
? condition.comparisons.every(executeComparison(variables))
: condition.comparisons.some(executeComparison(variables))
}
const executeComparison =
(variables: Variable[]) =>
(comparison: Comparison): boolean => {
if (!comparison?.variableId) return false
const inputValue =
variables.find((v) => v.id === comparison.variableId)?.value ?? null
const value =
comparison.value === 'undefined' || comparison.value === 'null'
? null
: findUniqueVariableValue(variables)(comparison.value) ??
parseVariables(variables)(comparison.value)
if (isNotDefined(comparison.comparisonOperator)) return false
switch (comparison.comparisonOperator) {
case ComparisonOperators.CONTAINS: {
if (Array.isArray(inputValue)) {
const equal = (a: string | null, b: string | null) => {
if (typeof a === 'string' && typeof b === 'string')
return a.normalize() === b.normalize()
return a !== b
}
return compare(equal, inputValue, value, 'some')
}
const contains = (a: string | null, b: string | null) => {
if (b === '' || !b || !a) return false
return a
.toLowerCase()
.trim()
.normalize()
.includes(b.toLowerCase().trim().normalize())
}
return compare(contains, inputValue, value, 'some')
}
case ComparisonOperators.NOT_CONTAINS: {
if (Array.isArray(inputValue)) {
const notEqual = (a: string | null, b: string | null) => {
if (typeof a === 'string' && typeof b === 'string')
return a.normalize() !== b.normalize()
return a !== b
}
return compare(notEqual, inputValue, value)
}
const notContains = (a: string | null, b: string | null) => {
if (b === '' || !b || !a) return true
return !a
.toLowerCase()
.trim()
.normalize()
.includes(b.toLowerCase().trim().normalize())
}
return compare(notContains, inputValue, value)
}
case ComparisonOperators.EQUAL: {
return compare(
(a, b) => {
if (typeof a === 'string' && typeof b === 'string')
return a.normalize() === b.normalize()
return a === b
},
inputValue,
value
)
}
case ComparisonOperators.NOT_EQUAL: {
return compare(
(a, b) => {
if (typeof a === 'string' && typeof b === 'string')
return a.normalize() !== b.normalize()
return a !== b
},
inputValue,
value
)
}
case ComparisonOperators.GREATER: {
if (isNotDefined(inputValue) || isNotDefined(value)) return false
if (typeof inputValue === 'string') {
if (typeof value === 'string')
return parseDateOrNumber(inputValue) > parseDateOrNumber(value)
return Number(inputValue) > value.length
}
if (typeof value === 'string') return inputValue.length > Number(value)
return inputValue.length > value.length
}
case ComparisonOperators.LESS: {
if (isNotDefined(inputValue) || isNotDefined(value)) return false
if (typeof inputValue === 'string') {
if (typeof value === 'string')
return parseDateOrNumber(inputValue) < parseDateOrNumber(value)
return Number(inputValue) < value.length
}
if (typeof value === 'string') return inputValue.length < Number(value)
return inputValue.length < value.length
}
case ComparisonOperators.IS_SET: {
return isDefined(inputValue) && inputValue.length > 0
}
case ComparisonOperators.IS_EMPTY: {
return isNotDefined(inputValue) || inputValue.length === 0
}
case ComparisonOperators.STARTS_WITH: {
const startsWith = (a: string | null, b: string | null) => {
if (b === '' || !b || !a) return false
return a
.toLowerCase()
.trim()
.normalize()
.startsWith(b.toLowerCase().trim().normalize())
}
return compare(startsWith, inputValue, value)
}
case ComparisonOperators.ENDS_WITH: {
const endsWith = (a: string | null, b: string | null) => {
if (b === '' || !b || !a) return false
return a
.toLowerCase()
.trim()
.normalize()
.endsWith(b.toLowerCase().trim().normalize())
}
return compare(endsWith, inputValue, value)
}
case ComparisonOperators.MATCHES_REGEX: {
const matchesRegex = (a: string | null, b: string | null) => {
if (b === '' || !b || !a) return false
const regex = preprocessRegex(b)
if (!regex) return false
return new RegExp(regex.pattern, regex.flags).test(a)
}
return compare(matchesRegex, inputValue, value, 'some')
}
case ComparisonOperators.NOT_MATCH_REGEX: {
const matchesRegex = (a: string | null, b: string | null) => {
if (b === '' || !b || !a) return false
const regex = preprocessRegex(b)
if (!regex) return true
return !new RegExp(regex.pattern, regex.flags).test(a)
}
return compare(matchesRegex, inputValue, value)
}
}
}
const compare = (
compareStrings: (a: string | null, b: string | null) => boolean,
a: Exclude<Variable['value'], undefined>,
b: Exclude<Variable['value'], undefined>,
type: 'every' | 'some' = 'every'
): boolean => {
if (!a || typeof a === 'string') {
if (!b || typeof b === 'string') return compareStrings(a, b)
return type === 'every'
? b.every((b) => compareStrings(a, b))
: b.some((b) => compareStrings(a, b))
}
if (!b || typeof b === 'string') {
return type === 'every'
? a.every((a) => compareStrings(a, b))
: a.some((a) => compareStrings(a, b))
}
if (type === 'every')
return a.every((a) => b.every((b) => compareStrings(a, b)))
return a.some((a) => b.some((b) => compareStrings(a, b)))
}
const parseDateOrNumber = (value: string): number => {
const parsed = Number(value)
if (isNaN(parsed)) {
const time = Date.parse(value)
return time
}
return parsed
}
const preprocessRegex = (regex: string) => {
const regexWithFlags = regex.match(/\/(.+)\/([gimuy]*)$/)
if (regexWithFlags)
return { pattern: regexWithFlags[1], flags: regexWithFlags[2] }
return { pattern: regex }
}

View File

@@ -1,14 +1,14 @@
import { ConditionBlock, SessionState } from '@typebot.io/schemas'
import { ExecuteLogicResponse } from '../../../types'
import { executeCondition } from './executeCondition'
import { executeCondition } from '@typebot.io/logic/executeCondition'
export const executeConditionBlock = (
state: SessionState,
block: ConditionBlock
): ExecuteLogicResponse => {
const { variables } = state.typebotsQueue[0].typebot
const passedCondition = block.items.find(
(item) => item.content && executeCondition(variables)(item.content)
(item) =>
item.content && executeCondition({ variables, condition: item.content })
)
return {
outgoingEdgeId: passedCondition

View File

@@ -24,14 +24,25 @@ export const executeScript = async (
body: block.options.content,
})
const newSessionState = newVariables
? updateVariablesInSession(state)(newVariables)
: state
const updateVarResults = newVariables
? updateVariablesInSession({
newVariables,
state,
currentBlockId: block.id,
})
: undefined
let newSessionState = state
if (updateVarResults) {
newSessionState = updateVarResults.updatedState
}
return {
outgoingEdgeId: block.outgoingEdgeId,
logs: error ? [{ status: 'error', description: error }] : [],
newSessionState,
newSetVariableHistory: updateVarResults?.newSetVariableHistory,
}
}

View File

@@ -1,4 +1,10 @@
import { SessionState, SetVariableBlock, Variable } from '@typebot.io/schemas'
import {
Answer,
SessionState,
SetVariableBlock,
SetVariableHistoryItem,
Variable,
} from '@typebot.io/schemas'
import { byId, isEmpty } from '@typebot.io/lib'
import { ExecuteLogicResponse } from '../../../types'
import { parseScriptToExecuteClientSideAction } from '../script/executeScript'
@@ -7,18 +13,27 @@ import { parseVariables } from '@typebot.io/variables/parseVariables'
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
import { createId } from '@paralleldrive/cuid2'
import { utcToZonedTime, format as tzFormat } from 'date-fns-tz'
import {
computeResultTranscript,
parseTranscriptMessageText,
} from '@typebot.io/logic/computeResultTranscript'
import prisma from '@typebot.io/lib/prisma'
import { sessionOnlySetVariableOptions } from '@typebot.io/schemas/features/blocks/logic/setVariable/constants'
import vm from 'vm'
export const executeSetVariable = (
export const executeSetVariable = async (
state: SessionState,
block: SetVariableBlock
): ExecuteLogicResponse => {
): Promise<ExecuteLogicResponse> => {
const { variables } = state.typebotsQueue[0].typebot
if (!block.options?.variableId)
return {
outgoingEdgeId: block.outgoingEdgeId,
}
const expressionToEvaluate = getExpressionToEvaluate(state)(block.options)
const expressionToEvaluate = await getExpressionToEvaluate(state)(
block.options,
block.id
)
const isCustomValue = !block.options.type || block.options.type === 'Custom'
if (
expressionToEvaluate &&
@@ -52,10 +67,25 @@ export const executeSetVariable = (
...existingVariable,
value: evaluatedExpression,
}
const newSessionState = updateVariablesInSession(state)([newVariable])
const { newSetVariableHistory, updatedState } = updateVariablesInSession({
state,
newVariables: [
{
...newVariable,
isSessionVariable: sessionOnlySetVariableOptions.includes(
block.options.type as (typeof sessionOnlySetVariableOptions)[number]
)
? true
: newVariable.isSessionVariable,
},
],
currentBlockId: block.id,
})
return {
outgoingEdgeId: block.outgoingEdgeId,
newSessionState,
newSessionState: updatedState,
newSetVariableHistory,
}
}
@@ -85,7 +115,10 @@ const evaluateSetVariableExpression =
const getExpressionToEvaluate =
(state: SessionState) =>
(options: SetVariableBlock['options']): string | null => {
async (
options: SetVariableBlock['options'],
blockId: string
): Promise<string | null> => {
switch (options?.type) {
case 'Contact name':
return state.whatsApp?.contact.name ?? null
@@ -149,6 +182,34 @@ const getExpressionToEvaluate =
case 'Environment name': {
return state.whatsApp ? 'whatsapp' : 'web'
}
case 'Transcript': {
const props = await parseTranscriptProps(state)
if (!props) return ''
const typebotWithEmptyVariables = {
...state.typebotsQueue[0].typebot,
variables: state.typebotsQueue[0].typebot.variables.map((v) => ({
...v,
value: undefined,
})),
}
const transcript = computeResultTranscript({
typebot: typebotWithEmptyVariables,
stopAtBlockId: blockId,
...props,
})
return (
'return `' +
transcript
.map(
(message) =>
`${
message.role === 'bot' ? 'Assistant:' : 'User:'
} "${parseTranscriptMessageText(message)}"`
)
.join('\n\n') +
'`'
)
}
case 'Custom':
case undefined: {
return options?.expressionToEvaluate ?? null
@@ -160,3 +221,79 @@ const toISOWithTz = (date: Date, timeZone: string) => {
const zonedDate = utcToZonedTime(date, timeZone)
return tzFormat(zonedDate, "yyyy-MM-dd'T'HH:mm:ssXXX", { timeZone })
}
type ParsedTranscriptProps = {
answers: Pick<Answer, 'blockId' | 'content'>[]
setVariableHistory: Pick<
SetVariableHistoryItem,
'blockId' | 'variableId' | 'value'
>[]
visitedEdges: string[]
}
const parseTranscriptProps = async (
state: SessionState
): Promise<ParsedTranscriptProps | undefined> => {
if (!state.typebotsQueue[0].resultId)
return parsePreviewTranscriptProps(state)
return parseResultTranscriptProps(state)
}
const parsePreviewTranscriptProps = async (
state: SessionState
): Promise<ParsedTranscriptProps | undefined> => {
if (!state.previewMetadata) return
return {
answers: state.previewMetadata.answers ?? [],
setVariableHistory: state.previewMetadata.setVariableHistory ?? [],
visitedEdges: state.previewMetadata.visitedEdges ?? [],
}
}
const parseResultTranscriptProps = async (
state: SessionState
): Promise<ParsedTranscriptProps | undefined> => {
const result = await prisma.result.findUnique({
where: {
id: state.typebotsQueue[0].resultId,
},
select: {
edges: {
select: {
edgeId: true,
index: true,
},
},
answers: {
select: {
blockId: true,
content: true,
},
},
answersV2: {
select: {
blockId: true,
content: true,
},
},
setVariableHistory: {
select: {
blockId: true,
variableId: true,
index: true,
value: true,
},
},
},
})
if (!result) return
return {
answers: result.answersV2.concat(result.answers),
setVariableHistory: (
result.setVariableHistory as SetVariableHistoryItem[]
).sort((a, b) => a.index - b.index),
visitedEdges: result.edges
.sort((a, b) => a.index - b.index)
.map((edge) => edge.edgeId),
}
}