2
0

Add Zemantic AI Integration block (#752)

Co-authored-by: Baptiste Arnaud <contact@baptiste-arnaud.fr>
This commit is contained in:
Ed Zynda
2023-09-08 17:21:50 +03:00
committed by GitHub
parent fbb198af9d
commit 75e4b16af0
36 changed files with 1624 additions and 11 deletions

2
.gitignore vendored
View File

@@ -36,4 +36,4 @@ typebotsToFix.json
snapshots
.env
.typebot-build
.typebot-build

View File

@@ -99,9 +99,10 @@ export const NumberInput = <HasVariable extends boolean>({
isRequired={isRequired}
justifyContent="space-between"
width={label ? 'full' : 'auto'}
spacing={0}
>
{label && (
<FormLabel mb="0" flexShrink={0}>
<FormLabel mb="2" flexShrink={0}>
{label}{' '}
{moreInfoTooltip && (
<MoreInfoTooltip>{moreInfoTooltip}</MoreInfoTooltip>

View File

@@ -0,0 +1,47 @@
import { Select } from '@/components/inputs/Select'
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { useToast } from '@/hooks/useToast'
import { trpc } from '@/lib/trpc'
type Props = {
credentialsId: string
blockId: string
defaultValue: string
onChange: (projectId: string | undefined) => void
}
export const ProjectsDropdown = ({
defaultValue,
onChange,
credentialsId,
}: Props) => {
const { typebot } = useTypebot()
const { workspace } = useWorkspace()
const { showToast } = useToast()
const { data } = trpc.zemanticAi.listProjects.useQuery(
{
credentialsId,
workspaceId: workspace?.id as string,
},
{
enabled: !!typebot && !!workspace,
onError: (error) => {
showToast({
description: error.message,
status: 'error',
})
},
}
)
return (
<Select
items={data?.projects as { label: string; value: string }[]}
selectedItem={defaultValue}
onSelect={onChange}
placeholder="Select a project"
/>
)
}

View File

@@ -0,0 +1,37 @@
import { DropdownList } from '@/components/DropdownList'
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { TableListItemProps } from '@/components/TableList'
import { Stack } from '@chakra-ui/react'
import { Variable } from '@typebot.io/schemas'
import {
ZemanticAiOptions,
searchResponseValues,
} from '@typebot.io/schemas/features/blocks/integrations/zemanticAi'
type Props = TableListItemProps<ZemanticAiOptions['responseMapping'][number]>
export const SearchResponseItem = ({ item, onItemChange }: Props) => {
const changeValueToExtract = (
valueToExtract: (typeof searchResponseValues)[number]
) => {
onItemChange({ ...item, valueToExtract })
}
const changeVariableId = (variable: Pick<Variable, 'id'> | undefined) => {
onItemChange({ ...item, variableId: variable ? variable.id : undefined })
}
return (
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
<DropdownList
currentItem={item.valueToExtract ?? 'Summary'}
items={searchResponseValues}
onItemSelect={changeValueToExtract}
/>
<VariableSearchInput
onSelectVariable={changeVariableId}
initialVariableId={item.variableId}
/>
</Stack>
)
}

View File

@@ -0,0 +1,125 @@
import { TextInput } from '@/components/inputs/TextInput'
import { TextLink } from '@/components/TextLink'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { useToast } from '@/hooks/useToast'
import { trpc } from '@/lib/trpc'
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalCloseButton,
ModalBody,
Stack,
ModalFooter,
Button,
} from '@chakra-ui/react'
import React, { useState } from 'react'
const zemanticAIDashboardPage = 'https://zemantic.ai/dashboard/settings'
type Props = {
isOpen: boolean
onClose: () => void
onNewCredentials: (id: string) => void
}
export const ZemanticAiCredentialsModal = ({
isOpen,
onClose,
onNewCredentials,
}: Props) => {
const { workspace } = useWorkspace()
const { showToast } = useToast()
const [apiKey, setApiKey] = useState('')
const [name, setName] = useState('')
const [isCreating, setIsCreating] = useState(false)
const {
credentials: {
listCredentials: { refetch: refetchCredentials },
},
} = trpc.useContext()
const { mutate } = trpc.credentials.createCredentials.useMutation({
onMutate: () => setIsCreating(true),
onSettled: () => setIsCreating(false),
onError: (err) => {
showToast({
description: err.message,
status: 'error',
})
},
onSuccess: (data) => {
refetchCredentials()
onNewCredentials(data.credentialsId)
onClose()
},
})
const createZemanticAiCredentials = async (e: React.FormEvent) => {
e.preventDefault()
if (!workspace) return
mutate({
credentials: {
type: 'zemanticAi',
workspaceId: workspace.id,
name,
data: {
apiKey,
},
},
})
}
return (
<Modal isOpen={isOpen} onClose={onClose} size="lg">
<ModalOverlay />
<ModalContent>
<ModalHeader>Add Zemantic AI account</ModalHeader>
<ModalCloseButton />
<form onSubmit={createZemanticAiCredentials}>
<ModalBody as={Stack} spacing="6">
<TextInput
isRequired
label="Name"
onChange={setName}
placeholder="My account"
withVariableButton={false}
debounceTimeout={0}
/>
<TextInput
isRequired
type="password"
label="API key"
helperText={
<>
You can generate an API key{' '}
<TextLink href={zemanticAIDashboardPage} isExternal>
here
</TextLink>
.
</>
}
onChange={setApiKey}
placeholder="ze..."
withVariableButton={false}
debounceTimeout={0}
/>
</ModalBody>
<ModalFooter>
<Button
type="submit"
isLoading={isCreating}
isDisabled={apiKey === '' || name === ''}
colorScheme="blue"
>
Create
</Button>
</ModalFooter>
</form>
</ModalContent>
</Modal>
)
}

View File

@@ -0,0 +1,21 @@
import { Icon, IconProps } from '@chakra-ui/react'
export const ZemanticAiLogo = (props: IconProps) => (
<Icon viewBox="0 0 24 24" {...props}>
<g transform="matrix(.049281 0 0 .064343 -.27105 -3.4424)">
<path
d="m99.5 205.5v221h-94v-373h94v152z"
fill="#8771b1"
opacity=".991"
/>
<path
d="m284.5 426.5v-221-152h94v373h-94z"
fill="#f05b4e"
opacity=".99"
/>
<path d="m99.5 205.5h93v221h-93v-221z" fill="#ec9896" />
<path d="m192.5 205.5h92v221h-92v-221z" fill="#efe894" />
<path d="m398.5 298.5h94v128h-94v-128z" fill="#46bb91" opacity=".989" />
</g>
</Icon>
)

View File

@@ -0,0 +1,37 @@
import React from 'react'
import { Stack, Text } from '@chakra-ui/react'
import { ZemanticAiOptions } from '@typebot.io/schemas'
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { SetVariableLabel } from '@/components/SetVariableLabel'
type Props = {
options: ZemanticAiOptions
}
export const ZemanticAiNodeBody = ({
options: { query, projectId, responseMapping },
}: Props) => {
const { typebot } = useTypebot()
return (
<Stack>
<Text
color={query && projectId ? 'currentcolor' : 'gray.500'}
noOfLines={1}
>
{query && projectId ? `Ask: ${query}` : 'Configure...'}
</Text>
{typebot &&
responseMapping
.map((mapping) => mapping.variableId)
.map((variableId, idx) =>
variableId ? (
<SetVariableLabel
key={variableId + idx}
variables={typebot.variables}
variableId={variableId}
/>
) : null
)}
</Stack>
)
}

View File

@@ -0,0 +1,185 @@
import { TextInput, Textarea, NumberInput } from '@/components/inputs'
import { CredentialsDropdown } from '@/features/credentials/components/CredentialsDropdown'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import {
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
Stack,
Text,
useDisclosure,
} from '@chakra-ui/react'
import { isEmpty } from '@typebot.io/lib'
import { ZemanticAiBlock } from '@typebot.io/schemas'
import { ZemanticAiCredentialsModal } from './ZemanticAiCredentialsModal'
import { ProjectsDropdown } from './ProjectsDropdown'
import { SearchResponseItem } from './SearchResponseItem'
import { TableList } from '@/components/TableList'
import { createId } from '@paralleldrive/cuid2'
type Props = {
block: ZemanticAiBlock
onOptionsChange: (options: ZemanticAiBlock['options']) => void
}
export const ZemanticAiSettings = ({
block: { id: blockId, options },
onOptionsChange,
}: Props) => {
const { workspace } = useWorkspace()
const { isOpen, onOpen, onClose } = useDisclosure()
const updateCredentialsId = (credentialsId: string | undefined) => {
onOptionsChange({
...options,
credentialsId,
responseMapping: [
{
id: createId(),
valueToExtract: 'Summary',
},
],
})
}
const updateProjectId = (projectId: string | undefined) => {
onOptionsChange({
...options,
projectId: isEmpty(projectId) ? undefined : projectId,
})
}
const updateQuery = (query: string) => {
onOptionsChange({
...options,
query: isEmpty(query) ? undefined : query,
})
}
const updateMaxResults = (
maxResults: number | `{{${string}}}` | undefined
) => {
onOptionsChange({
...options,
maxResults: maxResults as number,
})
}
const updateSystemPrompt = (systemPrompt: string) => {
onOptionsChange({
...options,
systemPrompt: isEmpty(systemPrompt) ? undefined : systemPrompt,
})
}
const updatePrompt = (prompt: string) => {
onOptionsChange({
...options,
prompt: isEmpty(prompt) ? undefined : prompt,
})
}
const updateResponseMapping = (
responseMapping: typeof options.responseMapping
) => {
onOptionsChange({
...options,
responseMapping,
})
}
return (
<Stack spacing={4}>
{workspace && (
<>
<CredentialsDropdown
type="zemanticAi"
workspaceId={workspace.id}
currentCredentialsId={options?.credentialsId}
onCredentialsSelect={updateCredentialsId}
onCreateNewClick={onOpen}
credentialsName="Zemantic AI account"
/>
<ZemanticAiCredentialsModal
isOpen={isOpen}
onClose={onClose}
onNewCredentials={updateCredentialsId}
/>
</>
)}
{options?.credentialsId && (
<>
<ProjectsDropdown
credentialsId={options?.credentialsId as string}
defaultValue={(options?.projectId as string) ?? ''}
onChange={updateProjectId}
blockId={blockId as string}
/>
<TextInput
label="Query:"
moreInfoTooltip="The question you want to ask or search against the documents in the project."
defaultValue={options?.query ?? ''}
onChange={updateQuery}
withVariableButton={true}
placeholder="Content"
/>
<NumberInput
label="Max Results:"
moreInfoTooltip="The maximum number of document chunk results to return from your search."
direction="column"
defaultValue={options?.maxResults}
onValueChange={updateMaxResults}
placeholder="i.e. 3"
w="full"
/>
<Accordion allowMultiple={true}>
<AccordionItem>
<AccordionButton>
<Text w="full" textAlign="left">
Advanced settings
</Text>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4} as={Stack} spacing={6}>
<Textarea
label="System Prompt:"
moreInfoTooltip="System prompt to send to the summarization LLM. This is prepended to the prompt and helps guide system behavior."
defaultValue={options?.systemPrompt ?? ''}
onChange={updateSystemPrompt}
placeholder="System Prompt"
withVariableButton={true}
/>
<Textarea
label="Prompt:"
moreInfoTooltip="Prompt to send to the summarization LLM."
defaultValue={options?.prompt ?? ''}
onChange={updatePrompt}
placeholder="Prompt"
withVariableButton={true}
/>
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<AccordionButton>
<Text w="full" textAlign="left">
Save answer
</Text>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pt="4">
<TableList
initialItems={options.responseMapping ?? []}
Item={SearchResponseItem}
onItemsChange={updateResponseMapping}
newItemDefaultProps={{ valueToExtract: 'Summary' }}
/>
</AccordionPanel>
</AccordionItem>
</Accordion>
</>
)}
</Stack>
)
}

View File

@@ -0,0 +1,106 @@
import prisma from '@/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { z } from 'zod'
import { isReadWorkspaceFobidden } from '@/features/workspace/helpers/isReadWorkspaceFobidden'
import { decrypt } from '@typebot.io/lib/api'
import { ZemanticAiCredentials } from '@typebot.io/schemas/features/blocks/integrations/zemanticAi'
import got from 'got'
export const listProjects = authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/zemantic-ai/projects',
protect: true,
summary: 'List Zemantic AI projects',
tags: ['ZemanticAi'],
},
})
.input(
z.object({
credentialsId: z.string(),
workspaceId: z.string(),
})
)
.output(
z.object({
projects: z.array(
z.object({
label: z.string(),
value: z.string(),
})
),
})
)
.query(async ({ input: { credentialsId, workspaceId }, ctx: { user } }) => {
const workspace = await prisma.workspace.findFirst({
where: { id: workspaceId },
select: {
members: {
select: {
userId: true,
},
},
credentials: {
where: {
id: credentialsId,
},
select: {
id: true,
data: true,
iv: true,
},
},
},
})
if (!workspace || isReadWorkspaceFobidden(workspace, user))
throw new TRPCError({
code: 'NOT_FOUND',
message: 'No workspace found',
})
const credentials = workspace.credentials.at(0)
if (!credentials)
throw new TRPCError({
code: 'NOT_FOUND',
message: 'No credentials found',
})
const data = (await decrypt(
credentials.data,
credentials.iv
)) as ZemanticAiCredentials['data']
const url = 'https://api.zemantic.ai/v1/projects'
try {
const response = await got
.get(url, {
headers: {
Authorization: `Bearer ${data.apiKey}`,
},
})
.json()
const projectsData = response as {
id: string
name: string
}[]
return {
projects: projectsData.map((project) => ({
label: project.name,
value: project.id,
})),
}
} catch (e) {
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Could not list projects',
cause: e,
})
}
})

View File

@@ -0,0 +1,6 @@
import { router } from '@/helpers/server/trpc'
import { listProjects } from './listProjects'
export const zemanticAiRouter = router({
listProjects,
})

View File

@@ -8,7 +8,7 @@ import { smtpCredentialsSchema } from '@typebot.io/schemas/features/blocks/integ
import { encrypt } from '@typebot.io/lib/api/encryption'
import { z } from 'zod'
import { whatsAppCredentialsSchema } from '@typebot.io/schemas/features/whatsapp'
import { Credentials } from '@typebot.io/schemas'
import { Credentials, zemanticAiCredentialsSchema } from '@typebot.io/schemas'
import { isDefined } from '@typebot.io/lib/utils'
import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden'
@@ -37,6 +37,7 @@ export const createCredentials = authenticatedProcedure
googleSheetsCredentialsSchema.pick(inputShape),
openAICredentialsSchema.pick(inputShape),
whatsAppCredentialsSchema.pick(inputShape),
zemanticAiCredentialsSchema.pick(inputShape),
]),
})
)

View File

@@ -8,6 +8,7 @@ import { smtpCredentialsSchema } from '@typebot.io/schemas/features/blocks/integ
import { z } from 'zod'
import { isReadWorkspaceFobidden } from '@/features/workspace/helpers/isReadWorkspaceFobidden'
import { whatsAppCredentialsSchema } from '@typebot.io/schemas/features/whatsapp'
import { zemanticAiCredentialsSchema } from '@typebot.io/schemas/features/blocks/integrations/zemanticAi'
export const listCredentials = authenticatedProcedure
.meta({
@@ -26,7 +27,8 @@ export const listCredentials = authenticatedProcedure
.or(smtpCredentialsSchema.shape.type)
.or(googleSheetsCredentialsSchema.shape.type)
.or(openAICredentialsSchema.shape.type)
.or(whatsAppCredentialsSchema.shape.type),
.or(whatsAppCredentialsSchema.shape.type)
.or(zemanticAiCredentialsSchema.shape.type),
})
)
.output(

View File

@@ -40,6 +40,7 @@ import { TypebotLinkIcon } from '@/features/blocks/logic/typebotLink/components/
import { AbTestIcon } from '@/features/blocks/logic/abTest/components/AbTestIcon'
import { PictureChoiceIcon } from '@/features/blocks/inputs/pictureChoice/components/PictureChoiceIcon'
import { PixelLogo } from '@/features/blocks/integrations/pixel/components/PixelLogo'
import { ZemanticAiLogo } from '@/features/blocks/integrations/zemanticAi/ZemanticAiLogo'
type BlockIconProps = { type: BlockType } & IconProps
@@ -118,6 +119,8 @@ export const BlockIcon = ({ type, ...props }: BlockIconProps): JSX.Element => {
return <OpenAILogo fill={openAIColor} {...props} />
case IntegrationBlockType.PIXEL:
return <PixelLogo {...props} />
case IntegrationBlockType.ZEMANTIC_AI:
return <ZemanticAiLogo {...props} />
case 'start':
return <FlagIcon {...props} />
}

View File

@@ -84,5 +84,7 @@ export const BlockLabel = ({ type }: Props): JSX.Element => {
return <Text fontSize="sm">{scopedT('openai.label')}</Text>
case IntegrationBlockType.PIXEL:
return <Text fontSize="sm">{scopedT('pixel.label')}</Text>
case IntegrationBlockType.ZEMANTIC_AI:
return <Text fontSize="sm">{scopedT('zemanticAi.label')}</Text>
}
}

View File

@@ -43,6 +43,7 @@ import { AbTestNodeBody } from '@/features/blocks/logic/abTest/components/AbTest
import { PictureChoiceNode } from '@/features/blocks/inputs/pictureChoice/components/PictureChoiceNode'
import { PixelNodeBody } from '@/features/blocks/integrations/pixel/components/PixelNodeBody'
import { useScopedI18n } from '@/locales'
import { ZemanticAiNodeBody } from '@/features/blocks/integrations/zemanticAi/ZemanticAiNodeBody'
type Props = {
block: Block | StartBlock
@@ -200,6 +201,9 @@ export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
case IntegrationBlockType.PIXEL: {
return <PixelNodeBody options={block.options} />
}
case IntegrationBlockType.ZEMANTIC_AI: {
return <ZemanticAiNodeBody options={block.options} />
}
case 'start': {
return <Text>{scopedT('text')}</Text>
}

View File

@@ -47,6 +47,7 @@ import { AbTestSettings } from '@/features/blocks/logic/abTest/components/AbTest
import { PictureChoiceSettings } from '@/features/blocks/inputs/pictureChoice/components/PictureChoiceSettings'
import { SettingsHoverBar } from './SettingsHoverBar'
import { PixelSettings } from '@/features/blocks/integrations/pixel/components/PixelSettings'
import { ZemanticAiSettings } from '@/features/blocks/integrations/zemanticAi/ZemanticAiSettings'
type Props = {
block: BlockWithOptions
@@ -314,5 +315,10 @@ export const BlockSettings = ({
/>
)
}
case IntegrationBlockType.ZEMANTIC_AI: {
return (
<ZemanticAiSettings block={block} onOptionsChange={updateOptions} />
)
}
}
}

View File

@@ -63,5 +63,7 @@ export const getHelpDocUrl = (blockType: BlockWithOptions['type']): string => {
return 'https://docs.typebot.io/editor/blocks/logic/jump'
case IntegrationBlockType.PIXEL:
return 'https://docs.typebot.io/editor/blocks/integrations/pixel'
case IntegrationBlockType.ZEMANTIC_AI:
return 'https://docs.typebot.io/editor/blocks/integrations/zemantic-ai'
}
}

View File

@@ -44,6 +44,7 @@ import {
defaultAbTestOptions,
BlockWithItems,
defaultTypebotLinkOptions,
zemanticAiDefaultOptions,
} from '@typebot.io/schemas'
import { defaultPictureChoiceOptions } from '@typebot.io/schemas/features/blocks/inputs/pictureChoice'
@@ -143,6 +144,8 @@ const parseDefaultBlockOptions = (type: BlockWithOptionsType): BlockOptions => {
return {}
case IntegrationBlockType.PIXEL:
return {}
case IntegrationBlockType.ZEMANTIC_AI:
return zemanticAiDefaultOptions
}
}

View File

@@ -16,6 +16,7 @@ import { customDomainsRouter } from '@/features/customDomains/api/router'
import { whatsAppRouter } from '@/features/whatsapp/router'
import { openAIRouter } from '@/features/blocks/integrations/openai/api/router'
import { generateUploadUrl } from '@/features/upload/api/generateUploadUrl'
import { zemanticAiRouter } from '@/features/blocks/integrations/zemanticAi/api/router'
export const trpcRouter = router({
getAppVersionProcedure,
@@ -35,6 +36,7 @@ export const trpcRouter = router({
whatsApp: whatsAppRouter,
openAI: openAIRouter,
generateUploadUrl,
zemanticAi: zemanticAiRouter,
})
export type AppRouter = typeof trpcRouter

View File

@@ -251,6 +251,7 @@ export default {
'editor.sidebarBlock.chatwoot.label': 'Chatwoot',
'editor.sidebarBlock.openai.label': 'OpenAI',
'editor.sidebarBlock.pixel.label': 'Pixel',
'editor.sidebarBlock.zemanticAi.label': 'Zemantic AI',
'editor.blockCard.bubbleBlock.tooltip.label':
'Ein PDF, ein iframe, eine Website einbetten...',
'editor.blockCard.inputBlock.tooltip.files.label': 'Dateien hochladen',

View File

@@ -245,6 +245,7 @@ export default {
'editor.sidebarBlock.chatwoot.label': 'Chatwoot',
'editor.sidebarBlock.openai.label': 'OpenAI',
'editor.sidebarBlock.pixel.label': 'Pixel',
'editor.sidebarBlock.zemanticAi.label': 'Zemantic AI',
'editor.blockCard.bubbleBlock.tooltip.label':
'Embed a pdf, an iframe, a website...',
'editor.blockCard.inputBlock.tooltip.files.label': 'Upload Files',

View File

@@ -251,6 +251,7 @@ export default {
'editor.sidebarBlock.chatwoot.label': 'Chatwoot',
'editor.sidebarBlock.openai.label': 'OpenAI',
'editor.sidebarBlock.pixel.label': 'Pixel',
'editor.sidebarBlock.zemanticAi.label': 'Zemantic AI',
'editor.blockCard.bubbleBlock.tooltip.label':
'Intégrer un pdf, un iframe, un site web...',
'editor.blockCard.inputBlock.tooltip.files.label': 'Télécharger des fichiers',

View File

@@ -252,6 +252,7 @@ export default {
'editor.sidebarBlock.chatwoot.label': 'Chatwoot',
'editor.sidebarBlock.openai.label': 'OpenAI',
'editor.sidebarBlock.pixel.label': 'Pixel',
'editor.sidebarBlock.zemanticAi.label': 'Zemantic AI',
'editor.blockCard.bubbleBlock.tooltip.label':
'Incorporar pdf, iframe, website...',
'editor.blockCard.inputBlock.tooltip.files.label': 'Carregar Ficheiros',

View File

@@ -253,6 +253,7 @@ export default {
'editor.sidebarBlock.chatwoot.label': 'Chatwoot',
'editor.sidebarBlock.openai.label': 'OpenAI',
'editor.sidebarBlock.pixel.label': 'Pixel',
'editor.sidebarBlock.zemanticAi.label': 'Zemantic AI',
'editor.blockCard.bubbleBlock.tooltip.label':
'Incorporar pdf, iframe, website...',
'editor.blockCard.inputBlock.tooltip.files.label': 'Carregar Ficheiros',

View File

@@ -0,0 +1,44 @@
# Zemantic AI
With the Zemantic AI block, you can search and retrieve results or LLM summaries from your documents stored on Zemantic AI.
<img
src="/img/blocks/integrations/zemanticAi/overview.png"
width="600"
alt="OpenAI block"
/>
## Settings
This integration requires a Zemantic AI account. If you don't have one yet, you can create one [here](https://zemantic.ai/).
The block has the following settings:
<img
src="/img/blocks/integrations/zemanticAi/settings.png"
width="600"
alt="OpenAI messages sequence"
/>
- Zemantic AI account: create or select the Zemantic AI credentials you want to use.
- Project ID: The project id of the project containing the documents you want to search
- Question or Query: The question you want to ask your Zemantic AI documents
- Max Results: The maximum number of results you want to retrieve
### Prompt Settings
- System Prompt: The prompt you want to use to guide the LLM behavior
- Prompt: The prompt you want to use to summarize your documents
## Troobleshooting
### Error message: "Zemantic AI block returned error"
It means your Zemantic AI block is not configured properly. Please check the following:
- You have selected an Zemantic AI account
- You have filled out the Question or Query field
### It returns an empty message
Either you misconfigured the block or your may have have gone over the context limits of the LLM. You can try lowering the number of results to retrieve or shortening your prompt.

View File

@@ -3906,6 +3906,88 @@
"options"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"groupId": {
"type": "string"
},
"outgoingEdgeId": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"Zemantic AI"
]
},
"blockId": {
"type": "string"
},
"options": {
"type": "object",
"properties": {
"credentialsId": {
"type": "string"
},
"projectId": {
"type": "string"
},
"systemPrompt": {
"type": "string"
},
"prompt": {
"type": "string"
},
"query": {
"type": "string"
},
"maxResults": {
"type": "integer"
},
"responseMapping": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"valueToExtract": {
"type": "string",
"enum": [
"Summary",
"Results"
]
},
"variableId": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
}
},
"required": [
"responseMapping"
],
"additionalProperties": false
}
},
"required": [
"id",
"groupId",
"type",
"options"
],
"additionalProperties": false
}
]
}
@@ -8211,6 +8293,88 @@
"options"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"groupId": {
"type": "string"
},
"outgoingEdgeId": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"Zemantic AI"
]
},
"blockId": {
"type": "string"
},
"options": {
"type": "object",
"properties": {
"credentialsId": {
"type": "string"
},
"projectId": {
"type": "string"
},
"systemPrompt": {
"type": "string"
},
"prompt": {
"type": "string"
},
"query": {
"type": "string"
},
"maxResults": {
"type": "integer"
},
"responseMapping": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"valueToExtract": {
"type": "string",
"enum": [
"Summary",
"Results"
]
},
"variableId": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
}
},
"required": [
"responseMapping"
],
"additionalProperties": false
}
},
"required": [
"id",
"groupId",
"type",
"options"
],
"additionalProperties": false
}
]
}
@@ -12151,6 +12315,88 @@
"options"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"groupId": {
"type": "string"
},
"outgoingEdgeId": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"Zemantic AI"
]
},
"blockId": {
"type": "string"
},
"options": {
"type": "object",
"properties": {
"credentialsId": {
"type": "string"
},
"projectId": {
"type": "string"
},
"systemPrompt": {
"type": "string"
},
"prompt": {
"type": "string"
},
"query": {
"type": "string"
},
"maxResults": {
"type": "integer"
},
"responseMapping": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"valueToExtract": {
"type": "string",
"enum": [
"Summary",
"Results"
]
},
"variableId": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
}
},
"required": [
"responseMapping"
],
"additionalProperties": false
}
},
"required": [
"id",
"groupId",
"type",
"options"
],
"additionalProperties": false
}
]
}
@@ -16231,6 +16477,88 @@
"options"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"groupId": {
"type": "string"
},
"outgoingEdgeId": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"Zemantic AI"
]
},
"blockId": {
"type": "string"
},
"options": {
"type": "object",
"properties": {
"credentialsId": {
"type": "string"
},
"projectId": {
"type": "string"
},
"systemPrompt": {
"type": "string"
},
"prompt": {
"type": "string"
},
"query": {
"type": "string"
},
"maxResults": {
"type": "integer"
},
"responseMapping": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"valueToExtract": {
"type": "string",
"enum": [
"Summary",
"Results"
]
},
"variableId": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
}
},
"required": [
"responseMapping"
],
"additionalProperties": false
}
},
"required": [
"id",
"groupId",
"type",
"options"
],
"additionalProperties": false
}
]
}
@@ -20191,6 +20519,88 @@
"options"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"groupId": {
"type": "string"
},
"outgoingEdgeId": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"Zemantic AI"
]
},
"blockId": {
"type": "string"
},
"options": {
"type": "object",
"properties": {
"credentialsId": {
"type": "string"
},
"projectId": {
"type": "string"
},
"systemPrompt": {
"type": "string"
},
"prompt": {
"type": "string"
},
"query": {
"type": "string"
},
"maxResults": {
"type": "integer"
},
"responseMapping": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"valueToExtract": {
"type": "string",
"enum": [
"Summary",
"Results"
]
},
"variableId": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
}
},
"required": [
"responseMapping"
],
"additionalProperties": false
}
},
"required": [
"id",
"groupId",
"type",
"options"
],
"additionalProperties": false
}
]
}
@@ -24206,6 +24616,88 @@
"options"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"groupId": {
"type": "string"
},
"outgoingEdgeId": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"Zemantic AI"
]
},
"blockId": {
"type": "string"
},
"options": {
"type": "object",
"properties": {
"credentialsId": {
"type": "string"
},
"projectId": {
"type": "string"
},
"systemPrompt": {
"type": "string"
},
"prompt": {
"type": "string"
},
"query": {
"type": "string"
},
"maxResults": {
"type": "integer"
},
"responseMapping": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"valueToExtract": {
"type": "string",
"enum": [
"Summary",
"Results"
]
},
"variableId": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
}
},
"required": [
"responseMapping"
],
"additionalProperties": false
}
},
"required": [
"id",
"groupId",
"type",
"options"
],
"additionalProperties": false
}
]
}
@@ -28284,6 +28776,88 @@
"options"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"groupId": {
"type": "string"
},
"outgoingEdgeId": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"Zemantic AI"
]
},
"blockId": {
"type": "string"
},
"options": {
"type": "object",
"properties": {
"credentialsId": {
"type": "string"
},
"projectId": {
"type": "string"
},
"systemPrompt": {
"type": "string"
},
"prompt": {
"type": "string"
},
"query": {
"type": "string"
},
"maxResults": {
"type": "integer"
},
"responseMapping": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"valueToExtract": {
"type": "string",
"enum": [
"Summary",
"Results"
]
},
"variableId": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
}
},
"required": [
"responseMapping"
],
"additionalProperties": false
}
},
"required": [
"id",
"groupId",
"type",
"options"
],
"additionalProperties": false
}
]
}
@@ -30536,6 +31110,42 @@
"name"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"data": {
"type": "object",
"properties": {
"apiKey": {
"type": "string"
}
},
"required": [
"apiKey"
],
"additionalProperties": false
},
"type": {
"type": "string",
"enum": [
"zemanticAi"
]
},
"workspaceId": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"data",
"type",
"workspaceId",
"name"
],
"additionalProperties": false
}
]
}
@@ -30607,15 +31217,25 @@
{
"anyOf": [
{
"type": "string",
"enum": [
"stripe"
"anyOf": [
{
"type": "string",
"enum": [
"stripe"
]
},
{
"type": "string",
"enum": [
"smtp"
]
}
]
},
{
"type": "string",
"enum": [
"smtp"
"google sheets"
]
}
]
@@ -30623,7 +31243,7 @@
{
"type": "string",
"enum": [
"google sheets"
"openai"
]
}
]
@@ -30631,7 +31251,7 @@
{
"type": "string",
"enum": [
"openai"
"whatsApp"
]
}
]
@@ -30639,7 +31259,7 @@
{
"type": "string",
"enum": [
"whatsApp"
"zemanticAi"
]
}
]
@@ -32356,6 +32976,78 @@
}
}
}
},
"/zemantic-ai/projects": {
"get": {
"operationId": "zemanticAi-listProjects",
"summary": "List Zemantic AI projects",
"tags": [
"ZemanticAi"
],
"security": [
{
"Authorization": []
}
],
"parameters": [
{
"name": "credentialsId",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "workspaceId",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"projects": {
"type": "array",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"label",
"value"
],
"additionalProperties": false
}
}
},
"required": [
"projects"
],
"additionalProperties": false
}
}
}
},
"default": {
"$ref": "#/components/responses/error"
}
}
}
}
},
"components": {

View File

@@ -3489,6 +3489,88 @@
"options"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"groupId": {
"type": "string"
},
"outgoingEdgeId": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"Zemantic AI"
]
},
"blockId": {
"type": "string"
},
"options": {
"type": "object",
"properties": {
"credentialsId": {
"type": "string"
},
"projectId": {
"type": "string"
},
"systemPrompt": {
"type": "string"
},
"prompt": {
"type": "string"
},
"query": {
"type": "string"
},
"maxResults": {
"type": "integer"
},
"responseMapping": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"valueToExtract": {
"type": "string",
"enum": [
"Summary",
"Results"
]
},
"variableId": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
}
},
"required": [
"responseMapping"
],
"additionalProperties": false
}
},
"required": [
"id",
"groupId",
"type",
"options"
],
"additionalProperties": false
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

View File

@@ -0,0 +1,129 @@
import { ExecuteIntegrationResponse } from '@/features/chat/types'
import prisma from '@/lib/prisma'
import { SessionState } from '@typebot.io/schemas'
import {
ZemanticAiBlock,
ZemanticAiCredentials,
ZemanticAiResponse,
} from '@typebot.io/schemas/features/blocks/integrations/zemanticAi'
import got from 'got'
import { decrypt } from '@typebot.io/lib/api/encryption'
import { byId, isDefined, isEmpty } from '@typebot.io/lib'
import { updateVariables } from '@/features/variables/updateVariables'
import { getDefinedVariables, parseAnswers } from '@typebot.io/lib/results'
const URL = 'https://api.zemantic.ai/v1/search-documents'
export const executeZemanticAiBlock = async (
state: SessionState,
block: ZemanticAiBlock
): Promise<ExecuteIntegrationResponse> => {
let newSessionState = state
const noCredentialsError = {
status: 'error',
description: 'Make sure to select a Zemantic AI account',
}
const zemanticRequestError = {
status: 'error',
description: 'Could not execute Zemantic AI request',
}
const credentials = await prisma.credentials.findUnique({
where: {
id: block.options.credentialsId,
},
})
if (!credentials) {
console.error('Could not find credentials in database')
return {
outgoingEdgeId: block.outgoingEdgeId,
logs: [noCredentialsError],
}
}
const { apiKey } = (await decrypt(
credentials.data,
credentials.iv
)) as ZemanticAiCredentials['data']
const { typebot, answers } = newSessionState.typebotsQueue[0]
const templateVars = parseAnswers({
variables: getDefinedVariables(typebot.variables),
answers: answers,
})
try {
const res: ZemanticAiResponse = await got
.post(URL, {
headers: {
Authorization: `Bearer ${apiKey}`,
},
json: {
projectId: block.options.projectId,
query: replaceTemplateVars(
block.options.query as string,
templateVars
),
maxResults: block.options.maxResults,
summarize: true,
summaryOptions: {
system_prompt:
replaceTemplateVars(
block.options.systemPrompt as string,
templateVars
) ?? '',
prompt:
replaceTemplateVars(
block.options.prompt as string,
templateVars
) ?? '',
},
},
})
.json()
for (const r of block.options.responseMapping || []) {
const variable = typebot.variables.find(byId(r.variableId))
switch (r.valueToExtract) {
case 'Summary':
if (isDefined(variable) && !isEmpty(res.summary)) {
newSessionState = updateVariables(newSessionState)([
{ ...variable, value: res.summary },
])
}
break
case 'Results':
if (isDefined(variable) && res.results.length) {
newSessionState = updateVariables(newSessionState)([
{ ...variable, value: JSON.stringify(res.results) },
])
}
break
default:
break
}
}
} catch (e) {
console.error(e)
return {
outgoingEdgeId: block.outgoingEdgeId,
logs: [zemanticRequestError],
}
}
return { outgoingEdgeId: block.outgoingEdgeId, newSessionState }
}
const replaceTemplateVars = (
template: string,
vars: Record<string, string>
) => {
if (!template) return
let result = template
for (const [key, value] of Object.entries(vars)) {
result = result.replaceAll(`{{${key}}}`, value)
}
return result
}

View File

@@ -5,6 +5,7 @@ import { executeChatwootBlock } from '@/features/blocks/integrations/chatwoot/ex
import { executeGoogleAnalyticsBlock } from '@/features/blocks/integrations/googleAnalytics/executeGoogleAnalyticsBlock'
import { executeGoogleSheetBlock } from '@/features/blocks/integrations/googleSheets/executeGoogleSheetBlock'
import { executePixelBlock } from '@/features/blocks/integrations/pixel/executePixelBlock'
import { executeZemanticAiBlock } from '@/features/blocks/integrations/zemanticAi/executeZemanticAiBlock'
import {
IntegrationBlock,
IntegrationBlockType,
@@ -33,5 +34,7 @@ export const executeIntegration =
return executeOpenAIBlock(state, block)
case IntegrationBlockType.PIXEL:
return executePixelBlock(state, block)
case IntegrationBlockType.ZEMANTIC_AI:
return executeZemanticAiBlock(state, block)
}
}

View File

@@ -9,4 +9,5 @@ export enum IntegrationBlockType {
PABBLY_CONNECT = 'Pabbly',
CHATWOOT = 'Chatwoot',
PIXEL = 'Pixel',
ZEMANTIC_AI = 'Zemantic AI',
}

View File

@@ -9,3 +9,4 @@ export * from './webhook/schemas'
export * from './zapier'
export * from './pixel/schemas'
export * from './pixel/constants'
export * from './zemanticAi'

View File

@@ -0,0 +1,62 @@
import { z } from 'zod'
import { blockBaseSchema, credentialsBaseSchema } from '../baseSchemas'
import { IntegrationBlockType } from './enums'
export const searchResponseValues = ['Summary', 'Results'] as const
export const zemanticAiOptionsSchema = z.object({
credentialsId: z.string().optional(),
projectId: z.string().optional(),
systemPrompt: z.string().optional(),
prompt: z.string().optional(),
query: z.string().optional(),
maxResults: z.number().int().optional(),
responseMapping: z.array(
z.object({
id: z.string(),
valueToExtract: z.preprocess(
(val) => (!val ? 'Summary' : val),
z.enum(searchResponseValues)
),
variableId: z.string().optional(),
})
),
})
export const zemanticAiBlockSchema = blockBaseSchema.merge(
z.object({
type: z.enum([IntegrationBlockType.ZEMANTIC_AI]),
blockId: z.string().optional(),
options: zemanticAiOptionsSchema,
})
)
export const zemanticAiCredentialsSchema = z
.object({
type: z.literal('zemanticAi'),
data: z.object({
apiKey: z.string(),
}),
})
.merge(credentialsBaseSchema)
export const zemanticSearchResponseSchema = z.object({
results: z.array(
z.object({
documentId: z.string(),
text: z.string(),
score: z.number(),
})
),
summary: z.string(),
})
export const zemanticAiDefaultOptions: ZemanticAiOptions = {
maxResults: 3,
responseMapping: [],
}
export type ZemanticAiResponse = z.infer<typeof zemanticSearchResponseSchema>
export type ZemanticAiCredentials = z.infer<typeof zemanticAiCredentialsSchema>
export type ZemanticAiOptions = z.infer<typeof zemanticAiOptionsSchema>
export type ZemanticAiBlock = z.infer<typeof zemanticAiBlockSchema>

View File

@@ -48,6 +48,7 @@ import {
import { jumpBlockSchema } from './logic/jump'
import { pictureChoiceBlockSchema } from './inputs/pictureChoice'
import { Item } from '../items'
import { zemanticAiBlockSchema } from './integrations/zemanticAi'
export type DraggableBlock =
| BubbleBlock
@@ -127,6 +128,7 @@ export const blockSchema = z.discriminatedUnion('type', [
webhookBlockSchema,
zapierBlockSchema,
pixelBlockSchema,
zemanticAiBlockSchema,
])
export type Block = z.infer<typeof blockSchema>

View File

@@ -4,6 +4,7 @@ import { googleSheetsCredentialsSchema } from './blocks/integrations/googleSheet
import { openAICredentialsSchema } from './blocks/integrations/openai'
import { smtpCredentialsSchema } from './blocks/integrations/sendEmail'
import { whatsAppCredentialsSchema } from './whatsapp'
import { zemanticAiCredentialsSchema } from './blocks'
export const credentialsSchema = z.discriminatedUnion('type', [
smtpCredentialsSchema,
@@ -11,6 +12,7 @@ export const credentialsSchema = z.discriminatedUnion('type', [
stripeCredentialsSchema,
openAICredentialsSchema,
whatsAppCredentialsSchema,
zemanticAiCredentialsSchema,
])
export type Credentials = z.infer<typeof credentialsSchema>