From 17893a474c96456ab896b98e91b5f6566fdccd0a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 28 Aug 2024 09:04:24 +0900 Subject: [PATCH 01/16] Bump aws-cdk from 2.147.3 to 2.148.1 in /cdk (#506) Bumps [aws-cdk](https://github.com/aws/aws-cdk/tree/HEAD/packages/aws-cdk) from 2.147.3 to 2.148.1. - [Release notes](https://github.com/aws/aws-cdk/releases) - [Changelog](https://github.com/aws/aws-cdk/blob/main/CHANGELOG.v2.md) - [Commits](https://github.com/aws/aws-cdk/commits/v2.148.1/packages/aws-cdk) --- updated-dependencies: - dependency-name: aws-cdk dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- cdk/package-lock.json | 9 +++++---- cdk/package.json | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/cdk/package-lock.json b/cdk/package-lock.json index 43e7d5114..9f03796c5 100644 --- a/cdk/package-lock.json +++ b/cdk/package-lock.json @@ -26,7 +26,7 @@ "@aws-prototyping-sdk/pdk-nag": "^0.19.68", "@types/jest": "^29.5.3", "@types/node": "20.4.2", - "aws-cdk": "2.147.3", + "aws-cdk": "2.148.1", "jest": "^29.6.1", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", @@ -1388,9 +1388,9 @@ } }, "node_modules/aws-cdk": { - "version": "2.147.3", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.147.3.tgz", - "integrity": "sha512-Y+o2eONCMQ2xwLpeUevwOeShxhlmW42Qx1uBWreF9fKzCeFkZ/in66FlaCBKlLXtKPQOdhmWNWKJ9A+pE+L+5A==", + "version": "2.148.1", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.148.1.tgz", + "integrity": "sha512-wiAi4vFJ52A42PpU3zRi2gVDqbTXSBVFrqKRqEd8wYL1mqa0qMv9FR35NsgbM1RL9s7g5ZljYvl+G2tXpcp5Eg==", "dev": true, "bin": { "cdk": "bin/cdk" @@ -2141,6 +2141,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, "engines": { "node": ">=0.10.0" } diff --git a/cdk/package.json b/cdk/package.json index 24af57e09..e3797130b 100644 --- a/cdk/package.json +++ b/cdk/package.json @@ -15,7 +15,7 @@ "@aws-prototyping-sdk/pdk-nag": "^0.19.68", "@types/jest": "^29.5.3", "@types/node": "20.4.2", - "aws-cdk": "2.147.3", + "aws-cdk": "2.148.1", "jest": "^29.6.1", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", From 668f3dea14179a985ec2adbd85729dc02a545e4b Mon Sep 17 00:00:00 2001 From: Chikako Oyanagi Date: Wed, 28 Aug 2024 10:55:55 +0900 Subject: [PATCH 02/16] implement BotEditPageV2 with KB supported --- cdk/bin/bedrock-chat.ts | 4 + cdk/lib/bedrock-chat-stack.ts | 3 + cdk/lib/constructs/frontend.ts | 4 + frontend/src/@types/bot.d.ts | 12 +- frontend/src/components/Slider.stories.tsx | 28 + frontend/src/components/Slider.tsx | 3 + .../components/DescriptiveSelect.tsx | 105 ++ .../features/knowledgeBase/constants/index.ts | 70 + .../knowledgeBase/pages/BotEditPageV2.tsx | 1288 +++++++++++++++++ .../features/knowledgeBase/types/index.d.ts | 42 + frontend/src/i18n/en/index.ts | 67 + frontend/src/i18n/ja/index.ts | 61 + frontend/src/pages/AdminBotManagementPage.tsx | 26 +- frontend/src/pages/BotApiSettingsPage.tsx | 14 +- frontend/src/pages/BotEditPage.tsx | 8 +- frontend/src/pages/BotExplorePage.tsx | 76 +- frontend/src/pages/ChatPage.tsx | 59 +- frontend/src/routes.tsx | 7 +- frontend/src/vite-env.d.ts | 1 + 19 files changed, 1844 insertions(+), 34 deletions(-) create mode 100644 frontend/src/features/knowledgeBase/components/DescriptiveSelect.tsx create mode 100644 frontend/src/features/knowledgeBase/constants/index.ts create mode 100644 frontend/src/features/knowledgeBase/pages/BotEditPageV2.tsx create mode 100644 frontend/src/features/knowledgeBase/types/index.d.ts diff --git a/cdk/bin/bedrock-chat.ts b/cdk/bin/bedrock-chat.ts index c6d35457f..2f1ed6ff3 100644 --- a/cdk/bin/bedrock-chat.ts +++ b/cdk/bin/bedrock-chat.ts @@ -38,6 +38,9 @@ const RDS_SCHEDULES: CronScheduleProps = app.node.tryGetContext("rdbSchedules"); const ENABLE_MISTRAL: boolean = app.node.tryGetContext("enableMistral"); const SELF_SIGN_UP_ENABLED: boolean = app.node.tryGetContext("selfSignUpEnabled"); +const ENABLE_KB: boolean = app.node.tryGetContext( + "useBedrockKnowledgeBaseForRag" +); // container size of embedding ecs tasks const EMBEDDING_CONTAINER_VCPU: number = app.node.tryGetContext( @@ -81,6 +84,7 @@ const chat = new BedrockChatStack(app, `BedrockChatStack`, { autoJoinUserGroups: AUTO_JOIN_USER_GROUPS, rdsSchedules: RDS_SCHEDULES, enableMistral: ENABLE_MISTRAL, + enableKB: ENABLE_KB, embeddingContainerVcpu: EMBEDDING_CONTAINER_VCPU, embeddingContainerMemory: EMBEDDING_CONTAINER_MEMORY, selfSignUpEnabled: SELF_SIGN_UP_ENABLED, diff --git a/cdk/lib/bedrock-chat-stack.ts b/cdk/lib/bedrock-chat-stack.ts index 390c8b447..291082657 100644 --- a/cdk/lib/bedrock-chat-stack.ts +++ b/cdk/lib/bedrock-chat-stack.ts @@ -36,6 +36,7 @@ export interface BedrockChatStackProps extends StackProps { readonly autoJoinUserGroups: string[]; readonly rdsSchedules: CronScheduleProps; readonly enableMistral: boolean; + readonly enableKB: boolean; readonly embeddingContainerVcpu: number; readonly embeddingContainerMemory: number; readonly selfSignUpEnabled: boolean; @@ -145,6 +146,7 @@ export class BedrockChatStack extends cdk.Stack { accessLogBucket, webAclId: props.webAclId, enableMistral: props.enableMistral, + enableKB: props.enableKB, enableIpV6: props.enableIpV6, }); @@ -212,6 +214,7 @@ export class BedrockChatStack extends cdk.Stack { webSocketApiEndpoint: websocket.apiEndpoint, userPoolDomainPrefix: props.userPoolDomainPrefix, enableMistral: props.enableMistral, + enableKB: props.enableKB, auth, idp, }); diff --git a/cdk/lib/constructs/frontend.ts b/cdk/lib/constructs/frontend.ts index 19e5fb8f2..b4a6d0214 100644 --- a/cdk/lib/constructs/frontend.ts +++ b/cdk/lib/constructs/frontend.ts @@ -18,6 +18,7 @@ import { NagSuppressions } from "cdk-nag"; export interface FrontendProps { readonly webAclId: string; readonly enableMistral: boolean; + readonly enableKB: boolean; readonly accessLogBucket?: IBucket; readonly enableIpV6: boolean; } @@ -100,6 +101,7 @@ export class Frontend extends Construct { webSocketApiEndpoint, userPoolDomainPrefix, enableMistral, + enableKB, auth, idp, }: { @@ -107,6 +109,7 @@ export class Frontend extends Construct { webSocketApiEndpoint: string; userPoolDomainPrefix: string; enableMistral: boolean; + enableKB: boolean; auth: Auth; idp: Idp; }) { @@ -119,6 +122,7 @@ export class Frontend extends Construct { VITE_APP_USER_POOL_ID: auth.userPool.userPoolId, VITE_APP_USER_POOL_CLIENT_ID: auth.client.userPoolClientId, VITE_APP_ENABLE_MISTRAL: enableMistral.toString(), + VITE_APP_ENABLE_KB: enableKB.toString(), VITE_APP_REGION: region, VITE_APP_USE_STREAMING: "true", }; diff --git a/frontend/src/@types/bot.d.ts b/frontend/src/@types/bot.d.ts index c771b3828..a63657ada 100644 --- a/frontend/src/@types/bot.d.ts +++ b/frontend/src/@types/bot.d.ts @@ -1,3 +1,5 @@ +import { BedrockKnowledgeBase } from '../features/knowledgeBase/types'; + export type BotKind = 'private' | 'mixed'; export type BotMeta = { @@ -45,6 +47,7 @@ export type BotSyncStatus = 'QUEUED' | 'RUNNING' | 'SUCCEEDED' | 'FAILED'; export type BotListItem = BotMeta & { available: boolean; + hasBedrockKnowledgeBase?: boolean; }; export type GenerationParams = { @@ -69,11 +72,13 @@ export type BotDetails = BotMeta & { syncStatusReason: string; displayRetrievedChunks: boolean; conversationQuickStarters: ConversationQuickStarter[]; + bedrockKnowledgeBase: BedrockKnowledgeBase | null; }; export type BotSummary = BotMeta & { hasKnowledge: boolean; hasAgent: boolean; + ownedAndHasBedrockKnowledgeBase: boolean; conversationQuickStarters: ConversationQuickStarter[]; }; @@ -90,12 +95,13 @@ export type RegisterBotRequest = { instruction: string; agent: AgentInput; description?: string; - embeddingParams?: EmdeddingParams; + embeddingParams?: EmdeddingParams | null; generationParams?: GenerationParams; searchParams?: SearchParams; knowledge?: BotKnowledge; displayRetrievedChunks: boolean; conversationQuickStarters: ConversationQuickStarter[]; + bedrockKnowledgeBase?: BedrockKnowledgeBase; }; export type RegisterBotResponse = BotDetails; @@ -105,12 +111,13 @@ export type UpdateBotRequest = { instruction: string; description?: string; agent: AgentInput; - embeddingParams?: EmdeddingParams; + embeddingParams?: EmdeddingParams | null; generationParams?: BotGenerationConfig; searchParams?: SearchParams; knowledge?: BotKnowledgeDiff; displayRetrievedChunks: boolean; conversationQuickStarters: ConversationQuickStarter[]; + bedrockKnowledgeBase?: BedrockKnowledgeBase; }; export type UpdateBotResponse = { @@ -124,6 +131,7 @@ export type UpdateBotResponse = { knowledge?: BotKnowledge; displayRetrievedChunks: boolean; conversationQuickStarters: ConversationQuickStarter[]; + bedrockKnowledgeBase: BedrockKnowledgeBase; }; export type UpdateBotPinnedRequest = { diff --git a/frontend/src/components/Slider.stories.tsx b/frontend/src/components/Slider.stories.tsx index 938dc16e1..8e225aff3 100644 --- a/frontend/src/components/Slider.stories.tsx +++ b/frontend/src/components/Slider.stories.tsx @@ -31,6 +31,34 @@ export const Ideal = () => { ); }; +export const IdealDisabled = () => { + const { t } = useTranslation(); + const [embeddingParams, setEmbeddingParams] = useState({ + chunkSize: DEFAULT_EMBEDDING_CONFIG.chunkSize, + chunkOverlap: DEFAULT_EMBEDDING_CONFIG.chunkOverlap, + enablePartitionPdf: DEFAULT_EMBEDDING_CONFIG.enablePartitionPdf, + }); + return ( + + setEmbeddingParams((params) => ({ + ...params, + chunkSize: chunkSize, + })) + } + disabled={true} + /> + ); +}; + export const Error = () => { const { t } = useTranslation(); const [embeddingParams, setEmbeddingParams] = useState({ diff --git a/frontend/src/components/Slider.tsx b/frontend/src/components/Slider.tsx index 9dd31131f..d82e564a3 100644 --- a/frontend/src/components/Slider.tsx +++ b/frontend/src/components/Slider.tsx @@ -11,6 +11,7 @@ interface Props { step: number; }; onChange: Dispatch; + disabled?: boolean; errorMessage?: string; enableDecimal?: boolean; } @@ -52,6 +53,7 @@ export const Slider: FC = (props) => { step={props.range.step} value={props.value} onChange={handleChange} + disabled={props.disabled} /> = (props) => { max={props.range.max} min={props.range.min} onChange={handleChange} + disabled={props.disabled} /> {props.hint && !props.errorMessage && ( diff --git a/frontend/src/features/knowledgeBase/components/DescriptiveSelect.tsx b/frontend/src/features/knowledgeBase/components/DescriptiveSelect.tsx new file mode 100644 index 000000000..89742d8bf --- /dev/null +++ b/frontend/src/features/knowledgeBase/components/DescriptiveSelect.tsx @@ -0,0 +1,105 @@ +import { Fragment, useCallback, useMemo } from 'react'; +import { Listbox, Transition } from '@headlessui/react'; +import { PiCaretUpDown, PiCheck, PiX } from 'react-icons/pi'; +import ButtonIcon from '../../../components/ButtonIcon'; +import { BaseProps } from '../../../@types/common'; +import { twMerge } from 'tailwind-merge'; + +type Props = BaseProps & { + label?: string; + value: string; + options: { + value: string; + label: string; + description?: string; + }[]; + clearable?: boolean; + disabled?: boolean; + onChange: (value: string) => void; +}; + +const DescriptiveSelect: React.FC = (props) => { + const selectedLabel = useMemo(() => { + return props.options.find((o) => o.value === props.value)?.label ?? ''; + }, [props.options, props.value]); + + const onClear = useCallback(() => { + props.onChange(''); + }, [props]); + + return ( +
+ {props.label && ( +
+ {props.label} +
+ )} + +
+ + {selectedLabel} + + + + + + {props.clearable && props.value !== '' && ( + + + + + + )} + + + {props.options.map((option, idx) => ( + + `relative cursor-default select-none py-2 pl-10 pr-4 ${ + active + ? 'bg-aws-smile/10 text-aws-smile' + : 'text-aws-font-color' + }` + } + value={option.value}> + {({ selected }) => ( + <> + + {option.label} + + {option.description} + + + {selected ? ( + + + + ) : null} + + )} + + ))} + + +
+
+
+ ); +}; + +export default DescriptiveSelect; diff --git a/frontend/src/features/knowledgeBase/constants/index.ts b/frontend/src/features/knowledgeBase/constants/index.ts new file mode 100644 index 000000000..98a7d263f --- /dev/null +++ b/frontend/src/features/knowledgeBase/constants/index.ts @@ -0,0 +1,70 @@ +import { BedrockKnowledgeBase, OpenSearchParams, SearchParams } from '../types'; + +export const OPENSEARCH_ANALYZER: { + [key: string]: OpenSearchParams; +} = { + icu: { + analyzer: { + characterFilters: ['icu_normalizer'], + tokenizer: 'icu_tokenizer', + tokenFilters: ['icu_folding'], + }, + } as OpenSearchParams, + kuromoji: { + analyzer: { + characterFilters: ['icu_normalizer'], + tokenizer: 'kuromoji_tokenizer', + tokenFilters: [ + 'kuromoji_baseform', + 'kuromoji_part_of_speech', + 'kuromoji_stemmer', + 'cjk_width', + 'ja_stop', + 'lowercase', + 'icu_folding', + ], + }, + } as OpenSearchParams, +} as const; + +export const DEFAULT_OPENSEARCH_ANALYZER: { + [key: string]: string; +} = { + en: 'icu', + ja: 'kuromoji', +} as const; + +export const DEFAULT_BEDROCK_KNOWLEDGEBASE: BedrockKnowledgeBase = { + embeddingsModel: 'cohere_multilingual_v3', + openSearch: OPENSEARCH_ANALYZER['icu'], + chunkingStrategy: 'default', + maxTokens: null, + overlapPercentage: null, + searchParams: { + maxResults: 20, + searchType: 'hybrid', + }, +}; + +export const DEFAULT_CHUNKING_MAX_TOKENS = 300; +export const DEFAULT_CHUNKING_OVERLAP_PERCENTAGE = 20; + +export const EDGE_CHUNKING_MAX_TOKENS = { + MAX: { + titan_v1: 8192, + cohere_multilingual_v3: 512, + }, + MIN: 20, + STEP: 1, +}; + +export const EDGE_CHUNKING_OVERLAP_PERCENTAGE = { + MAX: 100, + MIN: 0, + STEP: 1, +}; + +export const DEFAULT_SEARCH_CONFIG: SearchParams = { + maxResults: 20, + searchType: 'hybrid', +}; diff --git a/frontend/src/features/knowledgeBase/pages/BotEditPageV2.tsx b/frontend/src/features/knowledgeBase/pages/BotEditPageV2.tsx new file mode 100644 index 000000000..f90b04cfb --- /dev/null +++ b/frontend/src/features/knowledgeBase/pages/BotEditPageV2.tsx @@ -0,0 +1,1288 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import InputText from '../../../components/InputText'; +import Button from '../../../components/Button'; +import useBot from '../../../hooks/useBot'; +import { useNavigate, useParams } from 'react-router-dom'; +import { PiCaretLeft, PiNote, PiPlus, PiTrash } from 'react-icons/pi'; +import Textarea from '../../../components/Textarea'; +import DialogInstructionsSamples from '../../../components/DialogInstructionsSamples'; +import ButtonIcon from '../../../components/ButtonIcon'; +import { produce } from 'immer'; +import Alert from '../../../components/Alert'; +import KnowledgeFileUploader from '../../../components/KnowledgeFileUploader'; +import GenerationConfig from '../../../components/GenerationConfig'; +import Select from '../../../components/Select'; +import { BotFile, ConversationQuickStarter } from '../../../@types/bot'; + +import { ulid } from 'ulid'; +import { + EDGE_GENERATION_PARAMS, + EDGE_MISTRAL_GENERATION_PARAMS, + DEFAULT_GENERATION_CONFIG, + DEFAULT_MISTRAL_GENERATION_CONFIG, + EDGE_SEARCH_PARAMS, + TooltipDirection, +} from '../../../constants'; +import { Slider } from '../../../components/Slider'; +import ExpandableDrawerGroup from '../../../components/ExpandableDrawerGroup'; +import useErrorMessage from '../../../hooks/useErrorMessage'; +import Help from '../../../components/Help'; +import Toggle from '../../../components/Toggle'; +import { useAgent } from '../../../features/agent/hooks/useAgent'; +import { AgentTool } from '../../../features/agent/types'; +import { AvailableTools } from '../../../features/agent/components/AvailableTools'; +import DescriptiveSelect from '../components/DescriptiveSelect'; +import { + DEFAULT_CHUNKING_MAX_TOKENS, + DEFAULT_CHUNKING_OVERLAP_PERCENTAGE, + EDGE_CHUNKING_MAX_TOKENS, + EDGE_CHUNKING_OVERLAP_PERCENTAGE, + OPENSEARCH_ANALYZER, + DEFAULT_SEARCH_CONFIG, + DEFAULT_OPENSEARCH_ANALYZER, +} from '../constants'; +import { + ChunkingStrategy, + EmbeddingsModel, + OpenSearchParams, + SearchParams, + SearchType, +} from '../types'; + +const edgeGenerationParams = + import.meta.env.VITE_APP_ENABLE_MISTRAL === 'true' + ? EDGE_MISTRAL_GENERATION_PARAMS + : EDGE_GENERATION_PARAMS; + +const defaultGenerationConfig = + import.meta.env.VITE_APP_ENABLE_MISTRAL === 'true' + ? DEFAULT_MISTRAL_GENERATION_CONFIG + : DEFAULT_GENERATION_CONFIG; + +const BotEditPageV2: React.FC = () => { + const { i18n, t } = useTranslation(); + const navigate = useNavigate(); + const { botId: paramsBotId } = useParams(); + const { getMyBot, registerBot, updateBot } = useBot(); + const { availableTools } = useAgent(); + + const [isLoading, setIsLoading] = useState(false); + + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [instruction, setInstruction] = useState(''); + const [urls, setUrls] = useState(['']); + const [s3Urls, setS3Urls] = useState(['']); + const [files, setFiles] = useState([]); + const [addedFilenames, setAddedFilenames] = useState([]); + const [unchangedFilenames, setUnchangedFilenames] = useState([]); + const [deletedFilenames, setDeletedFilenames] = useState([]); + const [displayRetrievedChunks, setDisplayRetrievedChunks] = useState(true); + const [maxTokens, setMaxTokens] = useState( + defaultGenerationConfig.maxTokens + ); + const [topK, setTopK] = useState(defaultGenerationConfig.topK); + const [topP, setTopP] = useState(defaultGenerationConfig.topP); + const [temperature, setTemperature] = useState( + defaultGenerationConfig.temperature + ); + const [stopSequences, setStopSequences] = useState( + defaultGenerationConfig.stopSequences?.join(',') || '' + ); + const [tools, setTools] = useState([]); + const [conversationQuickStarters, setConversationQuickStarters] = useState< + ConversationQuickStarter[] + >([ + { + title: '', + example: '', + }, + ]); + + const [embeddingsModel, setEmbeddingsModel] = + useState('titan_v1'); + + const embeddingsModelOptions: { + label: string; + value: EmbeddingsModel; + }[] = [ + { + label: t('knowledgeBaseSettings.embeddingModel.titan_v1.label'), + value: 'titan_v1', + }, + { + label: t( + 'knowledgeBaseSettings.embeddingModel.cohere_multilingual_v3.label' + ), + value: 'cohere_multilingual_v3', + }, + ]; + + const [chunkingStrategy, setChunkingStrategy] = + useState('default'); + + const chunkingStrategyOptions: { + label: string; + value: ChunkingStrategy; + description: string; + }[] = [ + { + label: t('knowledgeBaseSettings.chunkingStrategy.default.label'), + value: 'default', + description: t('knowledgeBaseSettings.chunkingStrategy.default.hint'), + }, + { + label: t('knowledgeBaseSettings.chunkingStrategy.fixed_size.label'), + value: 'fixed_size', + description: t('knowledgeBaseSettings.chunkingStrategy.fixed_size.hint'), + }, + { + label: t('knowledgeBaseSettings.chunkingStrategy.none.label'), + value: 'none', + description: t('knowledgeBaseSettings.chunkingStrategy.none.hint'), + }, + ]; + + const [chunkingMaxTokens, setChunkingMaxTokens] = useState( + DEFAULT_CHUNKING_MAX_TOKENS + ); + + const [chunkingOverlapPercentage, setChunkingOverlapPercentage] = + useState(DEFAULT_CHUNKING_OVERLAP_PERCENTAGE); + + const [analyzer, setAnalyzer] = useState( + DEFAULT_OPENSEARCH_ANALYZER[i18n.language] ?? 'icu' + ); + + const [openSearchParams, setOpenSearchParams] = useState( + DEFAULT_OPENSEARCH_ANALYZER[i18n.language] + ? OPENSEARCH_ANALYZER[DEFAULT_OPENSEARCH_ANALYZER[i18n.language]] + : OPENSEARCH_ANALYZER['icu'] + ); + + const analyzerOptions: { + label: string; + value: string; + description: string; + }[] = [ + { + label: t('knowledgeBaseSettings.opensearchAnalyzer.icu.label'), + value: 'icu', + description: t('knowledgeBaseSettings.opensearchAnalyzer.icu.hint', { + tokenizer: OPENSEARCH_ANALYZER['icu'].analyzer.tokenizer, + normalizer: OPENSEARCH_ANALYZER['icu'].analyzer.characterFilters, + }), + }, + { + label: t('knowledgeBaseSettings.opensearchAnalyzer.kuromoji.label'), + value: 'kuromoji', + description: t('knowledgeBaseSettings.opensearchAnalyzer.kuromoji.hint', { + tokenizer: OPENSEARCH_ANALYZER['kuromoji'].analyzer.tokenizer, + normalizer: OPENSEARCH_ANALYZER['icu'].analyzer.characterFilters, + }), + }, + ]; + + const [searchParams, setSearchParams] = useState( + DEFAULT_SEARCH_CONFIG + ); + + const searchTypeOptions: { + label: string; + value: SearchType; + description: string; + }[] = [ + { + label: t('searchSettings.searchType.hybrid.label'), + value: 'hybrid', + description: t('searchSettings.searchType.hybrid.hint'), + }, + { + label: t('searchSettings.searchType.semantic.label'), + value: 'semantic', + description: t('searchSettings.searchType.semantic.hint'), + }, + ]; + + const { + errorMessages, + setErrorMessage: setErrorMessages, + clearAll: clearErrorMessages, + } = useErrorMessage(); + + const isNewBot = useMemo(() => { + return paramsBotId ? false : true; + }, [paramsBotId]); + + const botId = useMemo(() => { + return isNewBot ? ulid() : (paramsBotId ?? ''); + }, [isNewBot, paramsBotId]); + + useEffect(() => { + if (!isNewBot) { + setIsLoading(true); + getMyBot(botId) + .then((bot) => { + // Disallow editing of bots created under opposite VITE_APP_ENABLE_KB environment state + if (!bot.bedrockKnowledgeBase) { + navigate('/'); + return; + } + + setTools(bot.agent.tools); + setTitle(bot.title); + setDescription(bot.description); + setInstruction(bot.instruction); + setUrls( + bot.knowledge.sourceUrls.length === 0 + ? [''] + : bot.knowledge.sourceUrls + ); + setS3Urls( + bot.knowledge.s3Urls.length === 0 ? [''] : bot.knowledge.s3Urls + ); + setFiles( + bot.knowledge.filenames.map((filename) => ({ + filename, + status: 'UPLOADED', + })) + ); + setTopK(bot.generationParams.topK); + setTopP(bot.generationParams.topP); + setTemperature(bot.generationParams.temperature); + setMaxTokens(bot.generationParams.maxTokens); + setStopSequences(bot.generationParams.stopSequences.join(',')); + setUnchangedFilenames([...bot.knowledge.filenames]); + setDisplayRetrievedChunks(bot.displayRetrievedChunks); + if (bot.syncStatus === 'FAILED') { + setErrorMessages( + isSyncChunkError(bot.syncStatusReason) + ? 'syncChunkError' + : 'syncError', + bot.syncStatusReason + ); + } + setConversationQuickStarters( + bot.conversationQuickStarters.length > 0 + ? bot.conversationQuickStarters + : [ + { + title: '', + example: '', + }, + ] + ); + setEmbeddingsModel(bot.bedrockKnowledgeBase!.embeddingsModel); + setChunkingStrategy(bot.bedrockKnowledgeBase!.chunkingStrategy); + setChunkingMaxTokens( + bot.bedrockKnowledgeBase!.maxTokens ?? DEFAULT_CHUNKING_MAX_TOKENS + ); + setChunkingOverlapPercentage( + bot.bedrockKnowledgeBase!.overlapPercentage ?? + DEFAULT_CHUNKING_OVERLAP_PERCENTAGE + ); + setOpenSearchParams(bot.bedrockKnowledgeBase!.openSearch); + setSearchParams(bot.bedrockKnowledgeBase!.searchParams); + }) + .finally(() => { + setIsLoading(false); + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isNewBot, botId]); + + const isSyncChunkError = useCallback((syncErrorMessage: string) => { + const pattern = + /Got a larger chunk overlap \(\d+\) than chunk size \(\d+\), should be smaller\./; + return pattern.test(syncErrorMessage); + }, []); + + const onChangeS3Url = useCallback( + (s3Url: string, idx: number) => { + setS3Urls( + produce(s3Urls, (draft) => { + draft[idx] = s3Url; + }) + ); + }, + [s3Urls] + ); + + const onClickAddS3Url = useCallback(() => { + setS3Urls([...s3Urls, '']); + }, [s3Urls]); + + const onClickRemoveS3Url = useCallback( + (idx: number) => { + setS3Urls( + produce(s3Urls, (draft) => { + draft.splice(idx, 1); + if (draft.length === 0) { + draft.push(''); + } + return; + }) + ); + }, + [s3Urls] + ); + + const removeUnchangedFilenames = useCallback( + (filename: string) => { + const idx = unchangedFilenames.findIndex( + (unchangedFilename) => unchangedFilename === filename + ); + if (idx > -1) { + setUnchangedFilenames( + produce(unchangedFilenames, (draft) => { + draft.splice(idx, 1); + return; + }) + ); + } + }, + [unchangedFilenames] + ); + + const removeAddedFilenames = useCallback( + (filename: string) => { + const idx = addedFilenames.findIndex( + (addedFilename) => addedFilename === filename + ); + if (idx > -1) { + setAddedFilenames( + produce(addedFilenames, (draft) => { + draft.splice(idx, 1); + return; + }) + ); + } + }, + [addedFilenames] + ); + + const removeDeletedFilenames = useCallback( + (filename: string) => { + const idx = deletedFilenames.findIndex( + (deletedFilename) => deletedFilename === filename + ); + if (idx > -1) { + setDeletedFilenames( + produce(deletedFilenames, (draft) => { + draft.splice(idx, 1); + }) + ); + } + }, + [deletedFilenames] + ); + + const onAddFiles = useCallback( + (botFiles: BotFile[]) => { + setFiles(botFiles); + setAddedFilenames( + produce(addedFilenames, (draft) => { + botFiles.forEach((file) => { + if (file.status === 'UPLOADING') { + if (!draft.includes(file.filename)) { + draft.push(file.filename); + } + removeUnchangedFilenames(file.filename); + removeDeletedFilenames(file.filename); + } + }); + }) + ); + }, + [addedFilenames, removeDeletedFilenames, removeUnchangedFilenames] + ); + + const onUpdateFiles = useCallback((botFiles: BotFile[]) => { + setFiles(botFiles); + }, []); + + const onDeleteFiles = useCallback( + (botFiles: BotFile[], deletedFilename: string) => { + setFiles(botFiles); + + if (!deletedFilenames.includes(deletedFilename)) { + setDeletedFilenames( + produce(deletedFilenames, (draft) => { + draft.push(deletedFilename); + }) + ); + } + removeAddedFilenames(deletedFilename); + removeUnchangedFilenames(deletedFilename); + }, + [deletedFilenames, removeAddedFilenames, removeUnchangedFilenames] + ); + + const addQuickStarter = useCallback(() => { + setConversationQuickStarters( + produce(conversationQuickStarters, (draft) => { + draft.push({ + title: '', + example: '', + }); + }) + ); + }, [conversationQuickStarters]); + + const updateQuickStarter = useCallback( + (quickStart: ConversationQuickStarter, index: number) => { + setConversationQuickStarters( + produce(conversationQuickStarters, (draft) => { + draft[index] = quickStart; + }) + ); + }, + [conversationQuickStarters] + ); + + const removeQuickStarter = useCallback( + (index: number) => { + setConversationQuickStarters( + produce(conversationQuickStarters, (draft) => { + draft.splice(index, 1); + if (draft.length === 0) { + draft.push({ + title: '', + example: '', + }); + } + }) + ); + }, + [conversationQuickStarters] + ); + + const onChangeEmbeddingsModel = useCallback( + (value: EmbeddingsModel) => { + setEmbeddingsModel(value); + // Update maxTokens based on the selected embeddings model + const maxEdge = EDGE_CHUNKING_MAX_TOKENS.MAX[value]; + if (chunkingMaxTokens > maxEdge) { + setChunkingMaxTokens(maxEdge); + } + }, + [chunkingMaxTokens] + ); + + const onClickBack = useCallback(() => { + history.back(); + }, []); + + const isValidGenerationConfigParam = useCallback( + (value: number, key: 'maxTokens' | 'topK' | 'topP' | 'temperature') => { + if (value < edgeGenerationParams[key].MIN) { + setErrorMessages( + key, + t('validation.minRange.message', { + size: edgeGenerationParams[key].MIN, + }) + ); + return false; + } else if (value > edgeGenerationParams[key].MAX) { + setErrorMessages( + key, + t('validation.maxRange.message', { + size: edgeGenerationParams[key].MAX, + }) + ); + return false; + } + + return true; + }, + [setErrorMessages, t] + ); + + const isValid = useCallback((): boolean => { + clearErrorMessages(); + + // S3 URLs validation - s3://bucket-name/ + const isS3UrlsValid = s3Urls.every((url, idx) => { + if (url && !/^s3:\/\/[a-z0-9.-]+\/$/.test(url)) { + setErrorMessages(`s3Urls-${idx}`, 'S3 URL is invalid'); + return false; + } else { + return true; + } + }); + if (!isS3UrlsValid) { + return false; + } + + // Chunking Strategy params validation + if (chunkingStrategy === 'fixed_size') { + if (chunkingMaxTokens < EDGE_CHUNKING_MAX_TOKENS.MIN) { + setErrorMessages( + 'chunkingMaxTokens', + t('validation.minRange.message', { + size: EDGE_CHUNKING_MAX_TOKENS.MIN, + }) + ); + return false; + } else if ( + chunkingMaxTokens > EDGE_CHUNKING_MAX_TOKENS.MAX[embeddingsModel] + ) { + setErrorMessages( + 'chunkingMaxTokens', + t('validation.maxRange.message', { + size: EDGE_CHUNKING_MAX_TOKENS.MAX[embeddingsModel], + }) + ); + return false; + } + + if (chunkingOverlapPercentage < EDGE_CHUNKING_OVERLAP_PERCENTAGE.MIN) { + setErrorMessages( + 'chunkingOverlapPercentage', + t('validation.minRange.message', { + size: EDGE_CHUNKING_OVERLAP_PERCENTAGE.MIN, + }) + ); + return false; + } else if ( + chunkingOverlapPercentage > EDGE_CHUNKING_OVERLAP_PERCENTAGE.MAX + ) { + setErrorMessages( + 'chunkingOverlapPercentage', + t('validation.maxRange.message', { + size: EDGE_CHUNKING_OVERLAP_PERCENTAGE.MAX, + }) + ); + return false; + } + } + + if (stopSequences.length === 0) { + setErrorMessages('stopSequences', t('input.validationError.required')); + return false; + } + + if (searchParams.maxResults < EDGE_SEARCH_PARAMS.maxResults.MIN) { + setErrorMessages( + 'maxResults', + t('validation.minRange.message', { + size: EDGE_SEARCH_PARAMS.maxResults.MIN, + }) + ); + return false; + } else if (searchParams.maxResults > EDGE_SEARCH_PARAMS.maxResults.MAX) { + setErrorMessages( + 'maxResults', + t('validation.maxRange.message', { + size: EDGE_SEARCH_PARAMS.maxResults.MAX, + }) + ); + return false; + } + + const isQsValid = conversationQuickStarters.every((rs, idx) => { + if ((!rs.title && !!rs.example) || (!!rs.title && !rs.example)) { + setErrorMessages( + `conversationQuickStarter${idx}`, + t('validation.quickStarter.message') + ); + return false; + } else { + return true; + } + }); + if (!isQsValid) { + return false; + } + + return ( + isValidGenerationConfigParam(maxTokens, 'maxTokens') && + isValidGenerationConfigParam(topK, 'topK') && + isValidGenerationConfigParam(topP, 'topP') && + isValidGenerationConfigParam(temperature, 'temperature') + ); + }, [ + clearErrorMessages, + s3Urls, + stopSequences.length, + searchParams.maxResults, + conversationQuickStarters, + isValidGenerationConfigParam, + maxTokens, + topK, + topP, + temperature, + setErrorMessages, + embeddingsModel, + chunkingStrategy, + chunkingMaxTokens, + chunkingOverlapPercentage, + t, + ]); + + const onClickCreate = useCallback(() => { + if (!isValid()) { + return; + } + setIsLoading(true); + registerBot({ + agent: { + tools: tools.map(({ name }) => name), + }, + id: botId, + title, + description, + instruction, + embeddingParams: null, + generationParams: { + maxTokens, + temperature, + topK, + topP, + stopSequences: stopSequences.split(','), + }, + searchParams: { + // set same value as bedrockKnowledgeBase.searchParams + maxResults: searchParams.maxResults, + }, + knowledge: { + sourceUrls: urls.filter((s) => s !== ''), + // Sitemap cannot be used yet. + sitemapUrls: [], + s3Urls: s3Urls.filter((s) => s !== ''), + filenames: files.map((f) => f.filename), + }, + displayRetrievedChunks, + conversationQuickStarters: conversationQuickStarters.filter( + (qs) => qs.title !== '' && qs.example !== '' + ), + bedrockKnowledgeBase: { + embeddingsModel, + chunkingStrategy, + maxTokens: chunkingStrategy == 'fixed_size' ? chunkingMaxTokens : null, + overlapPercentage: + chunkingStrategy == 'fixed_size' ? chunkingOverlapPercentage : null, + openSearch: openSearchParams, + searchParams: searchParams, + }, + }) + .then(() => { + navigate('/bot/explore'); + }) + .catch(() => { + setIsLoading(false); + }); + }, [ + isValid, + registerBot, + tools, + botId, + title, + description, + instruction, + maxTokens, + temperature, + topK, + topP, + stopSequences, + searchParams, + urls, + s3Urls, + files, + displayRetrievedChunks, + conversationQuickStarters, + navigate, + embeddingsModel, + chunkingStrategy, + chunkingMaxTokens, + chunkingOverlapPercentage, + openSearchParams, + ]); + + const onClickEdit = useCallback(() => { + if (!isValid()) { + return; + } + if (!isNewBot) { + setIsLoading(true); + updateBot(botId, { + agent: { + tools: tools.map(({ name }) => name), + }, + title, + description, + instruction, + embeddingParams: null, + generationParams: { + maxTokens, + temperature, + topK, + topP, + stopSequences: stopSequences.split(','), + }, + searchParams: { + // set same value as bedrockKnowledgeBase.searchParams + maxResults: searchParams.maxResults, + }, + knowledge: { + sourceUrls: urls.filter((s) => s !== ''), + // Sitemap cannot be used yet. + sitemapUrls: [], + s3Urls: s3Urls.filter((s) => s !== ''), + addedFilenames, + deletedFilenames, + unchangedFilenames, + }, + displayRetrievedChunks, + conversationQuickStarters: conversationQuickStarters.filter( + (qs) => qs.title !== '' && qs.example !== '' + ), + bedrockKnowledgeBase: { + embeddingsModel, + chunkingStrategy, + maxTokens: + chunkingStrategy == 'fixed_size' ? chunkingMaxTokens : null, + overlapPercentage: + chunkingStrategy == 'fixed_size' ? chunkingOverlapPercentage : null, + openSearch: openSearchParams, + searchParams: searchParams, + }, + }) + .then(() => { + navigate('/bot/explore'); + }) + .catch(() => { + setIsLoading(false); + }); + } + }, [ + isValid, + isNewBot, + updateBot, + botId, + tools, + title, + description, + instruction, + maxTokens, + temperature, + topK, + topP, + stopSequences, + searchParams, + urls, + s3Urls, + addedFilenames, + deletedFilenames, + unchangedFilenames, + displayRetrievedChunks, + conversationQuickStarters, + navigate, + embeddingsModel, + chunkingStrategy, + chunkingMaxTokens, + chunkingOverlapPercentage, + openSearchParams, + ]); + + const [isOpenSamples, setIsOpenSamples] = useState(false); + + const disabledRegister = useMemo(() => { + return title === '' || files.findIndex((f) => f.status !== 'UPLOADED') > -1; + }, [files, title]); + + return ( + <> + { + setIsOpenSamples(false); + }} + /> +
+
+
+
+ {isNewBot ? t('bot.create.pageTitle') : t('bot.edit.pageTitle')} +
+ +
+ + +
+ +