2
0

⬆️ Upgrade AI SDK (#1641)

This commit is contained in:
Baptiste Arnaud
2024-07-15 14:32:42 +02:00
committed by GitHub
parent a4fb8b6d10
commit 043f0054b0
60 changed files with 2183 additions and 1683 deletions

View 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
View 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:*"
}
}

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

View 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
View 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>

View 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
}

View 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
View 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