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

@ -20,7 +20,7 @@ export const addEdgeToTypebot = (
})
export const createPortalEdge = ({ to }: Pick<Edge, 'to'>) => ({
id: createId(),
id: 'virtual-' + createId(),
from: { blockId: '', groupId: '' },
to,
})

View File

@ -52,6 +52,7 @@ export const continueChat = async ({ origin, sessionId, message }: Props) => {
logs,
lastMessageNewFormat,
visitedEdges,
setVariableHistory,
} = await continueBotFlow(message, {
version: 2,
state: session.state,
@ -68,6 +69,7 @@ export const continueChat = async ({ origin, sessionId, message }: Props) => {
logs,
clientSideActions,
visitedEdges,
setVariableHistory,
hasCustomEmbedBubble: messages.some(
(message) => message.type === 'custom-embed'
),

View File

@ -16,6 +16,7 @@ import { isForgedBlockType } from '@typebot.io/schemas/features/blocks/forged/he
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
import { updateSession } from '../queries/updateSession'
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
import { saveSetVariableHistoryItems } from '../queries/saveSetVariableHistoryItems'
type Props = {
sessionId: string
@ -114,11 +115,17 @@ export const getMessageStream = async ({ sessionId, messages }: Props) => {
(variable) => variable.id === id
)
if (!variable) return
const { updatedState, newSetVariableHistory } =
updateVariablesInSession({
newVariables: [{ ...variable, value }],
state: session.state,
currentBlockId: session.state.currentBlockId,
})
if (newSetVariableHistory.length > 0)
await saveSetVariableHistoryItems(newSetVariableHistory)
await updateSession({
id: session.id,
state: updateVariablesInSession(session.state)([
{ ...variable, value },
]),
state: updatedState,
isReplying: undefined,
})
},

View File

@ -33,6 +33,7 @@ export const startChat = async ({
clientSideActions,
newSessionState,
visitedEdges,
setVariableHistory,
} = await startSession({
version: 2,
startParams: {
@ -69,6 +70,7 @@ export const startChat = async ({
logs,
clientSideActions,
visitedEdges,
setVariableHistory,
hasCustomEmbedBubble: messages.some(
(message) => message.type === 'custom-embed'
),

View File

@ -13,6 +13,7 @@ type Props = {
typebot?: StartTypebot
userId?: string
prefilledVariables?: Record<string, unknown>
sessionId?: string
}
export const startChatPreview = async ({
@ -24,6 +25,7 @@ export const startChatPreview = async ({
typebot: startTypebot,
userId,
prefilledVariables,
sessionId,
}: Props) => {
const {
typebot,
@ -34,6 +36,7 @@ export const startChatPreview = async ({
clientSideActions,
newSessionState,
visitedEdges,
setVariableHistory,
} = await startSession({
version: 2,
startParams: {
@ -45,6 +48,7 @@ export const startChatPreview = async ({
typebot: startTypebot,
userId,
prefilledVariables,
sessionId,
},
message,
})
@ -61,9 +65,11 @@ export const startChatPreview = async ({
logs,
clientSideActions,
visitedEdges,
setVariableHistory,
hasCustomEmbedBubble: messages.some(
(message) => message.type === 'custom-embed'
),
initialSessionId: sessionId,
})
const isEnded =

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),
}
}

View File

@ -5,6 +5,7 @@ import {
Group,
InputBlock,
SessionState,
SetVariableHistoryItem,
} from '@typebot.io/schemas'
import { byId } from '@typebot.io/lib'
import { isInputBlock } from '@typebot.io/schemas/helpers'
@ -13,7 +14,7 @@ import { getNextGroup } from './getNextGroup'
import { validateEmail } from './blocks/inputs/email/validateEmail'
import { formatPhoneNumber } from './blocks/inputs/phone/formatPhoneNumber'
import { resumeWebhookExecution } from './blocks/integrations/webhook/resumeWebhookExecution'
import { upsertAnswer } from './queries/upsertAnswer'
import { saveAnswer } from './queries/saveAnswer'
import { parseButtonsReply } from './blocks/inputs/buttons/parseButtonsReply'
import { ParsedReply, Reply } from './types'
import { validateNumber } from './blocks/inputs/number/validateNumber'
@ -57,11 +58,13 @@ export const continueBotFlow = async (
ContinueChatResponse & {
newSessionState: SessionState
visitedEdges: VisitedEdge[]
setVariableHistory: SetVariableHistoryItem[]
}
> => {
let firstBubbleWasStreamed = false
let newSessionState = { ...state }
const visitedEdges: VisitedEdge[] = []
const setVariableHistory: SetVariableHistoryItem[] = []
if (!newSessionState.currentBlockId) return startBotFlow({ state, version })
@ -76,16 +79,17 @@ export const continueBotFlow = async (
message: 'Group / block not found',
})
let variableToUpdate
if (block.type === LogicBlockType.SET_VARIABLE) {
const existingVariable = state.typebotsQueue[0].typebot.variables.find(
byId(block.options?.variableId)
)
if (existingVariable && reply && typeof reply === 'string') {
const newVariable = {
variableToUpdate = {
...existingVariable,
value: safeJsonParse(reply),
}
newSessionState = updateVariablesInSession(state)([newVariable])
}
}
// Legacy
@ -121,42 +125,41 @@ export const continueBotFlow = async (
if (action) {
if (action.run?.stream?.getStreamVariableId) {
firstBubbleWasStreamed = true
const variableToUpdate =
state.typebotsQueue[0].typebot.variables.find(
(v) => v.id === action?.run?.stream?.getStreamVariableId(options)
)
if (variableToUpdate)
newSessionState = updateVariablesInSession(state)([
{
...variableToUpdate,
value: reply,
},
])
variableToUpdate = state.typebotsQueue[0].typebot.variables.find(
(v) => v.id === action?.run?.stream?.getStreamVariableId(options)
)
}
if (
action.run?.web?.displayEmbedBubble?.waitForEvent?.getSaveVariableId
) {
const variableToUpdate =
state.typebotsQueue[0].typebot.variables.find(
(v) =>
v.id ===
action?.run?.web?.displayEmbedBubble?.waitForEvent?.getSaveVariableId?.(
options
)
)
if (variableToUpdate)
newSessionState = updateVariablesInSession(state)([
{
...variableToUpdate,
value: reply,
},
])
variableToUpdate = state.typebotsQueue[0].typebot.variables.find(
(v) =>
v.id ===
action?.run?.web?.displayEmbedBubble?.waitForEvent?.getSaveVariableId?.(
options
)
)
}
}
}
}
if (variableToUpdate) {
const { newSetVariableHistory, updatedState } = updateVariablesInSession({
state: newSessionState,
currentBlockId: block.id,
newVariables: [
{
...variableToUpdate,
value: reply,
},
],
})
newSessionState = updatedState
setVariableHistory.push(...newSetVariableHistory)
}
let formattedReply: string | undefined
if (isInputBlock(block)) {
@ -167,6 +170,7 @@ export const continueBotFlow = async (
...(await parseRetryMessage(newSessionState)(block)),
newSessionState,
visitedEdges: [],
setVariableHistory: [],
}
formattedReply =
@ -176,7 +180,9 @@ export const continueBotFlow = async (
const groupHasMoreBlocks = blockIndex < group.blocks.length - 1
const nextEdgeId = getOutgoingEdgeId(newSessionState)(block, formattedReply)
const { edgeId: nextEdgeId, isOffDefaultPath } = getOutgoingEdgeId(
newSessionState
)(block, formattedReply)
if (groupHasMoreBlocks && !nextEdgeId) {
const chatReply = await executeGroup(
@ -188,6 +194,7 @@ export const continueBotFlow = async (
version,
state: newSessionState,
visitedEdges,
setVariableHistory,
firstBubbleWasStreamed,
startTime,
}
@ -206,9 +213,14 @@ export const continueBotFlow = async (
lastMessageNewFormat:
formattedReply !== reply ? formattedReply : undefined,
visitedEdges,
setVariableHistory,
}
const nextGroup = await getNextGroup(newSessionState)(nextEdgeId)
const nextGroup = await getNextGroup({
state: newSessionState,
edgeId: nextEdgeId,
isOffDefaultPath,
})
if (nextGroup.visitedEdge) visitedEdges.push(nextGroup.visitedEdge)
@ -221,6 +233,7 @@ export const continueBotFlow = async (
lastMessageNewFormat:
formattedReply !== reply ? formattedReply : undefined,
visitedEdges,
setVariableHistory,
}
const chatReply = await executeGroup(nextGroup.group, {
@ -228,6 +241,7 @@ export const continueBotFlow = async (
state: newSessionState,
firstBubbleWasStreamed,
visitedEdges,
setVariableHistory,
startTime,
})
@ -241,8 +255,7 @@ const processAndSaveAnswer =
(state: SessionState, block: InputBlock) =>
async (reply: string | undefined): Promise<SessionState> => {
if (!reply) return state
let newState = await saveAnswer(state, block)(reply)
newState = saveVariableValueIfAny(newState, block)(reply)
let newState = await saveAnswerInDb(state, block)(reply)
return newState
}
@ -255,16 +268,20 @@ const saveVariableValueIfAny =
)
if (!foundVariable) return state
const newSessionState = updateVariablesInSession(state)([
{
...foundVariable,
value: Array.isArray(foundVariable.value)
? foundVariable.value.concat(reply)
: reply,
},
])
const { updatedState } = updateVariablesInSession({
newVariables: [
{
...foundVariable,
value: Array.isArray(foundVariable.value)
? foundVariable.value.concat(reply)
: reply,
},
],
currentBlockId: undefined,
state,
})
return newSessionState
return updatedState
}
const parseRetryMessage =
@ -305,31 +322,43 @@ const parseDefaultRetryMessage = (block: InputBlock): string => {
}
}
const saveAnswer =
const saveAnswerInDb =
(state: SessionState, block: InputBlock) =>
async (reply: string): Promise<SessionState> => {
let newSessionState = state
const groupId = state.typebotsQueue[0].typebot.groups.find((group) =>
group.blocks.some((blockInGroup) => blockInGroup.id === block.id)
)?.id
if (!groupId) throw new Error('saveAnswer: Group not found')
await upsertAnswer({
await saveAnswer({
answer: {
blockId: block.id,
groupId,
content: reply,
variableId: block.options?.variableId,
},
reply,
state,
})
newSessionState = {
...saveVariableValueIfAny(newSessionState, block)(reply),
previewMetadata: state.typebotsQueue[0].resultId
? newSessionState.previewMetadata
: {
...newSessionState.previewMetadata,
answers: (newSessionState.previewMetadata?.answers ?? []).concat({
blockId: block.id,
content: reply,
}),
},
}
const key = block.options?.variableId
? state.typebotsQueue[0].typebot.variables.find(
? newSessionState.typebotsQueue[0].typebot.variables.find(
(variable) => variable.id === block.options?.variableId
)?.name
: parseGroupKey(block.id, { state })
: parseGroupKey(block.id, { state: newSessionState })
return setNewAnswerInState(state)({
return setNewAnswerInState(newSessionState)({
key: key ?? block.id,
value: reply,
})
@ -375,7 +404,10 @@ const setNewAnswerInState =
const getOutgoingEdgeId =
(state: Pick<SessionState, 'typebotsQueue'>) =>
(block: Block, reply: string | undefined) => {
(
block: Block,
reply: string | undefined
): { edgeId: string | undefined; isOffDefaultPath: boolean } => {
const variables = state.typebotsQueue[0].typebot.variables
if (
block.type === InputBlockType.CHOICE &&
@ -390,7 +422,8 @@ const getOutgoingEdgeId =
parseVariables(variables)(item.content).normalize() ===
reply.normalize()
)
if (matchedItem?.outgoingEdgeId) return matchedItem.outgoingEdgeId
if (matchedItem?.outgoingEdgeId)
return { edgeId: matchedItem.outgoingEdgeId, isOffDefaultPath: true }
}
if (
block.type === InputBlockType.PICTURE_CHOICE &&
@ -405,9 +438,10 @@ const getOutgoingEdgeId =
parseVariables(variables)(item.title).normalize() ===
reply.normalize()
)
if (matchedItem?.outgoingEdgeId) return matchedItem.outgoingEdgeId
if (matchedItem?.outgoingEdgeId)
return { edgeId: matchedItem.outgoingEdgeId, isOffDefaultPath: true }
}
return block.outgoingEdgeId
return { edgeId: block.outgoingEdgeId, isOffDefaultPath: false }
}
const parseReply =

View File

@ -4,6 +4,7 @@ import {
InputBlock,
RuntimeOptions,
SessionState,
SetVariableHistoryItem,
} from '@typebot.io/schemas'
import { isNotEmpty } from '@typebot.io/lib'
import {
@ -21,16 +22,16 @@ import { injectVariableValuesInPictureChoiceBlock } from './blocks/inputs/pictur
import { getPrefilledInputValue } from './getPrefilledValue'
import { parseDateInput } from './blocks/inputs/date/parseDateInput'
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
import {
BubbleBlockWithDefinedContent,
parseBubbleBlock,
} from './parseBubbleBlock'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { VisitedEdge } from '@typebot.io/prisma'
import { env } from '@typebot.io/env'
import { TRPCError } from '@trpc/server'
import { ExecuteIntegrationResponse, ExecuteLogicResponse } from './types'
import { createId } from '@paralleldrive/cuid2'
import {
BubbleBlockWithDefinedContent,
parseBubbleBlock,
} from './parseBubbleBlock'
type ContextProps = {
version: 1 | 2
@ -39,6 +40,7 @@ type ContextProps = {
currentLastBubbleId?: string
firstBubbleWasStreamed?: boolean
visitedEdges: VisitedEdge[]
setVariableHistory: SetVariableHistoryItem[]
startTime?: number
}
@ -48,6 +50,7 @@ export const executeGroup = async (
version,
state,
visitedEdges,
setVariableHistory,
currentReply,
currentLastBubbleId,
firstBubbleWasStreamed,
@ -56,6 +59,7 @@ export const executeGroup = async (
): Promise<
ContinueChatResponse & {
newSessionState: SessionState
setVariableHistory: SetVariableHistoryItem[]
visitedEdges: VisitedEdge[]
}
> => {
@ -70,6 +74,7 @@ export const executeGroup = async (
let newSessionState = state
let isNextEdgeOffDefaultPath = false
let index = -1
for (const block of group.blocks) {
if (
@ -110,6 +115,7 @@ export const executeGroup = async (
clientSideActions,
logs,
visitedEdges,
setVariableHistory,
}
const executionResponse = (
isLogicBlock(block)
@ -120,6 +126,29 @@ export const executeGroup = async (
) as ExecuteLogicResponse | ExecuteIntegrationResponse | null
if (!executionResponse) continue
if (
executionResponse.newSetVariableHistory &&
executionResponse.newSetVariableHistory?.length > 0
) {
if (!newSessionState.typebotsQueue[0].resultId)
newSessionState = {
...newSessionState,
previewMetadata: {
...newSessionState.previewMetadata,
setVariableHistory: (
newSessionState.previewMetadata?.setVariableHistory ?? []
).concat(
executionResponse.newSetVariableHistory.map((item) => ({
blockId: item.blockId,
variableId: item.variableId,
value: item.value,
}))
),
},
}
else setVariableHistory.push(...executionResponse.newSetVariableHistory)
}
if (
'startTimeShouldBeUpdated' in executionResponse &&
executionResponse.startTimeShouldBeUpdated
@ -165,33 +194,55 @@ export const executeGroup = async (
clientSideActions,
logs,
visitedEdges,
setVariableHistory,
}
}
}
if (executionResponse.outgoingEdgeId) {
isNextEdgeOffDefaultPath =
block.outgoingEdgeId !== executionResponse.outgoingEdgeId
nextEdgeId = executionResponse.outgoingEdgeId
break
}
}
if (!nextEdgeId && newSessionState.typebotsQueue.length === 1)
return { messages, newSessionState, clientSideActions, logs, visitedEdges }
return {
messages,
newSessionState,
clientSideActions,
logs,
visitedEdges,
setVariableHistory,
}
const nextGroup = await getNextGroup(newSessionState)(nextEdgeId ?? undefined)
const nextGroup = await getNextGroup({
state: newSessionState,
edgeId: nextEdgeId ?? undefined,
isOffDefaultPath: isNextEdgeOffDefaultPath,
})
newSessionState = nextGroup.newSessionState
if (nextGroup.visitedEdge) visitedEdges.push(nextGroup.visitedEdge)
if (!nextGroup.group) {
return { messages, newSessionState, clientSideActions, logs, visitedEdges }
return {
messages,
newSessionState,
clientSideActions,
logs,
visitedEdges,
setVariableHistory,
}
}
return executeGroup(nextGroup.group, {
version,
state: newSessionState,
visitedEdges,
setVariableHistory,
currentReply: {
messages,
clientSideActions,

View File

@ -2,12 +2,12 @@ import { VariableStore, LogsStore } from '@typebot.io/forge'
import { forgedBlocks } from '@typebot.io/forge-repository/definitions'
import { ForgedBlock } from '@typebot.io/forge-repository/types'
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
import { isPlaneteScale } from '@typebot.io/lib/isPlanetScale'
import {
SessionState,
ContinueChatResponse,
Block,
TypebotInSession,
SetVariableHistoryItem,
} from '@typebot.io/schemas'
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
import {
@ -73,6 +73,7 @@ export const executeForgedBlock = async (
}
let newSessionState = state
let setVariableHistory: SetVariableHistoryItem[] = []
const variables: VariableStore = {
get: (id: string) => {
@ -86,9 +87,13 @@ export const executeForgedBlock = async (
(variable) => variable.id === id
)
if (!variable) return
newSessionState = updateVariablesInSession(newSessionState)([
{ ...variable, value },
])
const { newSetVariableHistory, updatedState } = updateVariablesInSession({
newVariables: [{ ...variable, value }],
state: newSessionState,
currentBlockId: block.id,
})
newSessionState = updatedState
setVariableHistory.push(...newSetVariableHistory)
},
parse: (text: string, params?: ParseVariablesOptions) =>
parseVariables(
@ -159,6 +164,7 @@ export const executeForgedBlock = async (
},
}
: undefined,
newSetVariableHistory: setVariableHistory,
}
}

View File

@ -1,14 +1,13 @@
import { TRPCError } from '@trpc/server'
import { SessionState } from '@typebot.io/schemas'
import { TypebotInSession } from '@typebot.io/schemas'
export const getFirstEdgeId = ({
state,
typebot,
startEventId,
}: {
state: SessionState
typebot: Pick<TypebotInSession, 'events' | 'groups' | 'version'>
startEventId: string | undefined
}) => {
const { typebot } = state.typebotsQueue[0]
if (startEventId) {
const event = typebot.events?.find((e) => e.id === startEventId)
if (!event)
@ -18,6 +17,6 @@ export const getFirstEdgeId = ({
})
return event.outgoingEdgeId
}
if (typebot.version === '6') return typebot.events[0].outgoingEdgeId
if (typebot.version === '6') return typebot.events?.[0].outgoingEdgeId
return typebot.groups.at(0)?.blocks.at(0)?.outgoingEdgeId
}

View File

@ -9,116 +9,138 @@ export type NextGroup = {
visitedEdge?: VisitedEdge
}
export const getNextGroup =
(state: SessionState) =>
async (edgeId?: string): Promise<NextGroup> => {
const nextEdge = state.typebotsQueue[0].typebot.edges.find(byId(edgeId))
if (!nextEdge) {
if (state.typebotsQueue.length > 1) {
const nextEdgeId = state.typebotsQueue[0].edgeIdToTriggerWhenDone
const isMergingWithParent = state.typebotsQueue[0].isMergingWithParent
const currentResultId = state.typebotsQueue[0].resultId
if (!isMergingWithParent && currentResultId)
await upsertResult({
resultId: currentResultId,
typebot: state.typebotsQueue[0].typebot,
isCompleted: true,
hasStarted: state.typebotsQueue[0].answers.length > 0,
})
let newSessionState = {
...state,
typebotsQueue: [
{
...state.typebotsQueue[1],
typebot: isMergingWithParent
? {
...state.typebotsQueue[1].typebot,
variables: state.typebotsQueue[1].typebot.variables
.map((variable) => ({
...variable,
value:
state.typebotsQueue[0].typebot.variables.find(
(v) => v.name === variable.name
)?.value ?? variable.value,
}))
.concat(
state.typebotsQueue[0].typebot.variables.filter(
(variable) =>
isDefined(variable.value) &&
isNotDefined(
state.typebotsQueue[1].typebot.variables.find(
(v) => v.name === variable.name
)
export const getNextGroup = async ({
state,
edgeId,
isOffDefaultPath,
}: {
state: SessionState
edgeId?: string
isOffDefaultPath: boolean
}): Promise<NextGroup> => {
const nextEdge = state.typebotsQueue[0].typebot.edges.find(byId(edgeId))
if (!nextEdge) {
if (state.typebotsQueue.length > 1) {
const nextEdgeId = state.typebotsQueue[0].edgeIdToTriggerWhenDone
const isMergingWithParent = state.typebotsQueue[0].isMergingWithParent
const currentResultId = state.typebotsQueue[0].resultId
if (!isMergingWithParent && currentResultId)
await upsertResult({
resultId: currentResultId,
typebot: state.typebotsQueue[0].typebot,
isCompleted: true,
hasStarted: state.typebotsQueue[0].answers.length > 0,
})
let newSessionState = {
...state,
typebotsQueue: [
{
...state.typebotsQueue[1],
typebot: isMergingWithParent
? {
...state.typebotsQueue[1].typebot,
variables: state.typebotsQueue[1].typebot.variables
.map((variable) => ({
...variable,
value:
state.typebotsQueue[0].typebot.variables.find(
(v) => v.name === variable.name
)?.value ?? variable.value,
}))
.concat(
state.typebotsQueue[0].typebot.variables.filter(
(variable) =>
isDefined(variable.value) &&
isNotDefined(
state.typebotsQueue[1].typebot.variables.find(
(v) => v.name === variable.name
)
) as VariableWithValue[]
),
}
: state.typebotsQueue[1].typebot,
answers: isMergingWithParent
? [
...state.typebotsQueue[1].answers.filter(
(incomingAnswer) =>
!state.typebotsQueue[0].answers.find(
(currentAnswer) =>
currentAnswer.key === incomingAnswer.key
)
)
) as VariableWithValue[]
),
...state.typebotsQueue[0].answers,
]
: state.typebotsQueue[1].answers,
},
...state.typebotsQueue.slice(2),
],
} satisfies SessionState
if (state.progressMetadata)
newSessionState.progressMetadata = {
...state.progressMetadata,
totalAnswers:
state.progressMetadata.totalAnswers +
state.typebotsQueue[0].answers.length,
}
const nextGroup = await getNextGroup(newSessionState)(nextEdgeId)
newSessionState = nextGroup.newSessionState
if (!nextGroup)
return {
newSessionState,
}
}
: state.typebotsQueue[1].typebot,
answers: isMergingWithParent
? [
...state.typebotsQueue[1].answers.filter(
(incomingAnswer) =>
!state.typebotsQueue[0].answers.find(
(currentAnswer) =>
currentAnswer.key === incomingAnswer.key
)
),
...state.typebotsQueue[0].answers,
]
: state.typebotsQueue[1].answers,
},
...state.typebotsQueue.slice(2),
],
} satisfies SessionState
if (state.progressMetadata)
newSessionState.progressMetadata = {
...state.progressMetadata,
totalAnswers:
state.progressMetadata.totalAnswers +
state.typebotsQueue[0].answers.length,
}
const nextGroup = await getNextGroup({
state: newSessionState,
edgeId: nextEdgeId,
isOffDefaultPath,
})
newSessionState = nextGroup.newSessionState
if (!nextGroup)
return {
...nextGroup,
newSessionState,
}
}
return {
newSessionState: state,
...nextGroup,
newSessionState,
}
}
const nextGroup = state.typebotsQueue[0].typebot.groups.find(
byId(nextEdge.to.groupId)
)
if (!nextGroup)
return {
newSessionState: state,
}
const startBlockIndex = nextEdge.to.blockId
? nextGroup.blocks.findIndex(byId(nextEdge.to.blockId))
: 0
const currentVisitedEdgeIndex = (state.currentVisitedEdgeIndex ?? -1) + 1
const resultId = state.typebotsQueue[0].resultId
return {
group: {
...nextGroup,
blocks: nextGroup.blocks.slice(startBlockIndex),
} as Group,
newSessionState: {
...state,
currentVisitedEdgeIndex,
},
visitedEdge: resultId
newSessionState: state,
}
}
const nextGroup = state.typebotsQueue[0].typebot.groups.find(
byId(nextEdge.to.groupId)
)
if (!nextGroup)
return {
newSessionState: state,
}
const startBlockIndex = nextEdge.to.blockId
? nextGroup.blocks.findIndex(byId(nextEdge.to.blockId))
: 0
const currentVisitedEdgeIndex = isOffDefaultPath
? (state.currentVisitedEdgeIndex ?? -1) + 1
: state.currentVisitedEdgeIndex
const resultId = state.typebotsQueue[0].resultId
return {
group: {
...nextGroup,
blocks: nextGroup.blocks.slice(startBlockIndex),
} as Group,
newSessionState: {
...state,
currentVisitedEdgeIndex,
previewMetadata:
resultId || !isOffDefaultPath
? state.previewMetadata
: {
...state.previewMetadata,
visitedEdges: (state.previewMetadata?.visitedEdges ?? []).concat(
nextEdge.id
),
},
},
visitedEdge:
resultId && isOffDefaultPath && !nextEdge.id.startsWith('virtual-')
? {
index: currentVisitedEdgeIndex,
index: currentVisitedEdgeIndex as number,
edgeId: nextEdge.id,
resultId,
}
: undefined,
}
}
}

View File

@ -19,6 +19,7 @@
"@typebot.io/tsconfig": "workspace:*",
"@typebot.io/variables": "workspace:*",
"@udecode/plate-common": "30.4.5",
"@typebot.io/logic": "workspace:*",
"ai": "3.0.31",
"chrono-node": "2.7.5",
"date-fns": "2.30.0",

View File

@ -6,7 +6,7 @@ import {
Typebot,
} from '@typebot.io/schemas'
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
import { isEmpty, isNotEmpty } from '@typebot.io/lib/utils'
import { isDefined, isEmpty, isNotEmpty } from '@typebot.io/lib/utils'
import {
getVariablesToParseInfoInText,
parseVariables,
@ -49,7 +49,7 @@ export const parseBubbleBlock = (
richText: parseVariablesInRichText(block.content?.richText ?? [], {
variables,
takeLatestIfList: typebotVersion !== '6',
}),
}).parsedElements,
},
}
}
@ -93,14 +93,15 @@ export const parseBubbleBlock = (
}
}
const parseVariablesInRichText = (
export const parseVariablesInRichText = (
elements: TDescendant[],
{
variables,
takeLatestIfList,
}: { variables: Variable[]; takeLatestIfList?: boolean }
): TDescendant[] => {
): { parsedElements: TDescendant[]; parsedVariableIds: string[] } => {
const parsedElements: TDescendant[] = []
const parsedVariableIds: string[] = []
for (const element of elements) {
if ('text' in element) {
const text = element.text as string
@ -112,6 +113,9 @@ const parseVariablesInRichText = (
variables,
takeLatestIfList,
})
parsedVariableIds.push(
...variablesInText.map((v) => v.variableId).filter(isDefined)
)
if (variablesInText.length === 0) {
parsedElements.push(element)
continue
@ -185,19 +189,28 @@ const parseVariablesInRichText = (
? 'variable'
: element.type
const {
parsedElements: parsedChildren,
parsedVariableIds: parsedChildrenVariableIds,
} = parseVariablesInRichText(element.children as TDescendant[], {
variables,
takeLatestIfList,
})
parsedVariableIds.push(...parsedChildrenVariableIds)
parsedElements.push({
...element,
url: element.url
? parseVariables(variables)(element.url as string)
: undefined,
type,
children: parseVariablesInRichText(element.children as TDescendant[], {
variables,
takeLatestIfList,
}),
children: parsedChildren,
})
}
return parsedElements
return {
parsedElements,
parsedVariableIds,
}
}
const applyElementStyleToDescendants = (

View File

@ -12,11 +12,26 @@ export const createSession = ({
id,
state,
isReplying,
}: Props): Prisma.PrismaPromise<any> =>
prisma.chatSession.create({
data: {
}: Props): Prisma.PrismaPromise<any> => {
if (!id) {
return prisma.chatSession.create({
data: {
id,
state,
isReplying,
},
})
}
return prisma.chatSession.upsert({
where: { id },
update: {
state,
isReplying,
},
create: {
id,
state,
isReplying,
},
})
}

View File

@ -4,24 +4,33 @@ import { Answer, Result } from '@typebot.io/schemas'
type Props = {
id: string
}
export const findResult = ({ id }: Props) =>
prisma.result.findFirst({
where: { id, isArchived: { not: true } },
select: {
id: true,
variables: true,
hasStarted: true,
answers: {
select: {
content: true,
blockId: true,
variableId: true,
export const findResult = async ({ id }: Props) => {
const { answers, answersV2, ...result } =
(await prisma.result.findFirst({
where: { id, isArchived: { not: true } },
select: {
id: true,
variables: true,
hasStarted: true,
answers: {
select: {
content: true,
blockId: true,
},
},
answersV2: {
select: {
content: true,
blockId: true,
},
},
},
},
}) as Promise<
| (Pick<Result, 'id' | 'variables' | 'hasStarted'> & {
answers: Pick<Answer, 'content' | 'blockId' | 'variableId'>[]
})
| null
>
})) ?? {}
if (!result) return null
return {
...result,
answers: (answersV2 ?? []).concat(answers ?? []),
} as Pick<Result, 'id' | 'variables' | 'hasStarted'> & {
answers: Pick<Answer, 'content' | 'blockId'>[]
}
}

View File

@ -0,0 +1,16 @@
import prisma from '@typebot.io/lib/prisma'
import { Prisma } from '@typebot.io/prisma'
import { SessionState } from '@typebot.io/schemas'
type Props = {
answer: Omit<Prisma.AnswerV2CreateManyInput, 'resultId'>
reply: string
state: SessionState
}
export const saveAnswer = async ({ answer, state }: Props) => {
const resultId = state.typebotsQueue[0].resultId
if (!resultId) return
return prisma.answerV2.createMany({
data: [{ ...answer, resultId }],
})
}

View File

@ -0,0 +1,16 @@
import prisma from '@typebot.io/lib/prisma'
import { Prisma } from '@typebot.io/prisma'
import { SetVariableHistoryItem } from '@typebot.io/schemas'
export const saveSetVariableHistoryItems = (
setVariableHistory: SetVariableHistoryItem[]
) =>
prisma.setVariableHistoryItem.createMany({
data: {
...setVariableHistory.map((item) => ({
...item,
value: item.value === null ? Prisma.JsonNull : item.value,
})),
},
skipDuplicates: true,
})

View File

@ -1,34 +0,0 @@
import prisma from '@typebot.io/lib/prisma'
import { Prisma } from '@typebot.io/prisma'
import { InputBlock, SessionState } from '@typebot.io/schemas'
type Props = {
answer: Omit<Prisma.AnswerUncheckedCreateInput, 'resultId'>
reply: string
state: SessionState
}
export const upsertAnswer = async ({ answer, state }: Props) => {
const resultId = state.typebotsQueue[0].resultId
if (!resultId) return
const where = {
resultId,
blockId: answer.blockId,
groupId: answer.groupId,
}
const existingAnswer = await prisma.answer.findUnique({
where: {
resultId_blockId_groupId: where,
},
select: { resultId: true },
})
if (existingAnswer)
return prisma.answer.updateMany({
where,
data: {
content: answer.content,
},
})
return prisma.answer.createMany({
data: [{ ...answer, resultId }],
})
}

View File

@ -1,29 +1,79 @@
import prisma from '@typebot.io/lib/prisma'
import { Prisma } from '@typebot.io/prisma'
import { TypebotInSession } from '@typebot.io/schemas'
import { Prisma, SetVariableHistoryItem, VisitedEdge } from '@typebot.io/prisma'
import { ContinueChatResponse, TypebotInSession } from '@typebot.io/schemas'
import { filterNonSessionVariablesWithValues } from '@typebot.io/variables/filterVariablesWithValues'
import { formatLogDetails } from '../logs/helpers/formatLogDetails'
type Props = {
resultId: string
typebot: TypebotInSession
hasStarted: boolean
isCompleted: boolean
lastChatSessionId?: string
logs?: ContinueChatResponse['logs']
visitedEdges?: VisitedEdge[]
setVariableHistory?: SetVariableHistoryItem[]
}
export const upsertResult = ({
resultId,
typebot,
hasStarted,
isCompleted,
lastChatSessionId,
logs,
visitedEdges,
setVariableHistory,
}: Props): Prisma.PrismaPromise<any> => {
const variablesWithValue = filterNonSessionVariablesWithValues(
typebot.variables
)
const logsToCreate =
logs && logs.length > 0
? {
createMany: {
data: logs.map((log) => ({
...log,
details: formatLogDetails(log.details),
})),
},
}
: undefined
const setVariableHistoryToCreate =
setVariableHistory && setVariableHistory.length > 0
? ({
createMany: {
data: setVariableHistory.map((item) => ({
...item,
value: item.value === null ? Prisma.JsonNull : item.value,
resultId: undefined,
})),
},
} as Prisma.SetVariableHistoryItemUpdateManyWithoutResultNestedInput)
: undefined
const visitedEdgesToCreate =
visitedEdges && visitedEdges.length > 0
? {
createMany: {
data: visitedEdges.map((edge) => ({
...edge,
resultId: undefined,
})),
},
}
: undefined
return prisma.result.upsert({
where: { id: resultId },
update: {
isCompleted: isCompleted ? true : undefined,
hasStarted,
variables: variablesWithValue,
lastChatSessionId,
logs: logsToCreate,
setVariableHistory: setVariableHistoryToCreate,
edges: visitedEdgesToCreate,
},
create: {
id: resultId,
@ -31,6 +81,10 @@ export const upsertResult = ({
isCompleted: isCompleted ? true : false,
hasStarted,
variables: variablesWithValue,
lastChatSessionId,
logs: logsToCreate,
setVariableHistory: setVariableHistoryToCreate,
edges: visitedEdgesToCreate,
},
select: { id: true },
})

View File

@ -1,12 +1,12 @@
import { ContinueChatResponse, ChatSession } from '@typebot.io/schemas'
import {
ContinueChatResponse,
ChatSession,
SetVariableHistoryItem,
} from '@typebot.io/schemas'
import { upsertResult } from './queries/upsertResult'
import { saveLogs } from './queries/saveLogs'
import { updateSession } from './queries/updateSession'
import { formatLogDetails } from './logs/helpers/formatLogDetails'
import { createSession } from './queries/createSession'
import { deleteSession } from './queries/deleteSession'
import * as Sentry from '@sentry/nextjs'
import { saveVisitedEdges } from './queries/saveVisitedEdges'
import { Prisma, VisitedEdge } from '@typebot.io/prisma'
import prisma from '@typebot.io/lib/prisma'
@ -16,7 +16,9 @@ type Props = {
logs: ContinueChatResponse['logs']
clientSideActions: ContinueChatResponse['clientSideActions']
visitedEdges: VisitedEdge[]
setVariableHistory: SetVariableHistoryItem[]
hasCustomEmbedBubble?: boolean
initialSessionId?: string
}
export const saveStateToDatabase = async ({
@ -25,7 +27,9 @@ export const saveStateToDatabase = async ({
logs,
clientSideActions,
visitedEdges,
setVariableHistory,
hasCustomEmbedBubble,
initialSessionId,
}: Props) => {
const containsSetVariableClientSideAction = clientSideActions?.some(
(action) => action.expectsDedicatedReply
@ -46,7 +50,7 @@ export const saveStateToDatabase = async ({
const session = id
? { state, id }
: await createSession({ id, state, isReplying: false })
: await createSession({ id: initialSessionId, state, isReplying: false })
if (!resultId) {
if (queries.length > 0) await prisma.$transaction(queries)
@ -63,25 +67,13 @@ export const saveStateToDatabase = async ({
!input && !containsSetVariableClientSideAction && answers.length > 0
),
hasStarted: answers.length > 0,
lastChatSessionId: session.id,
logs,
visitedEdges,
setVariableHistory,
})
)
if (logs && logs.length > 0)
try {
await saveLogs(
logs.map((log) => ({
...log,
resultId,
details: formatLogDetails(log.details),
}))
)
} catch (e) {
console.error('Failed to save logs', e)
Sentry.captureException(e)
}
if (visitedEdges.length > 0) queries.push(saveVisitedEdges(visitedEdges))
await prisma.$transaction(queries)
return session

View File

@ -2,6 +2,7 @@ import { TRPCError } from '@trpc/server'
import {
ContinueChatResponse,
SessionState,
SetVariableHistoryItem,
StartFrom,
} from '@typebot.io/schemas'
import { executeGroup } from './executeGroup'
@ -25,10 +26,12 @@ export const startBotFlow = async ({
ContinueChatResponse & {
newSessionState: SessionState
visitedEdges: VisitedEdge[]
setVariableHistory: SetVariableHistoryItem[]
}
> => {
let newSessionState = state
const visitedEdges: VisitedEdge[] = []
const setVariableHistory: SetVariableHistoryItem[] = []
if (startFrom?.type === 'group') {
const group = state.typebotsQueue[0].typebot.groups.find(
(group) => group.id === startFrom.groupId
@ -42,22 +45,34 @@ export const startBotFlow = async ({
version,
state: newSessionState,
visitedEdges,
setVariableHistory,
startTime,
})
}
const firstEdgeId = getFirstEdgeId({
state: newSessionState,
typebot: newSessionState.typebotsQueue[0].typebot,
startEventId: startFrom?.type === 'event' ? startFrom.eventId : undefined,
})
if (!firstEdgeId) return { messages: [], newSessionState, visitedEdges: [] }
const nextGroup = await getNextGroup(newSessionState)(firstEdgeId)
if (!firstEdgeId)
return {
messages: [],
newSessionState,
setVariableHistory: [],
visitedEdges: [],
}
const nextGroup = await getNextGroup({
state: newSessionState,
edgeId: firstEdgeId,
isOffDefaultPath: false,
})
newSessionState = nextGroup.newSessionState
if (nextGroup.visitedEdge) visitedEdges.push(nextGroup.visitedEdge)
if (!nextGroup.group) return { messages: [], newSessionState, visitedEdges }
if (!nextGroup.group)
return { messages: [], newSessionState, visitedEdges, setVariableHistory }
return executeGroup(nextGroup.group, {
version,
state: newSessionState,
visitedEdges,
setVariableHistory,
startTime,
})
}

View File

@ -11,6 +11,7 @@ import {
SessionState,
TypebotInSession,
Block,
SetVariableHistoryItem,
} from '@typebot.io/schemas'
import {
StartChatInput,
@ -31,7 +32,10 @@ import { injectVariablesFromExistingResult } from '@typebot.io/variables/injectV
import { getNextGroup } from './getNextGroup'
import { upsertResult } from './queries/upsertResult'
import { continueBotFlow } from './continueBotFlow'
import { parseVariables } from '@typebot.io/variables/parseVariables'
import {
getVariablesToParseInfoInText,
parseVariables,
} from '@typebot.io/variables/parseVariables'
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
import { VisitedEdge } from '@typebot.io/prisma'
@ -42,6 +46,9 @@ import {
defaultGuestAvatarIsEnabled,
defaultHostAvatarIsEnabled,
} from '@typebot.io/schemas/features/typebot/theme/constants'
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
import { parseVariablesInRichText } from './parseBubbleBlock'
type StartParams =
| ({
@ -68,6 +75,7 @@ export const startSession = async ({
Omit<StartChatResponse, 'resultId' | 'isStreamEnabled' | 'sessionId'> & {
newSessionState: SessionState
visitedEdges: VisitedEdge[]
setVariableHistory: SetVariableHistoryItem[]
resultId?: string
}
> => {
@ -145,6 +153,8 @@ export const startSession = async ({
: typebot.theme.general?.progressBar?.isEnabled
? { totalAnswers: 0 }
: undefined,
setVariableIdsForHistory:
extractVariableIdsUsedForTranscript(typebotInSession),
...initialSessionState,
}
@ -164,6 +174,7 @@ export const startSession = async ({
dynamicTheme: parseDynamicTheme(initialState),
messages: [],
visitedEdges: [],
setVariableHistory: [],
}
}
@ -178,14 +189,18 @@ export const startSession = async ({
// If params has message and first block is an input block, we can directly continue the bot flow
if (message) {
const firstEdgeId = getFirstEdgeId({
state: chatReply.newSessionState,
typebot: chatReply.newSessionState.typebotsQueue[0].typebot,
startEventId:
startParams.type === 'preview' &&
startParams.startFrom?.type === 'event'
? startParams.startFrom.eventId
: undefined,
})
const nextGroup = await getNextGroup(chatReply.newSessionState)(firstEdgeId)
const nextGroup = await getNextGroup({
state: chatReply.newSessionState,
edgeId: firstEdgeId,
isOffDefaultPath: false,
})
const newSessionState = nextGroup.newSessionState
const firstBlock = nextGroup.group?.blocks.at(0)
if (firstBlock && isInputBlock(firstBlock)) {
@ -214,6 +229,7 @@ export const startSession = async ({
newSessionState,
logs,
visitedEdges,
setVariableHistory,
} = chatReply
const clientSideActions = startFlowClientActions ?? []
@ -268,6 +284,7 @@ export const startSession = async ({
dynamicTheme: parseDynamicTheme(newSessionState),
logs: startLogs.length > 0 ? startLogs : undefined,
visitedEdges,
setVariableHistory,
}
return {
@ -290,6 +307,7 @@ export const startSession = async ({
dynamicTheme: parseDynamicTheme(newSessionState),
logs: startLogs.length > 0 ? startLogs : undefined,
visitedEdges,
setVariableHistory,
}
}
@ -497,3 +515,59 @@ const convertStartTypebotToTypebotInSession = (
variables: startVariables,
events: typebot.events,
}
const extractVariableIdsUsedForTranscript = (
typebot: TypebotInSession
): string[] => {
const variableIds: Set<string> = new Set()
const parseVarParams = {
variables: typebot.variables,
takeLatestIfList: typebot.version !== '6',
}
typebot.groups.forEach((group) => {
group.blocks.forEach((block) => {
if (block.type === BubbleBlockType.TEXT) {
const { parsedVariableIds } = parseVariablesInRichText(
block.content?.richText ?? [],
parseVarParams
)
parsedVariableIds.forEach((variableId) => variableIds.add(variableId))
}
if (
block.type === BubbleBlockType.IMAGE ||
block.type === BubbleBlockType.VIDEO ||
block.type === BubbleBlockType.AUDIO
) {
if (!block.content?.url) return
const variablesInfo = getVariablesToParseInfoInText(
block.content.url,
parseVarParams
)
variablesInfo.forEach((variableInfo) =>
variableInfo.variableId
? variableIds.add(variableInfo.variableId ?? '')
: undefined
)
}
if (block.type === LogicBlockType.CONDITION) {
block.items.forEach((item) =>
item.content?.comparisons?.forEach((comparison) => {
if (comparison.variableId) variableIds.add(comparison.variableId)
if (comparison.value) {
const variableIdsInValue = getVariablesToParseInfoInText(
comparison.value,
parseVarParams
)
variableIdsInValue.forEach((variableInfo) => {
variableInfo.variableId
? variableIds.add(variableInfo.variableId)
: undefined
})
}
})
)
}
})
})
return [...variableIds]
}

View File

@ -2,6 +2,7 @@ import {
ContinueChatResponse,
CustomEmbedBubble,
SessionState,
SetVariableHistoryItem,
} from '@typebot.io/schemas'
export type EdgeId = string
@ -9,6 +10,7 @@ export type EdgeId = string
export type ExecuteLogicResponse = {
outgoingEdgeId: EdgeId | undefined
newSessionState?: SessionState
newSetVariableHistory?: SetVariableHistoryItem[]
} & Pick<ContinueChatResponse, 'clientSideActions' | 'logs'>
export type ExecuteIntegrationResponse = {
@ -16,6 +18,7 @@ export type ExecuteIntegrationResponse = {
newSessionState?: SessionState
startTimeShouldBeUpdated?: boolean
customEmbedBubble?: CustomEmbedBubble
newSetVariableHistory?: SetVariableHistoryItem[]
} & Pick<ContinueChatResponse, 'clientSideActions' | 'logs'>
type WhatsAppMediaMessage = {

View File

@ -114,6 +114,7 @@ export const resumeWhatsAppFlow = async ({
messages,
clientSideActions,
visitedEdges,
setVariableHistory,
} = resumeResponse
const isFirstChatChunk = (!session || isSessionExpired) ?? false
@ -140,6 +141,7 @@ export const resumeWhatsAppFlow = async ({
},
},
visitedEdges,
setVariableHistory,
})
return {

View File

@ -3,6 +3,7 @@ import {
ContinueChatResponse,
PublicTypebot,
SessionState,
SetVariableHistoryItem,
Settings,
Typebot,
} from '@typebot.io/schemas'
@ -35,6 +36,7 @@ export const startWhatsAppSession = async ({
| (ContinueChatResponse & {
newSessionState: SessionState
visitedEdges: VisitedEdge[]
setVariableHistory: SetVariableHistoryItem[]
})
| { error: string }
> => {