✨ Add Anthropic block (#1336)
Hello @baptisteArno, As we discussed in issue #1315 we created a basic implementation of Anthropic’s Claude AI block. This block is based on the OpenAI block and shares a similar structure. The most notable changes in this PR are: - Added the Claude AI block. - Added relevant documentation for the new block. - Formatted some other source files in order to pass git pre-hook checks. Some notes to be made: - Currently there is no way to dynamically fetch the model’s versions since there is no endpoint provided by the SDK. - All pre version-3 Claude models are hard-coded constant variables. - We have opened an issue for that on the SDK repository [here](https://github.com/anthropics/anthropic-sdk-typescript/issues/313). - We can implement in a new PR Claude’s new [Vision system](https://docs.anthropic.com/claude/docs/vision) which allows for image analysis and understanding. - This can be done in a later phase, given that you agree of course. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced the Anthropic block for creating chat messages with Claude AI in Typebot. - Added functionality to create chat messages using Anthropic AI SDK with configurable options. - Implemented encrypted credentials for Anthropic account integration. - Added constants and helpers for better handling of chat messages with Anthropic models. - Included Anthropic block in the list of enabled and forged blocks for broader access. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Retr0-01 <contact@retr0.dev> Co-authored-by: Baptiste Arnaud <baptiste.arnaud95@gmail.com> Co-authored-by: Baptiste Arnaud <contact@baptiste-arnaud.fr>
This commit is contained in:
41
apps/docs/editor/blocks/integrations/anthropic.mdx
Normal file
41
apps/docs/editor/blocks/integrations/anthropic.mdx
Normal file
@ -0,0 +1,41 @@
|
||||
---
|
||||
title: Anthropic
|
||||
---
|
||||
|
||||
## Create Message
|
||||
|
||||
With the Anthropic block, you can create chat messages based on your user queries and display the answer back to your typebot using Claude AI.
|
||||
|
||||
<Frame>
|
||||
<img
|
||||
src="/images/blocks/integrations/anthropic/overview.png"
|
||||
alt="Anthropic block"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
Similarly to the OpenAI block, this integration comes with a convenient message type called **Dialogue**. It allows you to easily pass a sequence of saved assistant / user messages history to Claude AI:
|
||||
|
||||
<Frame>
|
||||
<img
|
||||
src="/images/blocks/integrations/anthropic/append-to-history.png"
|
||||
alt="Claude AI messages sequence"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
Then you can give the Claude AI block access to this sequence of messages:
|
||||
|
||||
<Frame>
|
||||
<img
|
||||
src="/images/blocks/integrations/anthropic/dialogue-usage.png"
|
||||
alt="Claude AI messages sequence"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
Finally, save the response of the assistant to a variable in order to append it in the chat history and also display it on your typebot.
|
||||
|
||||
<Frame>
|
||||
<img
|
||||
src="/images/blocks/integrations/anthropic/assistant-message.png"
|
||||
alt="Claude AI assistant message variable"
|
||||
/>
|
||||
</Frame>
|
Binary file not shown.
After Width: | Height: | Size: 42 KiB |
Binary file not shown.
After Width: | Height: | Size: 36 KiB |
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
BIN
apps/docs/images/blocks/integrations/anthropic/overview.png
Normal file
BIN
apps/docs/images/blocks/integrations/anthropic/overview.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
@ -124,7 +124,8 @@
|
||||
"editor/blocks/integrations/openai",
|
||||
"editor/blocks/integrations/zemantic-ai",
|
||||
"editor/blocks/integrations/mistral",
|
||||
"editor/blocks/integrations/elevenlabs"
|
||||
"editor/blocks/integrations/elevenlabs",
|
||||
"editor/blocks/integrations/anthropic"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@ -18803,6 +18803,7 @@
|
||||
"dify-ai",
|
||||
"mistral",
|
||||
"elevenlabs",
|
||||
"anthropic",
|
||||
"together-ai",
|
||||
"open-router"
|
||||
]
|
||||
|
@ -128,8 +128,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -639,8 +638,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -9414,6 +9412,7 @@
|
||||
"dify-ai",
|
||||
"mistral",
|
||||
"elevenlabs",
|
||||
"anthropic",
|
||||
"together-ai",
|
||||
"open-router"
|
||||
]
|
||||
|
165
packages/forge/blocks/anthropic/actions/createChatMessage.tsx
Normal file
165
packages/forge/blocks/anthropic/actions/createChatMessage.tsx
Normal file
@ -0,0 +1,165 @@
|
||||
import { createAction, option } from '@typebot.io/forge'
|
||||
import { auth } from '../auth'
|
||||
import { Anthropic } from '@anthropic-ai/sdk'
|
||||
import { AnthropicStream } from 'ai'
|
||||
import { anthropicModels, defaultAnthropicOptions } from '../constants'
|
||||
import { parseChatMessages } from '../helpers/parseChatMessages'
|
||||
import { isDefined } from '@typebot.io/lib'
|
||||
|
||||
const nativeMessageContentSchema = {
|
||||
content: option.string.layout({
|
||||
inputType: 'textarea',
|
||||
placeholder: 'Content',
|
||||
}),
|
||||
}
|
||||
|
||||
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.enum(anthropicModels).layout({
|
||||
defaultValue: defaultAnthropicOptions.model,
|
||||
}),
|
||||
messages: option
|
||||
.array(
|
||||
option.discriminatedUnion('role', [
|
||||
userMessageItemSchema,
|
||||
assistantMessageItemSchema,
|
||||
dialogueMessageItemSchema,
|
||||
])
|
||||
)
|
||||
.layout({ accordion: 'Messages', itemLabel: 'message', isOrdered: true }),
|
||||
systemMessage: option.string.layout({
|
||||
accordion: 'Advanced Settings',
|
||||
label: 'System prompt',
|
||||
direction: 'row',
|
||||
inputType: 'textarea',
|
||||
}),
|
||||
temperature: option.number.layout({
|
||||
accordion: 'Advanced Settings',
|
||||
label: 'Temperature',
|
||||
direction: 'row',
|
||||
defaultValue: defaultAnthropicOptions.temperature,
|
||||
}),
|
||||
maxTokens: option.number.layout({
|
||||
accordion: 'Advanced Settings',
|
||||
label: 'Max Tokens',
|
||||
direction: 'row',
|
||||
defaultValue: defaultAnthropicOptions.maxTokens,
|
||||
}),
|
||||
responseMapping: option
|
||||
.saveResponseArray(['Message Content'] as const)
|
||||
.layout({
|
||||
accordion: 'Save Response',
|
||||
}),
|
||||
})
|
||||
|
||||
export const createChatMessage = createAction({
|
||||
name: 'Create Chat Message',
|
||||
auth,
|
||||
options,
|
||||
turnableInto: [
|
||||
{
|
||||
blockType: 'mistral',
|
||||
},
|
||||
{
|
||||
blockType: 'openai',
|
||||
},
|
||||
{ blockType: 'open-router' },
|
||||
{ blockType: 'together-ai' },
|
||||
],
|
||||
getSetVariableIds: ({ responseMapping }) =>
|
||||
responseMapping?.map((res) => res.variableId).filter(isDefined) ?? [],
|
||||
run: {
|
||||
server: async ({ credentials: { apiKey }, options, variables, logs }) => {
|
||||
const client = new Anthropic({
|
||||
apiKey: apiKey,
|
||||
})
|
||||
|
||||
const messages = parseChatMessages({ options, variables })
|
||||
|
||||
try {
|
||||
const reply = await client.messages.create({
|
||||
messages,
|
||||
model: options.model ?? defaultAnthropicOptions.model,
|
||||
system: options.systemMessage,
|
||||
temperature: options.temperature
|
||||
? Number(options.temperature)
|
||||
: undefined,
|
||||
max_tokens: options.maxTokens
|
||||
? Number(options.maxTokens)
|
||||
: defaultAnthropicOptions.maxTokens,
|
||||
})
|
||||
|
||||
messages.push(reply)
|
||||
|
||||
options.responseMapping?.forEach((mapping) => {
|
||||
if (!mapping.variableId) return
|
||||
|
||||
if (!mapping.item || mapping.item === 'Message Content')
|
||||
variables.set(mapping.variableId, reply.content[0].text)
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof Anthropic.APIError) {
|
||||
logs.add({
|
||||
status: 'error',
|
||||
description: `${error.status} ${error.name}`,
|
||||
details: error.message,
|
||||
})
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
},
|
||||
stream: {
|
||||
getStreamVariableId: (options) =>
|
||||
options.responseMapping?.find(
|
||||
(res) => res.item === 'Message Content' || !res.item
|
||||
)?.variableId,
|
||||
run: async ({ credentials: { apiKey }, options, variables }) => {
|
||||
const client = new Anthropic({
|
||||
apiKey: apiKey,
|
||||
})
|
||||
|
||||
const messages = parseChatMessages({ options, variables })
|
||||
|
||||
const response = await client.messages.create({
|
||||
messages,
|
||||
model: options.model ?? defaultAnthropicOptions.model,
|
||||
system: options.systemMessage,
|
||||
temperature: options.temperature
|
||||
? Number(options.temperature)
|
||||
: undefined,
|
||||
max_tokens: options.maxTokens
|
||||
? Number(options.maxTokens)
|
||||
: defaultAnthropicOptions.maxTokens,
|
||||
stream: true,
|
||||
})
|
||||
|
||||
return AnthropicStream(response)
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
17
packages/forge/blocks/anthropic/auth.ts
Normal file
17
packages/forge/blocks/anthropic/auth.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { option, AuthDefinition } from '@typebot.io/forge'
|
||||
|
||||
export const auth = {
|
||||
type: 'encryptedCredentials',
|
||||
name: 'Anthropic 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.anthropic.com/settings/keys).',
|
||||
placeholder: 'sk-...',
|
||||
withVariableButton: false,
|
||||
}),
|
||||
}),
|
||||
} satisfies AuthDefinition
|
12
packages/forge/blocks/anthropic/constants.ts
Normal file
12
packages/forge/blocks/anthropic/constants.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export const anthropicModels = [
|
||||
'claude-3-opus-20240229',
|
||||
'claude-2.1',
|
||||
'claude-2.0',
|
||||
'claude-instant-1.2',
|
||||
] as const
|
||||
|
||||
export const defaultAnthropicOptions = {
|
||||
model: anthropicModels[0],
|
||||
temperature: 1,
|
||||
maxTokens: 1024,
|
||||
} as const
|
52
packages/forge/blocks/anthropic/helpers/parseChatMessages.ts
Normal file
52
packages/forge/blocks/anthropic/helpers/parseChatMessages.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { Anthropic } from '@anthropic-ai/sdk'
|
||||
import { options as createMessageOptions } from '../actions/createChatMessage'
|
||||
import { ReadOnlyVariableStore } from '@typebot.io/forge'
|
||||
import { isNotEmpty } from '@typebot.io/lib'
|
||||
import { z } from '@typebot.io/forge/zod'
|
||||
|
||||
export const parseChatMessages = ({
|
||||
options: { messages },
|
||||
variables,
|
||||
}: {
|
||||
options: Pick<z.infer<typeof createMessageOptions>, 'messages'>
|
||||
variables: ReadOnlyVariableStore
|
||||
}): Anthropic.Messages.MessageParam[] => {
|
||||
const parsedMessages = 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 (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),
|
||||
} satisfies Anthropic.Messages.MessageParam
|
||||
})
|
||||
.filter(
|
||||
(message) =>
|
||||
isNotEmpty(message?.role) && isNotEmpty(message?.content?.toString())
|
||||
) as Anthropic.Messages.MessageParam[]
|
||||
|
||||
return parsedMessages
|
||||
}
|
13
packages/forge/blocks/anthropic/index.ts
Normal file
13
packages/forge/blocks/anthropic/index.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { createBlock } from '@typebot.io/forge'
|
||||
import { AnthropicLogo } from './logo'
|
||||
import { auth } from './auth'
|
||||
import { createChatMessage } from './actions/createChatMessage'
|
||||
|
||||
export const anthropic = createBlock({
|
||||
id: 'anthropic',
|
||||
name: 'Anthropic',
|
||||
tags: ['ai', 'chat', 'completion', 'claude', 'anthropic'],
|
||||
LightLogo: AnthropicLogo,
|
||||
auth,
|
||||
actions: [createChatMessage],
|
||||
})
|
7
packages/forge/blocks/anthropic/logo.tsx
Normal file
7
packages/forge/blocks/anthropic/logo.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from 'react'
|
||||
|
||||
export const AnthropicLogo = (props: React.SVGProps<SVGSVGElement>) => (
|
||||
<svg viewBox="0 0 16 16" fill="currentColor" {...props}>
|
||||
<path d="M11.54 2H9.09l4.46 12H16L11.54 2ZM4.46 2 0 14h2.5l.9-2.52h4.68L8.99 14h2.5L7.02 2H4.46Zm-.24 7.25 1.52-4.22 1.53 4.22H4.22Z"></path>
|
||||
</svg>
|
||||
)
|
20
packages/forge/blocks/anthropic/package.json
Normal file
20
packages/forge/blocks/anthropic/package.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "@typebot.io/anthropic-block",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.ts",
|
||||
"keywords": [],
|
||||
"author": "Enchatted P.C.",
|
||||
"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": {
|
||||
"@anthropic-ai/sdk": "^0.16.1",
|
||||
"ai": "2.2.33"
|
||||
}
|
||||
}
|
10
packages/forge/blocks/anthropic/tsconfig.json
Normal file
10
packages/forge/blocks/anthropic/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "@typebot.io/tsconfig/base.json",
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"],
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"noEmit": true,
|
||||
"jsx": "react"
|
||||
}
|
||||
}
|
@ -75,6 +75,7 @@ export const createChatCompletion = createAction({
|
||||
blockType: 'together-ai',
|
||||
},
|
||||
{ blockType: 'open-router' },
|
||||
{ blockType: 'anthropic' },
|
||||
],
|
||||
getSetVariableIds: (options) =>
|
||||
options.responseMapping?.map((res) => res.variableId).filter(isDefined) ??
|
||||
|
@ -20,6 +20,7 @@ export const createChatCompletion = createAction({
|
||||
blockType: 'together-ai',
|
||||
},
|
||||
{ blockType: 'mistral' },
|
||||
{ blockType: 'anthropic' },
|
||||
],
|
||||
options: parseChatCompletionOptions({
|
||||
modelFetchId: 'fetchModels',
|
||||
|
@ -27,6 +27,7 @@ export const createChatCompletion = createAction({
|
||||
blockType: 'together-ai',
|
||||
},
|
||||
{ blockType: 'mistral' },
|
||||
{ blockType: 'anthropic' },
|
||||
],
|
||||
fetchers: [
|
||||
{
|
||||
|
@ -22,6 +22,7 @@ export const createChatCompletion = createAction({
|
||||
blockType: 'open-router',
|
||||
},
|
||||
{ blockType: 'mistral' },
|
||||
{ blockType: 'anthropic' },
|
||||
],
|
||||
getSetVariableIds: getChatCompletionSetVarIds,
|
||||
run: {
|
||||
|
@ -8,6 +8,7 @@ export const enabledBlocks = [
|
||||
'dify-ai',
|
||||
'mistral',
|
||||
'elevenlabs',
|
||||
'anthropic',
|
||||
'together-ai',
|
||||
'open-router',
|
||||
] as const
|
||||
|
@ -1,4 +1,5 @@
|
||||
// Do not edit this file manually
|
||||
import { anthropic } from '@typebot.io/anthropic-block'
|
||||
import { openRouter } from '@typebot.io/open-router-block'
|
||||
import { togetherAi } from '@typebot.io/together-ai-block'
|
||||
import { elevenlabs } from '@typebot.io/elevenlabs-block'
|
||||
@ -26,6 +27,7 @@ export const forgedBlocks = [
|
||||
difyAi,
|
||||
mistral,
|
||||
elevenlabs,
|
||||
anthropic,
|
||||
togetherAi,
|
||||
openRouter,
|
||||
] as BlockDefinition<(typeof enabledBlocks)[number], any, any>[]
|
||||
|
@ -17,6 +17,7 @@
|
||||
"@typebot.io/dify-ai-block": "workspace:*",
|
||||
"@typebot.io/mistral-block": "workspace:*",
|
||||
"@typebot.io/elevenlabs-block": "workspace:*",
|
||||
"@typebot.io/anthropic-block": "workspace:*",
|
||||
"@typebot.io/together-ai-block": "workspace:*",
|
||||
"@typebot.io/open-router-block": "workspace:*"
|
||||
}
|
||||
|
3328
pnpm-lock.yaml
generated
3328
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user