2
0

(openai) Add Messages sequence type

To make it easy to just plug a sequence of user / assistant messages to Chat completion task

Closes #387
This commit is contained in:
Baptiste Arnaud
2023-03-13 16:28:08 +01:00
parent 48db171c1b
commit c4db2f42a6
27 changed files with 468 additions and 153 deletions

View File

@ -1,7 +1,14 @@
import { ExecuteIntegrationResponse } from '@/features/chat/types'
import { saveErrorLog } from '@/features/logs/api'
import { transformStringVariablesToList } from '@/features/variables/transformVariablesToList'
import { parseVariables, updateVariables } from '@/features/variables/utils'
import prisma from '@/lib/prisma'
import { SessionState, VariableWithUnknowValue } from 'models'
import {
SessionState,
Variable,
VariableWithUnknowValue,
VariableWithValue,
} from 'models'
import {
ChatCompletionOpenAIOptions,
OpenAICredentials,
@ -20,6 +27,7 @@ export const createChatCompletionOpenAI = async (
const {
typebot: { variables },
} = state
let newSessionState = state
if (!options.credentialsId) return { outgoingEdgeId }
const credentials = await prisma.credentials.findUnique({
where: {
@ -34,52 +42,147 @@ export const createChatCompletionOpenAI = async (
const configuration = new Configuration({
apiKey,
})
const { variablesTransformedToList, messages } = parseMessages(variables)(
options.messages
)
if (variablesTransformedToList.length > 0)
newSessionState = await updateVariables(state)(variablesTransformedToList)
const openai = new OpenAIApi(configuration)
const {
data: { choices, usage },
} = await openai.createChatCompletion({
model: options.model,
messages: options.messages
.map((message) => ({
role: message.role,
content: parseVariables(variables)(message.content),
}))
.filter(
(message) => isNotEmpty(message.role) && isNotEmpty(message.content)
) as ChatCompletionRequestMessage[],
})
const messageContent = choices[0].message?.content
const totalTokens = usage?.total_tokens
if (!messageContent) {
return { outgoingEdgeId }
}
const newVariables = options.responseMapping.reduce<
VariableWithUnknowValue[]
>((newVariables, mapping) => {
const existingVariable = variables.find(byId(mapping.variableId))
if (!existingVariable) return newVariables
if (mapping.valueToExtract === 'Message content') {
newVariables.push({
...existingVariable,
value: messageContent,
})
try {
const {
data: { choices, usage },
} = await openai.createChatCompletion({
model: options.model,
messages,
})
const messageContent = choices[0].message?.content
const totalTokens = usage?.total_tokens
if (!messageContent) {
return { outgoingEdgeId, newSessionState }
}
if (mapping.valueToExtract === 'Total tokens' && isDefined(totalTokens)) {
newVariables.push({
...existingVariable,
value: totalTokens,
})
const newVariables = options.responseMapping.reduce<
VariableWithUnknowValue[]
>((newVariables, mapping) => {
const existingVariable = variables.find(byId(mapping.variableId))
if (!existingVariable) return newVariables
if (mapping.valueToExtract === 'Message content') {
newVariables.push({
...existingVariable,
value: Array.isArray(existingVariable.value)
? existingVariable.value.concat(messageContent)
: messageContent,
})
}
if (mapping.valueToExtract === 'Total tokens' && isDefined(totalTokens)) {
newVariables.push({
...existingVariable,
value: totalTokens,
})
}
return newVariables
}, [])
if (newVariables.length > 0) {
newSessionState = await updateVariables(newSessionState)(newVariables)
return {
outgoingEdgeId,
newSessionState,
}
}
return newVariables
}, [])
if (newVariables.length > 0) {
const newSessionState = await updateVariables(state)(newVariables)
return {
outgoingEdgeId,
newSessionState,
}
}
return {
outgoingEdgeId,
} catch (err) {
const log = {
status: 'error',
description: 'OpenAI block returned error',
details: JSON.stringify(err, null, 2).substring(0, 1000),
}
state.result &&
(await saveErrorLog({
resultId: state.result.id,
message: log.description,
details: log.details,
}))
return {
outgoingEdgeId,
logs: [log],
newSessionState,
}
}
}
const parseMessages =
(variables: Variable[]) =>
(
messages: ChatCompletionOpenAIOptions['messages']
): {
variablesTransformedToList: VariableWithValue[]
messages: ChatCompletionRequestMessage[]
} => {
const variablesTransformedToList: VariableWithValue[] = []
const parsedMessages = messages
.flatMap((message) => {
if (!message.role) return
if (message.role === 'Messages sequence ✨') {
if (
!message.content?.assistantMessagesVariableId ||
!message.content?.userMessagesVariableId
)
return
variablesTransformedToList.push(
...transformStringVariablesToList(variables)([
message.content.assistantMessagesVariableId,
message.content.userMessagesVariableId,
])
)
const updatedVariables = variables.map((variable) => {
const variableTransformedToList = variablesTransformedToList.find(
byId(variable.id)
)
if (variableTransformedToList) return variableTransformedToList
return variable
})
const userMessages = (updatedVariables.find(
(variable) =>
variable.id === message.content?.userMessagesVariableId
)?.value ?? []) as string[]
const assistantMessages = (updatedVariables.find(
(variable) =>
variable.id === message.content?.assistantMessagesVariableId
)?.value ?? []) as string[]
if (userMessages.length > assistantMessages.length)
return userMessages.flatMap((userMessage, index) => [
{
role: 'user',
content: userMessage,
},
{ role: 'assistant', content: assistantMessages[index] },
]) satisfies ChatCompletionRequestMessage[]
else {
return assistantMessages.flatMap((assistantMessage, index) => [
{ role: 'assistant', content: assistantMessage },
{
role: 'user',
content: userMessages[index],
},
]) satisfies ChatCompletionRequestMessage[]
}
}
return {
role: message.role,
content: parseVariables(variables)(message.content),
} satisfies ChatCompletionRequestMessage
})
.filter(
(message) => isNotEmpty(message?.role) && isNotEmpty(message?.content)
) as ChatCompletionRequestMessage[]
return {
variablesTransformedToList,
messages: parsedMessages,
}
}

View File

@ -103,7 +103,12 @@ const saveVariableValueIfAny =
if (!foundVariable) return state
const newSessionState = await updateVariables(state)([
{ ...foundVariable, value: reply },
{
...foundVariable,
value: Array.isArray(foundVariable.value)
? foundVariable.value.concat(reply)
: reply,
},
])
return newSessionState

View File

@ -124,15 +124,12 @@ const computeRuntimeOptions =
const getPrefilledInputValue =
(variables: SessionState['typebot']['variables']) => (block: InputBlock) => {
return (
variables
.find(
(variable) =>
variable.id === block.options.variableId &&
isDefined(variable.value)
)
?.value?.toString() ?? undefined
)
const variableValue = variables.find(
(variable) =>
variable.id === block.options.variableId && isDefined(variable.value)
)?.value
if (!variableValue || Array.isArray(variableValue)) return
return variableValue
}
const parseBubbleBlock =

View File

@ -0,0 +1,26 @@
import { Variable, VariableWithValue } from 'models'
import { isNotDefined } from 'utils'
export const transformStringVariablesToList =
(variables: Variable[]) =>
(variableIds: string[]): VariableWithValue[] => {
const newVariables = variables.reduce<VariableWithValue[]>(
(variables, variable) => {
if (
!variableIds.includes(variable.id) ||
isNotDefined(variable.value) ||
typeof variable.value !== 'string'
)
return variables
return [
...variables,
{
...variable,
value: [variable.value],
},
]
},
[]
)
return newVariables
}