From b68f16f4f7cb0fbed73ba4be32f7969d2786c496 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Thu, 25 Jan 2024 16:35:16 +0100 Subject: [PATCH] :sparkles: Add Mistral AI block Closes #1179 --- .../editor/blocks/integrations/mistral.mdx | 19 ++ apps/docs/mint.json | 3 +- apps/docs/openapi/builder.json | 3 +- apps/docs/openapi/viewer.json | 3 +- .../mistral/actions/createChatCompletion.ts | 125 ++++++++++++ packages/forge/blocks/mistral/auth.ts | 15 ++ .../blocks/mistral/helpers/parseMessages.ts | 50 +++++ packages/forge/blocks/mistral/index.ts | 14 ++ packages/forge/blocks/mistral/logo.tsx | 191 ++++++++++++++++++ packages/forge/blocks/mistral/package.json | 19 ++ packages/forge/blocks/mistral/tsconfig.json | 10 + packages/forge/cli/index.ts | 2 +- packages/forge/repository/index.ts | 1 + packages/forge/schemas/index.ts | 2 + packages/forge/schemas/package.json | 3 +- pnpm-lock.yaml | 36 ++++ 16 files changed, 491 insertions(+), 5 deletions(-) create mode 100644 apps/docs/editor/blocks/integrations/mistral.mdx create mode 100644 packages/forge/blocks/mistral/actions/createChatCompletion.ts create mode 100644 packages/forge/blocks/mistral/auth.ts create mode 100644 packages/forge/blocks/mistral/helpers/parseMessages.ts create mode 100644 packages/forge/blocks/mistral/index.ts create mode 100644 packages/forge/blocks/mistral/logo.tsx create mode 100644 packages/forge/blocks/mistral/package.json create mode 100644 packages/forge/blocks/mistral/tsconfig.json diff --git a/apps/docs/editor/blocks/integrations/mistral.mdx b/apps/docs/editor/blocks/integrations/mistral.mdx new file mode 100644 index 000000000..316fc13ee --- /dev/null +++ b/apps/docs/editor/blocks/integrations/mistral.mdx @@ -0,0 +1,19 @@ +--- +title: Mistral AI +--- + +Similarly to the [OpenAI block](./openai), this block allows you to chat with Mistral's AI models. + +Create your Mistral account here: https://console.mistral.ai. + +## Create chat completion + +With the Mistral AI block, you can create a chat completion based on your user queries and display the answer back to your typebot. + +This action comes with a convenient message type called `Dialogue`. It allows you to easily pass a sequence of saved assistant / user messages history to Mistral AI + +## Troobleshooting + +- If you get HTTP 401 error while loading the Mistral models, it means your API key is still not propagated on Mistral's side. Please wait a few minutes and try again. + +- If you get a rate limit error, make sure to add a subscription to your Mistral account. You can do so by going to your [Mistral billing section](https://console.mistral.ai/billing/) and clicking on the `Subscribe` button. diff --git a/apps/docs/mint.json b/apps/docs/mint.json index 8e0b7b04b..3203cad59 100644 --- a/apps/docs/mint.json +++ b/apps/docs/mint.json @@ -125,7 +125,8 @@ "editor/blocks/integrations/chatwoot", "editor/blocks/integrations/meta-pixel", "editor/blocks/integrations/openai", - "editor/blocks/integrations/zemantic-ai" + "editor/blocks/integrations/zemantic-ai", + "editor/blocks/integrations/mistral" ] } ] diff --git a/apps/docs/openapi/builder.json b/apps/docs/openapi/builder.json index 9aa6f32ff..1734065cc 100644 --- a/apps/docs/openapi/builder.json +++ b/apps/docs/openapi/builder.json @@ -26852,7 +26852,8 @@ "zemantic-ai", "cal-com", "chat-node", - "qr-code" + "qr-code", + "mistral" ] }, "options": {} diff --git a/apps/docs/openapi/viewer.json b/apps/docs/openapi/viewer.json index 3ae8d4aeb..4e3cc1d8e 100644 --- a/apps/docs/openapi/viewer.json +++ b/apps/docs/openapi/viewer.json @@ -10353,7 +10353,8 @@ "zemantic-ai", "cal-com", "chat-node", - "qr-code" + "qr-code", + "mistral" ] }, "options": {} diff --git a/packages/forge/blocks/mistral/actions/createChatCompletion.ts b/packages/forge/blocks/mistral/actions/createChatCompletion.ts new file mode 100644 index 000000000..252a93787 --- /dev/null +++ b/packages/forge/blocks/mistral/actions/createChatCompletion.ts @@ -0,0 +1,125 @@ +import { option, createAction } from '@typebot.io/forge' +import { isDefined } from '@typebot.io/lib' +import { auth } from '../auth' +import MistralClient from '@mistralai/mistralai' +import { parseMessages } from '../helpers/parseMessages' +import { OpenAIStream } from 'ai' + +const nativeMessageContentSchema = { + content: option.string.layout({ + inputType: 'textarea', + placeholder: 'Content', + }), +} + +const systemMessageItemSchema = option + .object({ + role: option.literal('system'), + }) + .extend(nativeMessageContentSchema) + +const userMessageItemSchema = option + .object({ + role: option.literal('user'), + }) + .extend(nativeMessageContentSchema) + +const assistantMessageItemSchema = option + .object({ + role: option.literal('assistant'), + }) + .extend(nativeMessageContentSchema) + +const dialogueMessageItemSchema = option.object({ + role: option.literal('Dialogue'), + dialogueVariableId: option.string.layout({ + inputType: 'variableDropdown', + placeholder: 'Dialogue variable', + }), + startsBy: option.enum(['user', 'assistant']).layout({ + label: 'starts by', + direction: 'row', + defaultValue: 'user', + }), +}) + +export const options = option.object({ + model: option.string.layout({ + placeholder: 'Select a model', + fetcher: 'fetchModels', + }), + messages: option + .array( + option.discriminatedUnion('role', [ + systemMessageItemSchema, + userMessageItemSchema, + assistantMessageItemSchema, + dialogueMessageItemSchema, + ]) + ) + .layout({ accordion: 'Messages', itemLabel: 'message', isOrdered: true }), + responseMapping: option.saveResponseArray(['Message content']).layout({ + accordion: 'Save response', + }), +}) + +export const createChatCompletion = createAction({ + name: 'Create chat completion', + auth, + options, + getSetVariableIds: (options) => + options.responseMapping?.map((res) => res.variableId).filter(isDefined) ?? + [], + fetchers: [ + { + id: 'fetchModels', + dependencies: [], + fetch: async ({ credentials }) => { + const client = new MistralClient(credentials.apiKey) + + const listModelsResponse = await client.listModels() + + return ( + listModelsResponse.data + .sort((a, b) => b.created - a.created) + .map((model) => model.id) ?? [] + ) + }, + }, + ], + run: { + server: async ({ credentials: { apiKey }, options, variables, logs }) => { + if (!options.model) return logs.add('No model selected') + const client = new MistralClient(apiKey) + + const response = await client.chat({ + model: options.model, + messages: parseMessages({ options, variables }), + }) + + options.responseMapping?.forEach((mapping) => { + if (!mapping.variableId) return + if (!mapping.item || mapping.item === 'Message content') + variables.set(mapping.variableId, response.choices[0].message.content) + }) + }, + stream: { + getStreamVariableId: (options) => + options.responseMapping?.find( + (res) => res.item === 'Message content' || !res.item + )?.variableId, + run: async ({ credentials: { apiKey }, options, variables }) => { + if (!options.model) return + const client = new MistralClient(apiKey) + + const response = client.chatStream({ + model: options.model, + messages: parseMessages({ options, variables }), + }) + + // @ts-ignore https://github.com/vercel/ai/issues/936 + return OpenAIStream(response) + }, + }, + }, +}) diff --git a/packages/forge/blocks/mistral/auth.ts b/packages/forge/blocks/mistral/auth.ts new file mode 100644 index 000000000..df8ae5481 --- /dev/null +++ b/packages/forge/blocks/mistral/auth.ts @@ -0,0 +1,15 @@ +import { option, AuthDefinition } from '@typebot.io/forge' + +export const auth = { + type: 'encryptedCredentials', + name: 'Mistral account', + schema: option.object({ + apiKey: option.string.layout({ + label: 'API key', + isRequired: true, + inputType: 'password', + helperText: + 'You can generate an API key [here](https://console.mistral.ai/user/api-keys/).', + }), + }), +} satisfies AuthDefinition diff --git a/packages/forge/blocks/mistral/helpers/parseMessages.ts b/packages/forge/blocks/mistral/helpers/parseMessages.ts new file mode 100644 index 000000000..c56ecea52 --- /dev/null +++ b/packages/forge/blocks/mistral/helpers/parseMessages.ts @@ -0,0 +1,50 @@ +import { options as createChatCompletionOption } from '../actions/createChatCompletion' +import { ReadOnlyVariableStore } from '@typebot.io/forge' +import { isDefined, isNotEmpty } from '@typebot.io/lib' +import { z } from '@typebot.io/forge/zod' + +export const parseMessages = ({ + options: { messages }, + variables, +}: { + options: Pick, 'messages'> + variables: ReadOnlyVariableStore +}) => + messages + ?.flatMap((message) => { + if (!message.role) return + + if (message.role === 'Dialogue') { + if (!message.dialogueVariableId) return + const dialogue = variables.get(message.dialogueVariableId) ?? [] + const dialogueArr = Array.isArray(dialogue) ? dialogue : [dialogue] + + return dialogueArr.map((dialogueItem, index) => { + if (dialogueItem === null) return + if (index === 0 && message.startsBy === 'assistant') + return { + role: 'assistant', + content: dialogueItem, + } + return { + role: + index % (message.startsBy === 'assistant' ? 1 : 2) === 0 + ? 'user' + : 'assistant', + content: dialogueItem, + } + }) + } + + if (!message.content) return + + return { + role: message.role, + content: variables.parse(message.content), + } + }) + .filter(isDefined) + .filter( + (message) => + isNotEmpty(message?.role) && isNotEmpty(message?.content?.toString()) + ) ?? [] diff --git a/packages/forge/blocks/mistral/index.ts b/packages/forge/blocks/mistral/index.ts new file mode 100644 index 000000000..5469de3de --- /dev/null +++ b/packages/forge/blocks/mistral/index.ts @@ -0,0 +1,14 @@ +import { createBlock } from '@typebot.io/forge' +import { MistralLogo } from './logo' +import { auth } from './auth' +import { createChatCompletion } from './actions/createChatCompletion' + +export const mistral = createBlock({ + id: 'mistral', + name: 'Mistral', + tags: ['ai', 'chat', 'completion'], + LightLogo: MistralLogo, + auth, + actions: [createChatCompletion], + docsUrl: 'https://docs.typebot.io/forge/blocks/mistral', +}) diff --git a/packages/forge/blocks/mistral/logo.tsx b/packages/forge/blocks/mistral/logo.tsx new file mode 100644 index 000000000..95896b9b5 --- /dev/null +++ b/packages/forge/blocks/mistral/logo.tsx @@ -0,0 +1,191 @@ +import React from 'react' + +export const MistralLogo = (props: React.SVGProps) => ( + + Mistral AI + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +) diff --git a/packages/forge/blocks/mistral/package.json b/packages/forge/blocks/mistral/package.json new file mode 100644 index 000000000..658d76ec8 --- /dev/null +++ b/packages/forge/blocks/mistral/package.json @@ -0,0 +1,19 @@ +{ + "name": "@typebot.io/mistral-block", + "version": "1.0.0", + "description": "", + "main": "index.ts", + "keywords": [], + "license": "ISC", + "devDependencies": { + "@typebot.io/forge": "workspace:*", + "@typebot.io/lib": "workspace:*", + "@typebot.io/tsconfig": "workspace:*", + "@types/react": "18.2.15", + "typescript": "5.3.2" + }, + "dependencies": { + "@mistralai/mistralai": "^0.0.10", + "ai": "2.2.31" + } +} diff --git a/packages/forge/blocks/mistral/tsconfig.json b/packages/forge/blocks/mistral/tsconfig.json new file mode 100644 index 000000000..1eb9c7717 --- /dev/null +++ b/packages/forge/blocks/mistral/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@typebot.io/tsconfig/base.json", + "include": ["**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"], + "compilerOptions": { + "lib": ["ESNext", "DOM"], + "noEmit": true, + "jsx": "react" + } +} diff --git a/packages/forge/cli/index.ts b/packages/forge/cli/index.ts index 960d16bbc..afebc81cd 100644 --- a/packages/forge/cli/index.ts +++ b/packages/forge/cli/index.ts @@ -275,7 +275,7 @@ const createAuthFile = async ( apiKey: option.string.layout({ label: 'API key', isRequired: true, - input: 'password', + inputType: 'password', helperText: 'You can generate an API key [here]().', }), diff --git a/packages/forge/repository/index.ts b/packages/forge/repository/index.ts index 5c60ee65a..01c1e612d 100644 --- a/packages/forge/repository/index.ts +++ b/packages/forge/repository/index.ts @@ -5,4 +5,5 @@ export const enabledBlocks = [ 'cal-com', 'chat-node', 'qr-code', + 'mistral', ] as const diff --git a/packages/forge/schemas/index.ts b/packages/forge/schemas/index.ts index 5c3e922fe..840cbabb6 100644 --- a/packages/forge/schemas/index.ts +++ b/packages/forge/schemas/index.ts @@ -1,4 +1,5 @@ // Do not edit this file manually +import { mistral } from '@typebot.io/mistral-block' import { qrCode } from '@typebot.io/qrcode-block' import { chatNode } from '@typebot.io/chat-node-block' import { calCom } from '@typebot.io/cal-com-block' @@ -18,6 +19,7 @@ export const forgedBlocks = [ calCom, chatNode, qrCode, + mistral, ] as BlockDefinition<(typeof enabledBlocks)[number], any, any>[] export type ForgedBlockDefinition = (typeof forgedBlocks)[number] diff --git a/packages/forge/schemas/package.json b/packages/forge/schemas/package.json index 12e8bad88..677099654 100644 --- a/packages/forge/schemas/package.json +++ b/packages/forge/schemas/package.json @@ -13,6 +13,7 @@ "@typebot.io/zemantic-ai-block": "workspace:*", "@typebot.io/cal-com-block": "workspace:*", "@typebot.io/chat-node-block": "workspace:*", - "@typebot.io/qrcode-block": "workspace:*" + "@typebot.io/qrcode-block": "workspace:*", + "@typebot.io/mistral-block": "workspace:*" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d97dd449..a32834c28 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1212,6 +1212,31 @@ importers: specifier: 5.3.2 version: 5.3.2 + packages/forge/blocks/mistral: + dependencies: + '@mistralai/mistralai': + specifier: ^0.0.10 + version: 0.0.10 + ai: + specifier: 2.2.31 + version: 2.2.31(react@18.2.0)(solid-js@1.7.8)(svelte@4.2.1)(vue@3.3.4) + devDependencies: + '@typebot.io/forge': + specifier: workspace:* + version: link:../../core + '@typebot.io/lib': + specifier: workspace:* + version: link:../../../lib + '@typebot.io/tsconfig': + specifier: workspace:* + version: link:../../../tsconfig + '@types/react': + specifier: 18.2.15 + version: 18.2.15 + typescript: + specifier: 5.3.2 + version: 5.3.2 + packages/forge/blocks/openai: dependencies: ai: @@ -1333,6 +1358,9 @@ importers: '@typebot.io/forge-repository': specifier: workspace:* version: link:../repository + '@typebot.io/mistral-block': + specifier: workspace:* + version: link:../blocks/mistral '@typebot.io/openai-block': specifier: workspace:* version: link:../blocks/openai @@ -6344,6 +6372,14 @@ packages: zod-to-json-schema: 3.21.4(zod@3.22.4) dev: true + /@mistralai/mistralai@0.0.10: + resolution: {integrity: sha512-fZOt7A32DcPSff58wTa44pKUBoJBH5toAuzNI9yoM7s5NjTupa1IYcSqqk2LigO8M5EtOEkFsD/XzdyWPnhaRA==} + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + dev: false + /@next/env@14.0.3: resolution: {integrity: sha512-7xRqh9nMvP5xrW4/+L0jgRRX+HoNRGnfJpD+5Wq6/13j3dsdzxO3BCXn7D3hMqsDb+vjZnJq+vI7+EtgrYZTeA==}