2
0

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:
Stergios
2024-03-12 19:53:33 +02:00
committed by GitHub
parent a9daac68ba
commit ecec7023b9
24 changed files with 2065 additions and 1619 deletions

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -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"
]
}
]

View File

@ -18803,6 +18803,7 @@
"dify-ai",
"mistral",
"elevenlabs",
"anthropic",
"together-ai",
"open-router"
]

View File

@ -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"
]

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

View 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

View 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

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

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

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

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

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

View File

@ -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) ??

View File

@ -20,6 +20,7 @@ export const createChatCompletion = createAction({
blockType: 'together-ai',
},
{ blockType: 'mistral' },
{ blockType: 'anthropic' },
],
options: parseChatCompletionOptions({
modelFetchId: 'fetchModels',

View File

@ -27,6 +27,7 @@ export const createChatCompletion = createAction({
blockType: 'together-ai',
},
{ blockType: 'mistral' },
{ blockType: 'anthropic' },
],
fetchers: [
{

View File

@ -22,6 +22,7 @@ export const createChatCompletion = createAction({
blockType: 'open-router',
},
{ blockType: 'mistral' },
{ blockType: 'anthropic' },
],
getSetVariableIds: getChatCompletionSetVarIds,
run: {

View File

@ -8,6 +8,7 @@ export const enabledBlocks = [
'dify-ai',
'mistral',
'elevenlabs',
'anthropic',
'together-ai',
'open-router',
] as const

View File

@ -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>[]

View File

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

File diff suppressed because it is too large Load Diff