⬆️ Upgrade AI SDK (#1641)
This commit is contained in:
28
packages/ai/appendToolResultsToMessages.ts
Normal file
28
packages/ai/appendToolResultsToMessages.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { CoreMessage, ToolCallPart, ToolResultPart } from 'ai'
|
||||
|
||||
type Props = {
|
||||
messages: CoreMessage[]
|
||||
toolCalls: ToolCallPart[]
|
||||
toolResults: ToolResultPart[]
|
||||
}
|
||||
export const appendToolResultsToMessages = ({
|
||||
messages,
|
||||
toolCalls,
|
||||
toolResults,
|
||||
}: Props): CoreMessage[] => {
|
||||
if (toolCalls.length > 0) {
|
||||
messages.push({
|
||||
role: 'assistant',
|
||||
content: toolCalls,
|
||||
})
|
||||
}
|
||||
|
||||
if (toolResults.length > 0) {
|
||||
messages.push({
|
||||
role: 'tool',
|
||||
content: toolResults,
|
||||
})
|
||||
}
|
||||
|
||||
return messages
|
||||
}
|
||||
14
packages/ai/package.json
Normal file
14
packages/ai/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "@typebot.io/ai",
|
||||
"version": "1.0.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@typebot.io/lib": "workspace:*",
|
||||
"@typebot.io/forge": "workspace:*",
|
||||
"@typebot.io/variables": "workspace:*",
|
||||
"ai": "3.2.22",
|
||||
"ky": "1.2.4",
|
||||
"@typebot.io/tsconfig": "workspace:*"
|
||||
}
|
||||
}
|
||||
113
packages/ai/parseChatCompletionMessages.ts
Normal file
113
packages/ai/parseChatCompletionMessages.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { CoreAssistantMessage, CoreMessage, CoreUserMessage } from 'ai'
|
||||
import { VariableStore } from '@typebot.io/forge'
|
||||
import { isDefined, isEmpty } from '@typebot.io/lib'
|
||||
import { splitUserTextMessageIntoBlocks } from './splitUserTextMessageIntoBlocks'
|
||||
import { Message, StandardMessage, DialogueMessage } from './types'
|
||||
|
||||
type Props = {
|
||||
messages: Message[] | undefined
|
||||
isVisionEnabled: boolean
|
||||
shouldDownloadImages: boolean
|
||||
variables: VariableStore
|
||||
}
|
||||
|
||||
export const parseChatCompletionMessages = async ({
|
||||
messages,
|
||||
isVisionEnabled,
|
||||
shouldDownloadImages,
|
||||
variables,
|
||||
}: Props): Promise<CoreMessage[]> => {
|
||||
if (!messages) return []
|
||||
const parsedMessages: CoreMessage[] = (
|
||||
await Promise.all(
|
||||
messages.map(async (message) => {
|
||||
if (!message.role) return
|
||||
|
||||
if (message.role === 'Dialogue')
|
||||
return parseDialogueMessage({
|
||||
message,
|
||||
variables,
|
||||
isVisionEnabled,
|
||||
shouldDownloadImages,
|
||||
})
|
||||
|
||||
return parseStandardMessage({
|
||||
message,
|
||||
variables,
|
||||
isVisionEnabled,
|
||||
shouldDownloadImages,
|
||||
})
|
||||
})
|
||||
)
|
||||
)
|
||||
.flat()
|
||||
.filter(isDefined)
|
||||
|
||||
return parsedMessages
|
||||
}
|
||||
|
||||
const parseDialogueMessage = async ({
|
||||
message,
|
||||
variables,
|
||||
isVisionEnabled,
|
||||
shouldDownloadImages,
|
||||
}: Pick<Props, 'variables' | 'isVisionEnabled' | 'shouldDownloadImages'> & {
|
||||
message: DialogueMessage
|
||||
}) => {
|
||||
if (!message.dialogueVariableId) return
|
||||
const dialogue = variables.get(message.dialogueVariableId) ?? []
|
||||
const dialogueArr = Array.isArray(dialogue) ? dialogue : [dialogue]
|
||||
|
||||
return Promise.all(
|
||||
dialogueArr.map<
|
||||
Promise<CoreUserMessage | CoreAssistantMessage | undefined>
|
||||
>(async (dialogueItem, index) => {
|
||||
if (!dialogueItem) return
|
||||
if (index === 0 && message.startsBy === 'assistant')
|
||||
return { role: 'assistant' as const, content: dialogueItem }
|
||||
if (index % (message.startsBy === 'assistant' ? 1 : 2) === 0) {
|
||||
return {
|
||||
role: 'user' as const,
|
||||
content: isVisionEnabled
|
||||
? await splitUserTextMessageIntoBlocks({
|
||||
input: dialogueItem ?? '',
|
||||
shouldDownloadImages,
|
||||
})
|
||||
: dialogueItem,
|
||||
}
|
||||
}
|
||||
return { role: 'assistant' as const, content: dialogueItem }
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const parseStandardMessage = async ({
|
||||
message,
|
||||
variables,
|
||||
isVisionEnabled,
|
||||
shouldDownloadImages,
|
||||
}: Pick<Props, 'variables' | 'isVisionEnabled' | 'shouldDownloadImages'> & {
|
||||
message: StandardMessage
|
||||
}) => {
|
||||
if (!message.content) return
|
||||
|
||||
const content = variables.parse(message.content)
|
||||
|
||||
if (isEmpty(content)) return
|
||||
|
||||
if (message.role === 'user')
|
||||
return {
|
||||
role: 'user' as const,
|
||||
content: isVisionEnabled
|
||||
? await splitUserTextMessageIntoBlocks({
|
||||
input: content,
|
||||
shouldDownloadImages,
|
||||
})
|
||||
: content,
|
||||
}
|
||||
|
||||
return {
|
||||
role: message.role,
|
||||
content,
|
||||
}
|
||||
}
|
||||
68
packages/ai/parseTools.ts
Normal file
68
packages/ai/parseTools.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { VariableStore } from '@typebot.io/forge'
|
||||
import { z } from '@typebot.io/forge/zod'
|
||||
import { executeFunction } from '@typebot.io/variables/executeFunction'
|
||||
import { Variable } from '@typebot.io/variables/types'
|
||||
import { CoreTool } from 'ai'
|
||||
import { isNotEmpty } from '@typebot.io/lib'
|
||||
import { Tools } from './schemas'
|
||||
|
||||
export const parseTools = ({
|
||||
tools,
|
||||
variables,
|
||||
}: {
|
||||
tools: Tools
|
||||
variables: VariableStore
|
||||
onNewVariabes?: (newVariables: Variable[]) => void
|
||||
}): Record<string, CoreTool> => {
|
||||
if (!tools?.length) return {}
|
||||
return tools.reduce<Record<string, CoreTool>>((acc, tool) => {
|
||||
if (!tool.code || !tool.name) return acc
|
||||
acc[tool.name] = {
|
||||
description: tool.description,
|
||||
parameters: parseParameters(tool.parameters),
|
||||
execute: async (args) => {
|
||||
const { output, newVariables } = await executeFunction({
|
||||
variables: variables.list(),
|
||||
args,
|
||||
body: tool.code!,
|
||||
})
|
||||
newVariables?.forEach((v) => variables.set(v.id, v.value))
|
||||
return output
|
||||
},
|
||||
} satisfies CoreTool
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
const parseParameters = (
|
||||
parameters: NonNullable<Tools>[number]['parameters']
|
||||
): z.ZodTypeAny | undefined => {
|
||||
if (!parameters || parameters?.length === 0) return
|
||||
|
||||
const shape: z.ZodRawShape = {}
|
||||
parameters.forEach((param) => {
|
||||
if (!param.name) return
|
||||
switch (param.type) {
|
||||
case 'string':
|
||||
shape[param.name] = z.string()
|
||||
break
|
||||
case 'number':
|
||||
shape[param.name] = z.number()
|
||||
break
|
||||
case 'boolean':
|
||||
shape[param.name] = z.boolean()
|
||||
break
|
||||
case 'enum': {
|
||||
if (!param.values || param.values.length === 0) return
|
||||
shape[param.name] = z.enum(param.values as any)
|
||||
break
|
||||
}
|
||||
}
|
||||
if (isNotEmpty(param.description))
|
||||
shape[param.name] = shape[param.name].describe(param.description)
|
||||
if (param.required === false)
|
||||
shape[param.name] = shape[param.name].optional()
|
||||
})
|
||||
|
||||
return z.object(shape)
|
||||
}
|
||||
11
packages/ai/pumpStreamUntilDone.ts
Normal file
11
packages/ai/pumpStreamUntilDone.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const pumpStreamUntilDone = async (
|
||||
controller: ReadableStreamDefaultController<Uint8Array>,
|
||||
reader: ReadableStreamDefaultReader
|
||||
): Promise<void> => {
|
||||
const { done, value } = await reader.read()
|
||||
|
||||
if (done) return
|
||||
|
||||
controller.enqueue(value)
|
||||
return pumpStreamUntilDone(controller, reader)
|
||||
}
|
||||
79
packages/ai/schemas.ts
Normal file
79
packages/ai/schemas.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { option } from '@typebot.io/forge'
|
||||
import { z } from '@typebot.io/forge/zod'
|
||||
|
||||
const parameterBase = {
|
||||
name: option.string.layout({
|
||||
label: 'Name',
|
||||
placeholder: 'myVariable',
|
||||
withVariableButton: false,
|
||||
}),
|
||||
description: option.string.layout({
|
||||
label: 'Description',
|
||||
withVariableButton: false,
|
||||
}),
|
||||
required: option.boolean.layout({
|
||||
label: 'Is required?',
|
||||
}),
|
||||
}
|
||||
|
||||
export const toolParametersSchema = option
|
||||
.array(
|
||||
option.discriminatedUnion('type', [
|
||||
option
|
||||
.object({
|
||||
type: option.literal('string'),
|
||||
})
|
||||
.extend(parameterBase),
|
||||
option
|
||||
.object({
|
||||
type: option.literal('number'),
|
||||
})
|
||||
.extend(parameterBase),
|
||||
option
|
||||
.object({
|
||||
type: option.literal('boolean'),
|
||||
})
|
||||
.extend(parameterBase),
|
||||
option
|
||||
.object({
|
||||
type: option.literal('enum'),
|
||||
values: option
|
||||
.array(option.string)
|
||||
.layout({ itemLabel: 'possible value' }),
|
||||
})
|
||||
.extend(parameterBase),
|
||||
])
|
||||
)
|
||||
.layout({
|
||||
accordion: 'Parameters',
|
||||
itemLabel: 'parameter',
|
||||
})
|
||||
|
||||
const functionToolItemSchema = option.object({
|
||||
type: option.literal('function'),
|
||||
name: option.string.layout({
|
||||
label: 'Name',
|
||||
placeholder: 'myFunctionName',
|
||||
withVariableButton: false,
|
||||
}),
|
||||
description: option.string.layout({
|
||||
label: 'Description',
|
||||
placeholder: 'A brief description of what this function does.',
|
||||
withVariableButton: false,
|
||||
}),
|
||||
parameters: toolParametersSchema,
|
||||
code: option.string.layout({
|
||||
inputType: 'code',
|
||||
label: 'Code',
|
||||
lang: 'javascript',
|
||||
moreInfoTooltip:
|
||||
'A javascript code snippet that can use the defined parameters. It should return a value.',
|
||||
withVariableButton: false,
|
||||
}),
|
||||
})
|
||||
|
||||
export const toolsSchema = option
|
||||
.array(option.discriminatedUnion('type', [functionToolItemSchema]))
|
||||
.layout({ accordion: 'Tools', itemLabel: 'tool' })
|
||||
|
||||
export type Tools = z.infer<typeof toolsSchema>
|
||||
57
packages/ai/splitUserTextMessageIntoBlocks.ts
Normal file
57
packages/ai/splitUserTextMessageIntoBlocks.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { ImagePart, TextPart, UserContent } from 'ai'
|
||||
import ky, { HTTPError } from 'ky'
|
||||
|
||||
type Props = {
|
||||
input: string
|
||||
shouldDownloadImages: boolean
|
||||
}
|
||||
export const splitUserTextMessageIntoBlocks = async ({
|
||||
input,
|
||||
shouldDownloadImages,
|
||||
}: Props): Promise<UserContent> => {
|
||||
const urlRegex = /(^|\n\n)(https?:\/\/[^\s]+)(\n\n|$)/g
|
||||
const match = input.match(urlRegex)
|
||||
if (!match) return input
|
||||
let parts: (TextPart | ImagePart)[] = []
|
||||
let processedInput = input
|
||||
|
||||
for (const url of match) {
|
||||
const textBeforeUrl = processedInput.slice(0, processedInput.indexOf(url))
|
||||
if (textBeforeUrl.trim().length > 0) {
|
||||
parts.push({ type: 'text', text: textBeforeUrl })
|
||||
}
|
||||
const cleanUrl = url.trim()
|
||||
|
||||
try {
|
||||
const response = await ky.get(cleanUrl)
|
||||
if (
|
||||
!response.ok ||
|
||||
!response.headers.get('content-type')?.startsWith('image/')
|
||||
) {
|
||||
parts.push({ type: 'text', text: cleanUrl })
|
||||
} else {
|
||||
parts.push({
|
||||
type: 'image',
|
||||
image: shouldDownloadImages
|
||||
? await response.arrayBuffer()
|
||||
: url.trim(),
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof HTTPError) {
|
||||
console.log(err.response.status, await err.response.text())
|
||||
} else {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
processedInput = processedInput.slice(
|
||||
processedInput.indexOf(url) + url.length
|
||||
)
|
||||
}
|
||||
|
||||
if (processedInput.trim().length > 0) {
|
||||
parts.push({ type: 'text', text: processedInput })
|
||||
}
|
||||
|
||||
return parts
|
||||
}
|
||||
8
packages/ai/tsconfig.json
Normal file
8
packages/ai/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@typebot.io/tsconfig/base.json",
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["node_modules"],
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2021", "DOM"]
|
||||
}
|
||||
}
|
||||
12
packages/ai/types.ts
Normal file
12
packages/ai/types.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export type DialogueMessage = {
|
||||
role: 'Dialogue'
|
||||
startsBy?: 'user' | 'assistant'
|
||||
dialogueVariableId?: string
|
||||
}
|
||||
|
||||
export type StandardMessage = {
|
||||
role: 'user' | 'assistant' | 'system'
|
||||
content?: string
|
||||
}
|
||||
|
||||
export type Message = DialogueMessage | StandardMessage
|
||||
Reference in New Issue
Block a user