diff --git a/README.md b/README.md index 97886fa2..035aee92 100644 --- a/README.md +++ b/README.md @@ -253,30 +253,15 @@ npm run cdk:deploy ## お客様事例 -### [株式会社やさしい手](https://www.yasashiite.com/) - -![yasashiite_logo.png](/imgs/株式会社やさしい手_ロゴ.png) -GenUを活用した介護現場の記録・報告業務の効率化。介護利用者の家族や医師、ケアマネージャーが読みやすい報告業務の自動化を実現。介護記録データから個別作業手順の生成や自動更新を行ったり、ケアマネージャーと利用者の会話音声から標準項目に沿ったケアプランの作成を行う。中堅・中小企業向け事業戦略に関する[説明会](https://japan.zdnet.com/article/35221718/)に登壇。 - -### [株式会社サルソニード](https://salsonido.com/) - -![salsonido.png](/imgs/株式会社サルソニード_事例.png) -マーケターの記事執筆支援に GenU を活用。専門知識を持ったうえで記事の執筆が必要であり、記事制作に時間・人・スキルの面で課題があった。GenU の RAG を利用し、情報をもとに記事を作成することで課題を克服。リライト業務にかける時間を 70% 削減した。 - -### [株式会社タムラ製作所](https://www.tamura-ss.co.jp/jp/index.html) - -![tamura.png](/imgs/株式会社タムラ製作所_事例.png) -製品の実験を行うにあたり、大量の製品書類があり必要な情報がどの文書に記載されているか特定が困難であるという課題があった。GenU の RAG を活用することで、文書の発見を容易にした。加えて、文字起こしや文書生成も活用し、議事録を簡単に作成できるようになったことで、情報の共有が活発化した。 - -### [株式会社JDSC](https://jdsc.ai/) - -![jdsc.png](/imgs/株式会社JDSC_事例.png) -GenU をリファレンスにし、Bedrock の生成AI の出力を実際にアプリケーションで確認しつつ、開発できたことが成功要因でした。海事産業特有の専門的な問合せについて、90%以上の性能改善ができたのは、Haiku, Sonnet, Opus の適宜の利用と、AWSの各種サービス活用によります。特に性能向上の観点で新たにSonnet 3.5 への適用、 Kendra から RDS for PostgreSQL の pgvector への切替など、確固たるGenUのベースと自社ノウハウを両立させられたのも良かったです。 - -### [アイレット株式会社](https://www.iret.co.jp/) -[株式会社バンダイナムコアミューズメント様のクラウドを活用した導入事例](https://cloudpack.jp/casestudy/302.html?_gl=1*17hkazh*_gcl_au*ODA5MDk3NzI0LjE3MTM0MTQ2MDU) - -株式会社バンダイナムコアミューズメントの生成 AI 活用に向けて社内のナレッジを蓄積・体系化すべく、AWS が提供している Generative AI Use Cases JP を活用したユースケースサイトを開発。アイレット株式会社が本プロジェクトの設計・構築・開発を支援。 +| Customer | Quote | +|:--------|:---------| +| | **株式会社やさしい手**
*GenU のおかげで、利用者への付加価値提供と従業員の業務効率向上が実現できました。従業員にとって「いままでの仕事」が楽しい仕事に変化していく「サクサクからワクワクへ」更に進化を続けます!*
・[事例の詳細を見る](./imgs/cases/yasashiite_case.png)| +| | **株式会社サルソニード**
*ソリューションとして用意されている GenU を活用することで、生成 AI による業務プロセスの改善に素早く取り掛かることができました。*
・[事例の詳細を見る](./imgs/cases/salsonido_case.png)
・[適用サービス](https://kirei.ai/)| +| | **株式会社タムラ製作所**
*AWS が Github に公開しているアプリケーションサンプルは即テスト可能な機能が豊富で、そのまま利用することで自分たちにあった機能の選定が難なくでき、最終システムの開発時間を短縮することができました。*
・[事例の詳細を見る](./imgs/cases/tamura-ss_case.png)
| +| | **株式会社JDSC**
*Amazon Bedrock ではセキュアにデータを用い LLM が活用できます。また、用途により最適なモデルを切り替えて利用できるので、コストを抑えながら速度・精度を高めることができました。*
・[事例の詳細を見る](./imgs/cases/jdsc_case.png) | +| | **アイレット株式会社**
*株式会社バンダイナムコアミューズメントの生成 AI 活用に向けて社内のナレッジを蓄積・体系化すべく、AWS が提供している Generative AI Use Cases JP を活用したユースケースサイトを開発。アイレット株式会社が本プロジェクトの設計・構築・開発を支援。*
・[株式会社バンダイナムコアミューズメント様のクラウドを活用した導入事例](https://cloudpack.jp/casestudy/302.html?_gl=1*17hkazh*_gcl_au*ODA5MDk3NzI0LjE3MTM0MTQ2MDU) | +| | **株式会社アイデアログ**
*M従来の生成 AI ツールよりもさらに業務効率化ができていると感じます。入出力データをモデルの学習に使わない Amazon Bedrock を使っているので、セキュリティ面も安心です。*
・[事例の詳細を見る](./imgs/cases/idealog_case.png)
・[適用サービス](https://kaijosearch.com/)| +| | **株式会社エスタイル**
*GenU を活用して短期間で生成 AI 環境を構築し、社内のナレッジシェアを促進することができました。*
・[事例の詳細を見る](./imgs/cases/estyle_case.png) | 活用事例を掲載させて頂ける場合は、[Issue](https://github.com/aws-samples/generative-ai-use-cases-jp/issues)よりご連絡ください。 diff --git a/browser-extension/README.md b/browser-extension/README.md index 70d76b90..630de26a 100644 --- a/browser-extension/README.md +++ b/browser-extension/README.md @@ -55,7 +55,7 @@ - プリセットのプロンプトを追加したい - `browser-extension/src/app/features/prompt-settings/presetPrompts.ts` にプロンプトを設定して、再度「ビルド + インストール」をしてください。 - GenU の Web アプリで使っているプロンプトを利用したい - - Web アプリから保存したシステムコンテキストを利用できます。 + - Web アプリから保存したシステムプロンプトを利用できます。 - 拡張機能の「プロンプト設定」画面から、利用設定を行なってください。 - ログインに失敗します - まずは、GenU の Web アプリから正常にログインできるか、ご確認ください。 diff --git a/browser-extension/src/app/features/prompt-settings/PromptSettings.tsx b/browser-extension/src/app/features/prompt-settings/PromptSettings.tsx index 1c920cd3..53ed2e3d 100644 --- a/browser-extension/src/app/features/prompt-settings/PromptSettings.tsx +++ b/browser-extension/src/app/features/prompt-settings/PromptSettings.tsx @@ -140,7 +140,7 @@ const PromptSettings: React.FC = (props) => { setisSelectedPreset(false); }} > - 登録済みのシステムコンテキスト + 登録済みのシステムプロンプト
=> { - const useCaseUserId = `user#useCase#${_userId}`; - const favoriteUserId = `user#favorite#${_userId}`; - - const [useCaseRes, favoriteRes] = await Promise.all([ - dynamoDbDocument.send( - new QueryCommand({ - TableName: USECASE_TABLE_NAME, - KeyConditionExpression: '#id = :id', - ExpressionAttributeNames: { - '#id': 'id', - }, - ExpressionAttributeValues: { - ':id': useCaseUserId, - }, - }) - ), - - dynamoDbDocument.send( - new QueryCommand({ - TableName: USECASE_TABLE_NAME, - KeyConditionExpression: '#id = :id', - ExpressionAttributeNames: { - '#id': 'id', - }, - ExpressionAttributeValues: { - ':id': favoriteUserId, - }, - }) - ), - ]); - - const favoriteSet = new Set( - (favoriteRes.Items || []).map((item) => item.useCaseId) +// useCaseId のユースケースを取得 +const innerFindUseCaseByUseCaseId = async ( + useCaseId: string +): Promise => { + const useCaseInTable = await dynamoDbDocument.send( + new QueryCommand({ + TableName: USECASE_TABLE_NAME, + IndexName: USECASE_ID_INDEX_NAME, + KeyConditionExpression: + '#useCaseId = :useCaseId and begins_with(#dataType, :dataTypePrefix)', + ExpressionAttributeNames: { + '#useCaseId': 'useCaseId', + '#dataType': 'dataType', + }, + ExpressionAttributeValues: { + ':useCaseId': useCaseId, + ':dataTypePrefix': 'useCase', + }, + }) ); - return ((useCaseRes.Items as TableUseCase[]) || []).map((item) => ({ - useCaseId: item.useCaseId, - title: item.title, - description: item.description, - isFavorite: favoriteSet.has(item.useCaseId), - hasShared: item.hasShared, - isMyUseCase: true, - })); + if (useCaseInTable.Items && useCaseInTable.Items.length > 0) { + return useCaseInTable.Items[0] as UseCaseInTable; + } else { + return null; + } }; -export const listFavoriteUseCases = async ( - _userId: string -): Promise => { - const useCaseUserId = `user#useCase#${_userId}`; - const favoriteUserId = `user#favorite#${_userId}`; - - const favoriteRes = await dynamoDbDocument.send( +// userId のユースケース一覧を取得 +const innerFindUseCasesByUserId = async ( + userId: string +): Promise => { + const useCasesInTable = await dynamoDbDocument.send( new QueryCommand({ TableName: USECASE_TABLE_NAME, - KeyConditionExpression: '#id = :id', + KeyConditionExpression: + '#id = :id and begins_with(#dataType, :dataTypePrefix)', ExpressionAttributeNames: { '#id': 'id', + '#dataType': 'dataType', }, ExpressionAttributeValues: { - ':id': favoriteUserId, + ':id': `useCase#${userId}`, + ':dataTypePrefix': 'useCase', }, + ScanIndexForward: false, }) ); - if (!favoriteRes.Items || favoriteRes.Items.length === 0) { - return []; - } + return (useCasesInTable.Items || []) as UseCaseInTable[]; +}; - const favoriteUseCaseIds = favoriteRes.Items.map((item) => item.useCaseId); +// useCaseId の配列からユースケース一覧を取得 +const innerFindUseCasesByUseCaseIds = async ( + useCaseIds: string[] +): Promise => { + const useCasesInTable: UseCaseInTable[] = []; - const useCases: TableUseCase[] = []; - for (const favoriteUseCaseId of favoriteUseCaseIds) { - const useCaseRes = await dynamoDbDocument.send( - new QueryCommand({ - TableName: USECASE_TABLE_NAME, - IndexName: USECASE_ID_INDEX_NAME, - KeyConditionExpression: - 'useCaseId = :useCaseId AND begins_with(#id, :prefixId)', - ExpressionAttributeNames: { - '#id': 'id', - }, - ExpressionAttributeValues: { - ':useCaseId': favoriteUseCaseId, - ':prefixId': 'user#useCase#', - }, - }) - ); - if (!useCaseRes.Items || useCaseRes.Items.length === 0) continue; - const useCaseItem = useCaseRes.Items[0]; - if (!useCaseItem) continue; - useCases.push(useCaseItem as TableUseCase); + for (const useCaseId of useCaseIds) { + const useCaseInTable = await innerFindUseCaseByUseCaseId(useCaseId); + + if (useCaseInTable) { + useCasesInTable.push(useCaseInTable); + } } - return useCases - .filter((useCase): useCase is NonNullable => { - if (!useCase) return false; - return useCase.id === useCaseUserId || useCase.hasShared; - }) - .map((item) => ({ - useCaseId: item.useCaseId, - title: item.title, - description: item.description, - isFavorite: true, - hasShared: item.hasShared, - isMyUseCase: item.id === useCaseUserId, - })); + return useCasesInTable; }; -// 自分が作成しただけでなく、共有されたユースケースも取得 -export const getUseCase = async ( - _userId: string, - useCaseId: string -): Promise => { - const useCaseUserId = `user#useCase#${_userId}`; - const favoriteUserId = `user#favorite#${_userId}`; - const useCaseRes = await dynamoDbDocument.send( +// userId の特定のデータタイプ (お気に入り・利用履歴) 一覧を取得 +const innerFindCommonsByUserIdAndDataType = async ( + userId: string, + dataTypePrefix: string, + limit?: number +): Promise => { + const commons = await dynamoDbDocument.send( new QueryCommand({ TableName: USECASE_TABLE_NAME, - IndexName: USECASE_ID_INDEX_NAME, KeyConditionExpression: - 'useCaseId = :useCaseId AND begins_with(#id, :prefixId)', + '#id = :id and begins_with(#dataType, :dataTypePrefix)', ExpressionAttributeNames: { '#id': 'id', + '#dataType': 'dataType', }, ExpressionAttributeValues: { - ':useCaseId': useCaseId, - ':prefixId': 'user#useCase#', + ':id': `useCase#${userId}`, + ':dataTypePrefix': dataTypePrefix, }, + Limit: limit, + ScanIndexForward: false, }) ); - const useCase = useCaseRes.Items?.[0] as TableUseCase | undefined; - - if (!useCase || (useCase.id !== useCaseUserId && !useCase.hasShared)) { - return null; - } + return (commons.Items || []) as UseCaseCommon[]; +}; - const favoriteRes = await dynamoDbDocument.send( - new GetCommand({ +// useCaseId に関連する全てのデータ (本体・お気に入り・利用履歴) 一覧を取得 +const innerFindCommonsByUseCaseId = async ( + useCaseId: string +): Promise => { + const commons = await dynamoDbDocument.send( + new QueryCommand({ TableName: USECASE_TABLE_NAME, - Key: { - id: favoriteUserId, - useCaseId: useCaseId, + IndexName: USECASE_ID_INDEX_NAME, + KeyConditionExpression: '#useCaseId = :useCaseId', + ExpressionAttributeNames: { + '#useCaseId': 'useCaseId', + }, + ExpressionAttributeValues: { + ':useCaseId': useCaseId, }, }) ); - return { - useCaseId: useCase.useCaseId, - title: useCase.title, - description: useCase.description, - inputExamples: useCase.inputExamples, - isFavorite: Boolean(favoriteRes.Item), - promptTemplate: useCase.promptTemplate, - hasShared: useCase.hasShared, - isMyUseCase: useCase.id === useCaseUserId, - }; + return (commons.Items || []) as UseCaseCommon[]; }; -export const createUseCase = async (params: { - userId: string; - title: string; - promptTemplate: string; - description?: string; - inputExamples?: UseCaseInputExample[]; -}): Promise => { - const userId = `user#useCase#${params.userId}`; +export const createUseCase = async ( + userId: string, + content: UseCaseContent +): Promise => { + const id = `useCase#${userId}`; const useCaseId = crypto.randomUUID(); - const item: TableUseCase = { - id: userId, - useCaseId: useCaseId, - title: params.title, - description: params.description, - promptTemplate: params.promptTemplate, - inputExamples: params.inputExamples, - hasShared: false, + const dataType = `useCase#${Date.now()}`; + + const item: UseCaseInTable = { + id, + dataType, + useCaseId, + title: content.title, + description: content.description, + promptTemplate: content.promptTemplate, + inputExamples: content.inputExamples, + isShared: false, }; await dynamoDbDocument.send( @@ -211,308 +166,327 @@ export const createUseCase = async (params: { }) ); - return { useCaseId: useCaseId }; + return { + ...item, + isFavorite: false, + isMyUseCase: true, + }; +}; + +export const getUseCase = async ( + userId: string, + useCaseId: string +): Promise => { + const useCaseInTable = await innerFindUseCaseByUseCaseId(useCaseId); + + if (!useCaseInTable) { + return null; + } + + if (useCaseInTable.id.split('#')[1] !== userId) { + return null; + } + + const favorites = await innerFindCommonsByUserIdAndDataType( + userId, + 'favorite' + ); + const favoritesUseCaseIds = favorites.map((f) => f.useCaseId); + + const useCaseAsOutput: UseCaseAsOutput = { + ...useCaseInTable, + isFavorite: favoritesUseCaseIds.includes(useCaseId), + isMyUseCase: useCaseInTable.id.split('#')[1] === userId, + }; + + return useCaseAsOutput; +}; + +export const listUseCases = async ( + userId: string +): Promise => { + const useCasesInTable = await innerFindUseCasesByUserId(userId); + + const favorites = await innerFindCommonsByUserIdAndDataType( + userId, + 'favorite' + ); + const favoritesUseCaseIds = favorites.map((f) => f.useCaseId); + + const useCasesAsOutput: UseCaseAsOutput[] = useCasesInTable.map((u) => { + return { + ...u, + isFavorite: favoritesUseCaseIds.includes(u.useCaseId), + isMyUseCase: u.id.split('#')[1] === userId, + }; + }); + + return useCasesAsOutput; }; export const updateUseCase = async ( - _userId: string, + userId: string, useCaseId: string, - params: { - title: string; - promptTemplate: string; - description?: string; - inputExamples?: UseCaseInputExample[]; - } + content: UseCaseContent ): Promise => { - const userId = `user#useCase#${_userId}`; + const useCaseInTable = await innerFindUseCaseByUseCaseId(useCaseId); + + if (!useCaseInTable) { + console.error( + `Use case doesn't exist for userId=${userId} and useCaseId=${useCaseId}` + ); + return; + } + + if (useCaseInTable.id.split('#')[1] !== userId) { + console.error( + `userId mismatch ${userId} vs ${useCaseInTable.id.split('#')[1]}` + ); + return; + } + await dynamoDbDocument.send( new UpdateCommand({ TableName: USECASE_TABLE_NAME, Key: { - id: userId, - useCaseId: useCaseId, + id: useCaseInTable.id, + dataType: useCaseInTable.dataType, }, UpdateExpression: 'set title = :title, promptTemplate = :promptTemplate, description = :description, inputExamples = :inputExamples', ExpressionAttributeValues: { - ':title': params.title, - ':promptTemplate': params.promptTemplate, - ':description': params.description ?? '', - ':inputExamples': params.inputExamples ?? [], + ':title': content.title, + ':promptTemplate': content.promptTemplate, + ':description': content.description ?? '', + ':inputExamples': content.inputExamples ?? [], }, }) ); }; export const deleteUseCase = async ( - _userId: string, + userId: string, useCaseId: string ): Promise => { - const useCaseUserId = `user#useCase#${_userId}`; - const favoriteUserId = `user#favorite#${_userId}`; + const useCaseInTable = await innerFindUseCaseByUseCaseId(useCaseId); - // ユースケース削除 - await dynamoDbDocument.send( - new DeleteCommand({ - TableName: USECASE_TABLE_NAME, - Key: { - id: useCaseUserId, - useCaseId: useCaseId, + if (!useCaseInTable) { + console.error( + `Use case doesn't exist for userId=${userId} and useCaseId=${useCaseId}` + ); + return; + } + + if (useCaseInTable.id.split('#')[1] !== userId) { + console.error( + `userId mismatch ${userId} vs ${useCaseInTable.id.split('#')[1]}` + ); + return; + } + + const commons = await innerFindCommonsByUseCaseId(useCaseId); + const requestItems = commons.map((common) => { + return { + DeleteRequest: { + Key: { + id: common.id, + dataType: common.dataType, + }, }, - }) - ); + }; + }); - // お気に入り登録があれば削除 - const result = await dynamoDbDocument.send( - new GetCommand({ - TableName: USECASE_TABLE_NAME, - Key: { - id: favoriteUserId, - useCaseId: useCaseId, + // 本体・お気に入り・利用履歴をまとめて削除 + await dynamoDbDocument.send( + new BatchWriteCommand({ + RequestItems: { + [USECASE_TABLE_NAME]: requestItems, }, }) ); - if (result.Item) { - await dynamoDbDocument.send( - new DeleteCommand({ - TableName: USECASE_TABLE_NAME, - Key: { - id: favoriteUserId, - useCaseId: useCaseId, - }, - }) - ); - } +}; + +export const listFavoriteUseCases = async ( + userId: string +): Promise => { + const commons = await innerFindCommonsByUserIdAndDataType(userId, 'favorite'); + const useCaseIds = commons.map((c) => c.useCaseId); + const useCasesInTable = await innerFindUseCasesByUseCaseIds(useCaseIds); + const useCasesAsOutput: UseCaseAsOutput[] = useCasesInTable.map((u) => { + return { + ...u, + isFavorite: true, + isMyUseCase: u.id.split('#')[1] === userId, + }; + }); + + // 自分のもの or シェアされているもののみ + const useCasesAsOutputFiltered = useCasesAsOutput.filter((u) => { + return u.isMyUseCase || u.isShared; + }); + + return useCasesAsOutputFiltered; }; export const toggleFavorite = async ( - _userId: string, + userId: string, useCaseId: string ): Promise => { - const favoriteUserId = `user#favorite#${_userId}`; + // 自分のお気に入り一覧を取得してすでに登録されているかを確認する + // MEMO: お気に入りの数が膨大になった場合リストから溢れる可能性あり + const commons = await innerFindCommonsByUserIdAndDataType(userId, 'favorite'); + const useCaseIds = commons.map((c) => c.useCaseId); + const index = useCaseIds.indexOf(useCaseId); - const existingFavorite = await dynamoDbDocument.send( - new GetCommand({ - TableName: USECASE_TABLE_NAME, - Key: { - id: favoriteUserId, - useCaseId: useCaseId, - }, - }) - ); + if (index >= 0) { + // お気に入りを解除 + const common = commons[index]; - if (existingFavorite.Item) { await dynamoDbDocument.send( new DeleteCommand({ TableName: USECASE_TABLE_NAME, Key: { - id: favoriteUserId, - useCaseId: useCaseId, + id: common.id, + dataType: common.dataType, }, }) ); + return { isFavorite: false }; } else { + // お気に入りに登録 await dynamoDbDocument.send( new PutCommand({ TableName: USECASE_TABLE_NAME, Item: { - id: favoriteUserId, + id: `useCase#${userId}`, + dataType: `favorite#${Date.now()}`, useCaseId: useCaseId, }, }) ); + return { isFavorite: true }; } }; -export const getOwnedUseCase = async ( - _userId: string, +export const toggleShared = async ( + userId: string, useCaseId: string -): Promise => { - const useCaseUserId = `user#useCase#${_userId}`; - const favoriteUserId = `user#favorite#${_userId}`; - - const useCaseRes = await dynamoDbDocument.send( - new GetCommand({ - TableName: USECASE_TABLE_NAME, - Key: { - id: useCaseUserId, - useCaseId: useCaseId, - }, - }) - ); - if (!useCaseRes.Item) return null; +): Promise => { + const useCaseInTable = await innerFindUseCaseByUseCaseId(useCaseId); - const favoriteRes = await dynamoDbDocument.send( - new GetCommand({ - TableName: USECASE_TABLE_NAME, - Key: { - id: favoriteUserId, - useCaseId: useCaseId, - }, - }) - ); - const item = useCaseRes.Item; + if (!useCaseInTable) { + console.error( + `Use case doesn't exist for userId=${userId} and useCaseId=${useCaseId}` + ); + return { isShared: false }; + } - return { - useCaseId: item.useCaseId, - title: item.title, - isFavorite: Boolean(favoriteRes.Item), - promptTemplate: item.promptTemplate, - hasShared: item.hasShared, - isMyUseCase: true, - }; -}; + if (useCaseInTable.id.split('#')[1] !== userId) { + console.error( + `userId mismatch ${userId} vs ${useCaseInTable.id.split('#')[1]}` + ); + return { isShared: false }; + } -export const toggleShared = async ( - _userId: string, - useCase: CustomUseCaseMeta -): Promise => { - const useCaseUserId = `user#useCase#${_userId}`; - const newSharedState = !useCase.hasShared; await dynamoDbDocument.send( new UpdateCommand({ TableName: USECASE_TABLE_NAME, Key: { - id: useCaseUserId, - useCaseId: useCase.useCaseId, + id: useCaseInTable.id, + dataType: useCaseInTable.dataType, }, - UpdateExpression: 'set hasShared = :hasShared', + UpdateExpression: 'set isShared = :isShared', ExpressionAttributeValues: { - ':hasShared': newSharedState, + ':isShared': !useCaseInTable.isShared, }, }) ); - return { hasShared: newSharedState }; + + return { isShared: !useCaseInTable.isShared }; }; export const listRecentlyUsedUseCases = async ( - _userId: string -): Promise => { - const useCaseUserId = `user#useCase#${_userId}`; - const favoriteUserId = `user#favorite#${_userId}`; - const recentlyUsedUserId = `user#recentlyUsed#${_userId}`; - - const recentlyUsedRes = await dynamoDbDocument.send( - new GetCommand({ - TableName: USECASE_TABLE_NAME, - Key: { - id: recentlyUsedUserId, - useCaseId: 'recently', - }, - }) - ); - - if ( - !recentlyUsedRes.Item?.recentUseCaseIds || - recentlyUsedRes.Item.recentUseCaseIds.length === 0 - ) { - return []; - } - - const recentUseCaseIds = recentlyUsedRes.Item.recentUseCaseIds; - - const favoriteRes = await dynamoDbDocument.send( - new QueryCommand({ - TableName: USECASE_TABLE_NAME, - KeyConditionExpression: '#id = :id', - ExpressionAttributeNames: { - '#id': 'id', - }, - ExpressionAttributeValues: { - ':id': favoriteUserId, - }, - }) + userId: string +): Promise => { + const commons = await innerFindCommonsByUserIdAndDataType( + userId, + 'recentlyUsed', + 15 ); + const useCaseIds = commons.map((c) => c.useCaseId); + const useCasesInTable = await innerFindUseCasesByUseCaseIds(useCaseIds); - const favoriteSet = new Set( - (favoriteRes.Items || []).map((item) => item.useCaseId) + const favorites = await innerFindCommonsByUserIdAndDataType( + userId, + 'favorite' ); - - const useCasesMeta: CustomUseCaseMeta[] = []; - for (const useCaseId of recentUseCaseIds) { - if (useCasesMeta.length >= 15) break; - const useCaseRes = await dynamoDbDocument.send( - new QueryCommand({ - TableName: USECASE_TABLE_NAME, - IndexName: USECASE_ID_INDEX_NAME, - KeyConditionExpression: - 'useCaseId = :useCaseId AND begins_with(#id, :prefixId)', - ExpressionAttributeNames: { - '#id': 'id', - }, - ExpressionAttributeValues: { - ':useCaseId': useCaseId, - ':prefixId': 'user#useCase#', - }, - }) - ); - - if (!useCaseRes.Items || useCaseRes.Items.length === 0) continue; - - const useCase = useCaseRes.Items[0] as TableUseCase; - if (useCase.id !== useCaseUserId && !useCase.hasShared) continue; - useCasesMeta.push({ - useCaseId: useCase.useCaseId, - title: useCase.title, - description: useCase.description, - isFavorite: favoriteSet.has(useCase.useCaseId), - hasShared: useCase.hasShared, - isMyUseCase: useCase.id === useCaseUserId, - }); - } - - return useCasesMeta; + const favoritesUseCaseIds = favorites.map((f) => f.useCaseId); + + const useCasesAsOutput: UseCaseAsOutput[] = useCasesInTable.map((u) => { + return { + ...u, + isFavorite: favoritesUseCaseIds.includes(u.useCaseId), + isMyUseCase: u.id.split('#')[1] === userId, + }; + }); + + // 自分のもの or シェアされているもののみ + const useCasesAsOutputFiltered = useCasesAsOutput.filter((u) => { + return u.isMyUseCase || u.isShared; + }); + + return useCasesAsOutputFiltered; }; export const updateRecentlyUsedUseCase = async ( - _userId: string, + userId: string, useCaseId: string ): Promise => { - const recentlyUsedUserId = `user#recentlyUsed#${_userId}`; - - const currentRes = await dynamoDbDocument.send( - new GetCommand({ - TableName: USECASE_TABLE_NAME, - Key: { - id: recentlyUsedUserId, - useCaseId: 'recently', - }, - }) - ); - - let currentUseCaseIds = currentRes.Item?.recentUseCaseIds || []; - currentUseCaseIds = currentUseCaseIds.filter( - (id: string) => id !== useCaseId + const commons = await innerFindCommonsByUserIdAndDataType( + userId, + 'recentlyUsed' ); - currentUseCaseIds.unshift(useCaseId); - currentUseCaseIds = currentUseCaseIds.slice(0, 15); + const useCaseIds = commons.map((c) => c.useCaseId); + const index = useCaseIds.indexOf(useCaseId); - if (!currentRes.Item) { - // 新規作成 + if (index >= 0) { + // 削除と追加を同時に await dynamoDbDocument.send( - new PutCommand({ - TableName: USECASE_TABLE_NAME, - Item: { - id: recentlyUsedUserId, - useCaseId: 'recently', - recentUseCaseIds: currentUseCaseIds, - }, + new TransactWriteCommand({ + TransactItems: [ + { + Delete: { + TableName: USECASE_TABLE_NAME, + Key: { + id: commons[index].id, + dataType: commons[index].dataType, + }, + }, + }, + { + Put: { + TableName: USECASE_TABLE_NAME, + Item: { + id: `useCase#${userId}`, + dataType: `recentlyUsed#${Date.now()}`, + useCaseId, + }, + }, + }, + ], }) ); } else { - // 更新 + // 追加のみ await dynamoDbDocument.send( - new UpdateCommand({ + new PutCommand({ TableName: USECASE_TABLE_NAME, - Key: { - id: recentlyUsedUserId, - useCaseId: 'recently', - }, - UpdateExpression: 'SET recentUseCaseIds = :recentUseCaseIds', - ExpressionAttributeValues: { - ':recentUseCaseIds': currentUseCaseIds, + Item: { + id: `useCase#${userId}`, + dataType: `recentlyUsed#${Date.now()}`, + useCaseId, }, }) ); diff --git a/packages/cdk/lambda/utils/models.ts b/packages/cdk/lambda/utils/models.ts index 5fcc7314..2844982f 100644 --- a/packages/cdk/lambda/utils/models.ts +++ b/packages/cdk/lambda/utils/models.ts @@ -44,7 +44,7 @@ export const defaultImageGenerationModel: Model = { // Prompt Templates -const LLAMA2_PROMPT: PromptTemplate = { +const LLAMA_PROMPT: PromptTemplate = { prefix: '[INST] ', suffix: ' [/INST]', join: '', @@ -180,7 +180,7 @@ const createConverseCommandInput = ( defaultConverseInferenceParams: ConverseInferenceParams, usecaseConverseInferenceParams: UsecaseConverseInferenceParams ) => { - // system role で渡された文字列を、システムコンテキストに設定 + // system role で渡された文字列を、システムプロンプトに設定 const system = messages.find((message) => message.role === 'system'); const systemContext = system ? [{ text: system.content }] : []; @@ -245,7 +245,7 @@ const createConverseCommandInput = ( return converseCommandInput; }; -// システムコンテキストに対応していないモデル用の関数 +// システムプロンプトに対応していないモデル用の関数 // - Amazon Titan モデル (amazon.titan-text-premier-v1:0) // - Mistral AI Instruct (mistral.mixtral-8x7b-instruct-v0:1, mistral.mistral-7b-instruct-v0:2) // https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html#conversation-inference-supported-models-features @@ -305,7 +305,7 @@ const createConverseStreamCommandInput = ( } as ConverseStreamCommandInput; }; -// システムコンテキストに対応していないモデル用の関数 +// システムプロンプトに対応していないモデル用の関数 // - Amazon Titan モデル (amazon.titan-text-premier-v1:0) // - Mistral AI Instruct (mistral.mixtral-8x7b-instruct-v0:1, mistral.mistral-7b-instruct-v0:2) // https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html#conversation-inference-supported-models-features @@ -582,6 +582,30 @@ export const BEDROCK_TEXT_GEN_MODELS: { extractConverseOutputText: extractConverseOutputText, extractConverseStreamOutputText: extractConverseStreamOutputText, }, + 'us.anthropic.claude-3-5-sonnet-20241022-v2:0': { + defaultParams: CLAUDE_DEFAULT_PARAMS, + usecaseParams: USECASE_DEFAULT_PARAMS, + createConverseCommandInput: createConverseCommandInput, + createConverseStreamCommandInput: createConverseStreamCommandInput, + extractConverseOutputText: extractConverseOutputText, + extractConverseStreamOutputText: extractConverseStreamOutputText, + }, + 'anthropic.claude-3-5-haiku-20241022-v1:0': { + defaultParams: CLAUDE_DEFAULT_PARAMS, + usecaseParams: USECASE_DEFAULT_PARAMS, + createConverseCommandInput: createConverseCommandInput, + createConverseStreamCommandInput: createConverseStreamCommandInput, + extractConverseOutputText: extractConverseOutputText, + extractConverseStreamOutputText: extractConverseStreamOutputText, + }, + 'us.anthropic.claude-3-5-haiku-20241022-v1:0': { + defaultParams: CLAUDE_DEFAULT_PARAMS, + usecaseParams: USECASE_DEFAULT_PARAMS, + createConverseCommandInput: createConverseCommandInput, + createConverseStreamCommandInput: createConverseStreamCommandInput, + extractConverseOutputText: extractConverseOutputText, + extractConverseStreamOutputText: extractConverseStreamOutputText, + }, 'anthropic.claude-3-5-sonnet-20240620-v1:0': { defaultParams: CLAUDE_DEFAULT_PARAMS, usecaseParams: USECASE_DEFAULT_PARAMS, @@ -606,6 +630,14 @@ export const BEDROCK_TEXT_GEN_MODELS: { extractConverseOutputText: extractConverseOutputText, extractConverseStreamOutputText: extractConverseStreamOutputText, }, + 'apac.anthropic.claude-3-5-sonnet-20240620-v1:0': { + defaultParams: CLAUDE_DEFAULT_PARAMS, + usecaseParams: USECASE_DEFAULT_PARAMS, + createConverseCommandInput: createConverseCommandInput, + createConverseStreamCommandInput: createConverseStreamCommandInput, + extractConverseOutputText: extractConverseOutputText, + extractConverseStreamOutputText: extractConverseStreamOutputText, + }, 'anthropic.claude-3-opus-20240229-v1:0': { defaultParams: CLAUDE_DEFAULT_PARAMS, usecaseParams: USECASE_DEFAULT_PARAMS, @@ -646,6 +678,14 @@ export const BEDROCK_TEXT_GEN_MODELS: { extractConverseOutputText: extractConverseOutputText, extractConverseStreamOutputText: extractConverseStreamOutputText, }, + 'apac.anthropic.claude-3-sonnet-20240229-v1:0': { + defaultParams: CLAUDE_DEFAULT_PARAMS, + usecaseParams: USECASE_DEFAULT_PARAMS, + createConverseCommandInput: createConverseCommandInput, + createConverseStreamCommandInput: createConverseStreamCommandInput, + extractConverseOutputText: extractConverseOutputText, + extractConverseStreamOutputText: extractConverseStreamOutputText, + }, 'anthropic.claude-3-haiku-20240307-v1:0': { defaultParams: CLAUDE_DEFAULT_PARAMS, usecaseParams: USECASE_DEFAULT_PARAMS, @@ -670,6 +710,14 @@ export const BEDROCK_TEXT_GEN_MODELS: { extractConverseOutputText: extractConverseOutputText, extractConverseStreamOutputText: extractConverseStreamOutputText, }, + 'apac.anthropic.claude-3-haiku-20240307-v1:0': { + defaultParams: CLAUDE_DEFAULT_PARAMS, + usecaseParams: USECASE_DEFAULT_PARAMS, + createConverseCommandInput: createConverseCommandInput, + createConverseStreamCommandInput: createConverseStreamCommandInput, + extractConverseOutputText: extractConverseOutputText, + extractConverseStreamOutputText: extractConverseStreamOutputText, + }, 'anthropic.claude-v2:1': { defaultParams: CLAUDE_DEFAULT_PARAMS, usecaseParams: USECASE_DEFAULT_PARAMS, @@ -784,22 +832,6 @@ export const BEDROCK_TEXT_GEN_MODELS: { extractConverseOutputText: extractConverseOutputText, extractConverseStreamOutputText: extractConverseStreamOutputText, }, - 'meta.llama2-13b-chat-v1': { - defaultParams: LLAMA_DEFAULT_PARAMS, - usecaseParams: USECASE_DEFAULT_PARAMS, - createConverseCommandInput: createConverseCommandInput, - createConverseStreamCommandInput: createConverseStreamCommandInput, - extractConverseOutputText: extractConverseOutputText, - extractConverseStreamOutputText: extractConverseStreamOutputText, - }, - 'meta.llama2-70b-chat-v1': { - defaultParams: LLAMA_DEFAULT_PARAMS, - usecaseParams: USECASE_DEFAULT_PARAMS, - createConverseCommandInput: createConverseCommandInput, - createConverseStreamCommandInput: createConverseStreamCommandInput, - extractConverseOutputText: extractConverseOutputText, - extractConverseStreamOutputText: extractConverseStreamOutputText, - }, 'mistral.mistral-7b-instruct-v0:2': { defaultParams: MISTRAL_DEFAULT_PARAMS, usecaseParams: USECASE_DEFAULT_PARAMS, @@ -897,8 +929,8 @@ export const BEDROCK_IMAGE_GEN_MODELS: { }; export const getSageMakerModelTemplate = (model: string): PromptTemplate => { - if (model.includes('llama-2')) { - return LLAMA2_PROMPT; + if (model.includes('llama')) { + return LLAMA_PROMPT; } else if (model.includes('bilingual-rinna')) { return BILINGUAL_RINNA_PROMPT; } else if (model.includes('rinna')) { diff --git a/packages/cdk/lib/construct/api.ts b/packages/cdk/lib/construct/api.ts index 6e571dcc..dd5e9d7f 100644 --- a/packages/cdk/lib/construct/api.ts +++ b/packages/cdk/lib/construct/api.ts @@ -72,10 +72,13 @@ export class Api extends Construct { // Validate Model Names const supportedModelIds = [ 'anthropic.claude-3-5-sonnet-20241022-v2:0', + 'anthropic.claude-3-5-haiku-20241022-v1:0', 'anthropic.claude-3-5-sonnet-20240620-v1:0', 'anthropic.claude-3-opus-20240229-v1:0', 'anthropic.claude-3-sonnet-20240229-v1:0', 'anthropic.claude-3-haiku-20240307-v1:0', + 'us.anthropic.claude-3-5-sonnet-20241022-v2:0', + 'us.anthropic.claude-3-5-haiku-20241022-v1:0', 'us.anthropic.claude-3-5-sonnet-20240620-v1:0', 'us.anthropic.claude-3-opus-20240229-v1:0', 'us.anthropic.claude-3-sonnet-20240229-v1:0', @@ -83,6 +86,9 @@ export class Api extends Construct { 'eu.anthropic.claude-3-5-sonnet-20240620-v1:0', 'eu.anthropic.claude-3-sonnet-20240229-v1:0', 'eu.anthropic.claude-3-haiku-20240307-v1:0', + 'apac.anthropic.claude-3-5-sonnet-20240620-v1:0', + 'apac.anthropic.claude-3-sonnet-20240229-v1:0', + 'apac.anthropic.claude-3-haiku-20240307-v1:0', 'anthropic.claude-v2:1', 'anthropic.claude-v2', 'anthropic.claude-instant-v1', @@ -104,8 +110,6 @@ export class Api extends Construct { 'us.meta.llama3-2-3b-instruct-v1:0', 'us.meta.llama3-2-11b-instruct-v1:0', 'us.meta.llama3-2-90b-instruct-v1:0', - 'meta.llama2-13b-chat-v1', - 'meta.llama2-70b-chat-v1', 'mistral.mistral-7b-instruct-v0:2', 'mistral.mixtral-8x7b-instruct-v0:1', 'mistral.mistral-small-2402-v1:0', @@ -120,6 +124,7 @@ export class Api extends Construct { 'anthropic.claude-3-opus-20240229-v1:0', 'anthropic.claude-3-sonnet-20240229-v1:0', 'anthropic.claude-3-haiku-20240307-v1:0', + 'us.anthropic.claude-3-5-sonnet-20241022-v2:0', 'us.anthropic.claude-3-5-sonnet-20240620-v1:0', 'us.anthropic.claude-3-opus-20240229-v1:0', 'us.anthropic.claude-3-sonnet-20240229-v1:0', @@ -127,6 +132,9 @@ export class Api extends Construct { 'eu.anthropic.claude-3-5-sonnet-20240620-v1:0', 'eu.anthropic.claude-3-sonnet-20240229-v1:0', 'eu.anthropic.claude-3-haiku-20240307-v1:0', + 'apac.anthropic.claude-3-5-sonnet-20240620-v1:0', + 'apac.anthropic.claude-3-sonnet-20240229-v1:0', + 'apac.anthropic.claude-3-haiku-20240307-v1:0', 'us.meta.llama3-2-11b-instruct-v1:0', 'us.meta.llama3-2-90b-instruct-v1:0', ]; diff --git a/packages/cdk/lib/construct/database.ts b/packages/cdk/lib/construct/database.ts index 165f9b8b..49d1d612 100644 --- a/packages/cdk/lib/construct/database.ts +++ b/packages/cdk/lib/construct/database.ts @@ -4,8 +4,6 @@ import * as ddb from 'aws-cdk-lib/aws-dynamodb'; export class Database extends Construct { public readonly table: ddb.Table; public readonly feedbackIndexName: string; - public readonly useCaseBuilderTable: ddb.Table; - public readonly useCaseIdIndexName: string; constructor(scope: Construct, id: string) { super(scope, id); @@ -30,35 +28,7 @@ export class Database extends Construct { }, }); - const useCaseIdIndexName = 'UseCaseIdIndexName'; - const useCaseBuilderTable = new ddb.Table(this, 'UseCaseBuilderTable', { - partitionKey: { - name: 'id', - type: ddb.AttributeType.STRING, - }, - sortKey: { - name: 'useCaseId', - type: ddb.AttributeType.STRING, - }, - billingMode: ddb.BillingMode.PAY_PER_REQUEST, - }); - - useCaseBuilderTable.addGlobalSecondaryIndex({ - indexName: useCaseIdIndexName, - partitionKey: { - name: 'useCaseId', - type: ddb.AttributeType.STRING, - }, - sortKey: { - name: 'id', - type: ddb.AttributeType.STRING, - }, - projectionType: ddb.ProjectionType.ALL, - }); - this.table = table; this.feedbackIndexName = feedbackIndexName; - this.useCaseBuilderTable = useCaseBuilderTable; - this.useCaseIdIndexName = useCaseIdIndexName; } } diff --git a/packages/cdk/lib/construct/use-case-builder.ts b/packages/cdk/lib/construct/use-case-builder.ts index f39b57b4..bb98eb3d 100644 --- a/packages/cdk/lib/construct/use-case-builder.ts +++ b/packages/cdk/lib/construct/use-case-builder.ts @@ -4,7 +4,6 @@ import { CognitoUserPoolsAuthorizer, AuthorizationType, } from 'aws-cdk-lib/aws-apigateway'; -import { Table } from 'aws-cdk-lib/aws-dynamodb'; import { Construct } from 'constructs'; import { NodejsFunction, @@ -13,24 +12,50 @@ import { import { Runtime } from 'aws-cdk-lib/aws-lambda'; import { Duration } from 'aws-cdk-lib'; import { UserPool } from 'aws-cdk-lib/aws-cognito'; +import * as ddb from 'aws-cdk-lib/aws-dynamodb'; export interface UseCaseBuilderProps { userPool: UserPool; api: RestApi; - useCaseBuilderTable: Table; - useCaseIdIndexName: string; } export class UseCaseBuilder extends Construct { constructor(scope: Construct, id: string, props: UseCaseBuilderProps) { super(scope, id); - const { userPool, api, useCaseBuilderTable, useCaseIdIndexName } = props; + const { userPool, api } = props; + + const useCaseIdIndexName = 'UseCaseIdIndexName'; + const useCaseBuilderTable = new ddb.Table(this, 'UseCaseBuilderTable', { + partitionKey: { + name: 'id', + type: ddb.AttributeType.STRING, + }, + sortKey: { + name: 'dataType', + type: ddb.AttributeType.STRING, + }, + billingMode: ddb.BillingMode.PAY_PER_REQUEST, + }); + + useCaseBuilderTable.addGlobalSecondaryIndex({ + indexName: useCaseIdIndexName, + partitionKey: { + name: 'useCaseId', + type: ddb.AttributeType.STRING, + }, + sortKey: { + name: 'dataType', + type: ddb.AttributeType.STRING, + }, + projectionType: ddb.ProjectionType.ALL, + }); const commonProperty: NodejsFunctionProps = { runtime: Runtime.NODEJS_18_X, timeout: Duration.minutes(15), environment: { USECASE_TABLE_NAME: useCaseBuilderTable.tableName, + USECASE_ID_INDEX_NAME: useCaseIdIndexName, }, }; @@ -60,10 +85,6 @@ export class UseCaseBuilder extends Construct { const getUseCaseFunction = new NodejsFunction(this, 'GetUseCase', { ...commonProperty, entry: `${commonPath}/getUseCase.ts`, - environment: { - ...commonProperty.environment, - USECASE_ID_INDEX_NAME: useCaseIdIndexName, - }, }); useCaseBuilderTable.grantReadData(getUseCaseFunction); @@ -103,10 +124,6 @@ export class UseCaseBuilder extends Construct { { ...commonProperty, entry: `${commonPath}/listRecentlyUsedUseCases.ts`, - environment: { - ...commonProperty.environment, - USECASE_ID_INDEX_NAME: useCaseIdIndexName, - }, } ); useCaseBuilderTable.grantReadData(listRecentlyUsedUseCasesFunction); diff --git a/packages/cdk/lib/generative-ai-use-cases-stack.ts b/packages/cdk/lib/generative-ai-use-cases-stack.ts index b2bdbb32..0eb093bb 100644 --- a/packages/cdk/lib/generative-ai-use-cases-stack.ts +++ b/packages/cdk/lib/generative-ai-use-cases-stack.ts @@ -218,8 +218,6 @@ export class GenerativeAiUseCasesStack extends Stack { new UseCaseBuilder(this, 'UseCaseBuilder', { userPool: auth.userPool, api: api.api, - useCaseBuilderTable: database.useCaseBuilderTable, - useCaseIdIndexName: database.useCaseIdIndexName, }); } diff --git a/packages/types/src/protocolUseCaseBuilder.d.ts b/packages/types/src/protocolUseCaseBuilder.d.ts index dd3ba4fd..b9d9c255 100644 --- a/packages/types/src/protocolUseCaseBuilder.d.ts +++ b/packages/types/src/protocolUseCaseBuilder.d.ts @@ -1,36 +1,20 @@ import { - CustomUseCase, - CustomUseCaseMeta, - UseCaseInputExample, + UseCaseContent, + UseCaseAsOutput, + IsFavorite, + IsShared, } from './useCaseBuilder'; -export type ListUseCasesRespose = CustomUseCaseMeta[]; -export type ListFavoriteUseCasesResponse = CustomUseCaseMeta[]; -export type ListRecentlyUsedUseCasesResponse = CustomUseCaseMeta[]; +export type ListUseCasesRespose = UseCaseAsOutput[]; +export type ListFavoriteUseCasesResponse = UseCaseAsOutput[]; +export type ListRecentlyUsedUseCasesResponse = UseCaseAsOutput[]; -export type GetUseCaseResponse = CustomUseCase; +export type GetUseCaseResponse = UseCaseAsOutput; -export type CreateUseCaseRequest = { - title: string; - description?: string; - promptTemplate: string; - inputExamples?: UseCaseInputExample[]; -}; -export type CreateUseCaseRespose = { - useCaseId: string; -}; +export type CreateUseCaseRequest = UseCaseContent; +export type CreateUseCaseRespose = UseCaseAsOutput; -export type UpdateUseCaseRequest = { - title: string; - description?: string; - promptTemplate: string; - inputExamples?: UseCaseInputExample[]; -}; +export type UpdateUseCaseRequest = UseCaseContent; -export type ToggleFavoriteResponse = { - isFavorite: boolean; -}; - -export type ToggleShareResponse = { - isShared: boolean; -}; +export type ToggleFavoriteResponse = IsFavorite; +export type ToggleShareResponse = IsShared; diff --git a/packages/types/src/text.d.ts b/packages/types/src/text.d.ts index d8a639c7..c55ce3f6 100644 --- a/packages/types/src/text.d.ts +++ b/packages/types/src/text.d.ts @@ -68,7 +68,7 @@ export type ClaudeMessageParams = { top_p?: number; }; -// Llama2 +// Llama // https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-meta.html export type LlamaParams = { prompt?: string; @@ -166,7 +166,7 @@ export type BedrockResponse = { }; // Titan outputText: string; - // Llama2 + // Llama generation: string; // Mistral outputs: { diff --git a/packages/types/src/useCaseBuilder.d.ts b/packages/types/src/useCaseBuilder.d.ts index 9f813206..dfbe244b 100644 --- a/packages/types/src/useCaseBuilder.d.ts +++ b/packages/types/src/useCaseBuilder.d.ts @@ -3,47 +3,42 @@ export type UseCaseInputExample = { examples: Record; }; -export type TableUseCase = { +// 全てのデータで共通の項目 +// Table: PartitionKey=id, SortKey=dataType +// Index: PartitionKey=useCaseId, SortKey=dataType +export type UseCaseCommon = { id: string; + dataType: string; useCaseId: string; - title: string; - description?: string; - promptTemplate: string; - inputExamples?: UseCaseInputExample[]; - hasShared: boolean; -}; -export type TableFavoriteUseCase = { - id: string; - useCaseId: string; -}; -export type TableRecentlyUseedUseCases = { - id: string; - useCaseId: 'recently'; - recentUseIds: string[]; }; -export type CustomUseCaseMeta = { - useCaseId: string; +// ユースケースの内容 (ユースケース作成やアップデート時のリクエスト型) +export type UseCaseContent = { title: string; description?: string; - isFavorite: boolean; - hasShared: boolean; - isMyUseCase?: boolean; -}; - -export type CustomUseCase = CustomUseCaseMeta & { promptTemplate: string; inputExamples?: UseCaseInputExample[]; }; -export type UseCaseId = { - useCaseId: string; +// Table に記録されている内容 +export type UseCaseInTable = UseCaseCommon & + UseCaseContent & { + isShared: boolean; + }; + +// Frontend に返される内容 +// isFavorite, isMyUseCase は動的に付与 +export type UseCaseAsOutput = UseCaseInTable & { + isFavorite: boolean; + isMyUseCase: boolean; }; +// お気に入り Toggle のレスポンス用 export type IsFavorite = { isFavorite: boolean; }; -export type HasShared = { - hasShared: boolean; +// 共有 Toggle のレスポンス用 +export type IsShared = { + isShared: boolean; }; diff --git a/packages/web/src/App.tsx b/packages/web/src/App.tsx index ab99796e..c0f99559 100644 --- a/packages/web/src/App.tsx +++ b/packages/web/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React, { useMemo, useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import { PiList, @@ -33,7 +33,7 @@ import useChatList from './hooks/useChatList'; import PopupInterUseCasesDemo from './components/PopupInterUseCasesDemo'; import useInterUseCases from './hooks/useInterUseCases'; import { MODELS } from './hooks/useModel'; -import useObserveScreen from './hooks/useObserveScreen'; +import useScreen from './hooks/useScreen'; const ragEnabled: boolean = import.meta.env.VITE_APP_RAG_ENABLED === 'true'; const ragKnowledgeBaseEnabled: boolean = @@ -219,7 +219,8 @@ const App: React.FC = () => { const { pathname } = useLocation(); const { getChatTitle } = useChatList(); const { isShow } = useInterUseCases(); - const { handleScroll } = useObserveScreen(); + const { screen, notifyScreen, scrollTopAnchorRef, scrollBottomAnchorRef } = + useScreen(); const label = useMemo(() => { const chatId = extractChatId(pathname); @@ -231,11 +232,20 @@ const App: React.FC = () => { } }, [pathname, getChatTitle]); + // 画面間遷移時にスクロールイベントが発火しない場合 (ページ最上部からページ最上部への移動など) + // 最上部/最下部の判定がされないので、pathname の変化に応じて再判定する + useEffect(() => { + if (screen.current) { + notifyScreen(screen.current); + } + }, [pathname, screen, notifyScreen]); + return (
+ ref={screen}>
+
+
); diff --git a/packages/web/src/UseCaseBuilderRoot.tsx b/packages/web/src/UseCaseBuilderRoot.tsx index 3a7b3d6c..d254b524 100644 --- a/packages/web/src/UseCaseBuilderRoot.tsx +++ b/packages/web/src/UseCaseBuilderRoot.tsx @@ -1,56 +1,45 @@ import React, { useMemo } from 'react'; -import { useLocation } from 'react-router-dom'; -import { PiList, PiX, PiHammer } from 'react-icons/pi'; -import { Outlet } from 'react-router-dom'; +import { PiList, PiX, PiSwatches, PiListDashes } from 'react-icons/pi'; +import { Outlet, useLocation } from 'react-router-dom'; import { ItemProps } from './components/Drawer'; import ButtonIcon from './components/ButtonIcon'; import '@aws-amplify/ui-react/styles.css'; import useDrawer from './hooks/useDrawer'; -import useChatList from './hooks/useChatList'; import PopupInterUseCasesDemo from './components/PopupInterUseCasesDemo'; import useInterUseCases from './hooks/useInterUseCases'; -import useObserveScreen from './hooks/useObserveScreen'; import UseCaseBuilderDrawer from './components/useCaseBuilder/UseCaseBuilderDrawer'; - -const items: ItemProps[] = [ - { - label: 'ビルダーコンソール', - to: '/use-case-builder', - icon: , - display: 'usecase' as const, - }, -].flatMap((i) => (i !== null ? [i] : [])); - -// /chat/:chatId の形式から :chatId を返す -// path が別の形式の場合は null を返す -const extractChatId = (path: string): string | null => { - const pattern = /\/chat\/(.+)/; - const match = path.match(pattern); - - return match ? match[1] : null; -}; +import { ROUTE_INDEX_USE_CASE_BUILDER } from './main'; const UseCaseBuilderRoot: React.FC = () => { const { switchOpen: switchDrawer, opened: isOpenDrawer } = useDrawer(); - const { pathname } = useLocation(); - const { getChatTitle } = useChatList(); const { isShow } = useInterUseCases(); - const { handleScroll } = useObserveScreen(); + const { pathname } = useLocation(); - const label = useMemo(() => { - const chatId = extractChatId(pathname); + const items = useMemo( + () => + [ + { + label: 'サンプル集', + to: ROUTE_INDEX_USE_CASE_BUILDER, + icon: , + display: 'usecase' as const, + }, + { + label: 'マイユースケース', + to: `${ROUTE_INDEX_USE_CASE_BUILDER}/my-use-case`, + icon: , + display: 'usecase' as const, + }, + ].flatMap((i) => (i !== null ? [i] : [])), + [] + ); - if (chatId) { - return getChatTitle(chatId) || ''; - } else { - return items.find((i) => i.to === pathname)?.label || ''; - } - }, [pathname, getChatTitle]); + const label = useMemo(() => { + return items.find((i) => i.to === pathname)?.label || ''; + }, [items, pathname]); return ( -
+
diff --git a/packages/web/src/components/ChatList.tsx b/packages/web/src/components/ChatList.tsx index 29bd00df..69674890 100644 --- a/packages/web/src/components/ChatList.tsx +++ b/packages/web/src/components/ChatList.tsx @@ -4,11 +4,9 @@ import useChatList from '../hooks/useChatList'; import { useNavigate, useParams } from 'react-router-dom'; import ChatListItem from './ChatListItem'; import { decomposeId } from '../utils/ChatUtils'; -import { ROUTE_INDEX_USE_CASE_BUILDER } from '../main'; type Props = BaseProps & { searchWords: string[]; - isUseCaseBuilder?: boolean; }; const ChatList: React.FC = (props) => { @@ -19,14 +17,12 @@ const ChatList: React.FC = (props) => { const onDelete = useCallback( (_chatId: string) => { - navigate(props.isUseCaseBuilder ? ROUTE_INDEX_USE_CASE_BUILDER : '/chat'); + navigate('/chat'); return deleteChat(_chatId).catch(() => { - navigate( - `${props.isUseCaseBuilder ? ROUTE_INDEX_USE_CASE_BUILDER : ''}/chat/${_chatId}` - ); + navigate(`/chat/${_chatId}`); }); }, - [deleteChat, navigate, props.isUseCaseBuilder] + [deleteChat, navigate] ); const onUpdateTitle = useCallback( @@ -66,7 +62,6 @@ const ChatList: React.FC = (props) => { onDelete={onDelete} onUpdateTitle={onUpdateTitle} highlightWords={props.searchWords} - isUseCaseBuilder={props.isUseCaseBuilder} /> ); })} diff --git a/packages/web/src/components/ChatListItem.tsx b/packages/web/src/components/ChatListItem.tsx index 94ae7f67..743e34cb 100644 --- a/packages/web/src/components/ChatListItem.tsx +++ b/packages/web/src/components/ChatListItem.tsx @@ -14,7 +14,6 @@ import { Chat } from 'generative-ai-use-cases-jp'; import { decomposeId } from '../utils/ChatUtils'; import DialogConfirmDeleteChat from './DialogConfirmDeleteChat'; import HighlightWithinTextarea from 'react-highlight-within-textarea'; -import { ROUTE_INDEX_USE_CASE_BUILDER } from '../main'; type Props = BaseProps & { active: boolean; @@ -24,7 +23,6 @@ type Props = BaseProps & { // eslint-disable-next-line @typescript-eslint/no-explicit-any onUpdateTitle: (chatId: string, title: string) => Promise; highlightWords: string[]; - isUseCaseBuilder?: boolean; }; const ChatListItem: React.FC = (props) => { @@ -107,7 +105,7 @@ const ChatListItem: React.FC = (props) => { props.active && 'bg-aws-sky' } ${props.className}`} - to={`${props.isUseCaseBuilder ? ROUTE_INDEX_USE_CASE_BUILDER : ''}/chat/${chatId}`}> + to={`/chat/${chatId}`}>
diff --git a/packages/web/src/components/ChatMessage.tsx b/packages/web/src/components/ChatMessage.tsx index eebaf8f1..72ebe4db 100644 --- a/packages/web/src/components/ChatMessage.tsx +++ b/packages/web/src/components/ChatMessage.tsx @@ -3,8 +3,9 @@ import { useLocation } from 'react-router-dom'; import Markdown from './Markdown'; import ButtonCopy from './ButtonCopy'; import ButtonFeedback from './ButtonFeedback'; +import ButtonIcon from './ButtonIcon'; import ZoomUpImage from './ZoomUpImage'; -import { PiUserFill, PiChalkboardTeacher } from 'react-icons/pi'; +import { PiUserFill, PiChalkboardTeacher, PiFloppyDisk } from 'react-icons/pi'; import { BaseProps } from '../@types/common'; import { ShownMessage, @@ -22,6 +23,8 @@ type Props = BaseProps & { chatContent?: ShownMessage; loading?: boolean; hideFeedback?: boolean; + setSaveSystemContext?: (s: string) => void; + setShowSystemContextModal?: (value: boolean) => void; }; const ChatMessage: React.FC = (props) => { @@ -146,7 +149,7 @@ const ChatMessage: React.FC = (props) => {
)} -
+
{chatContent?.trace && (
@@ -217,16 +220,23 @@ const ChatMessage: React.FC = (props) => { )} {chatContent?.role === 'assistant' && ( -
+
{chatContent?.llmType}
)}
-
- {(chatContent?.role === 'user' || chatContent?.role === 'system') && ( -
+
+ {chatContent?.role === 'system' && ( + { + props.setSaveSystemContext?.(chatContent?.content || ''); + props.setShowSystemContextModal?.(true); + }}> + + )} {chatContent?.role === 'assistant' && !props.loading && diff --git a/packages/web/src/components/Drawer.tsx b/packages/web/src/components/Drawer.tsx index ce807486..683d0bf8 100644 --- a/packages/web/src/components/Drawer.tsx +++ b/packages/web/src/components/Drawer.tsx @@ -8,7 +8,8 @@ import ExpandableMenu from './ExpandableMenu'; import ChatList from './ChatList'; import DrawerItem, { DrawerItemProps } from './DrawerItem'; import DrawerBase from './DrawerBase'; -import DrawerTabs from './DrawerTabs'; +import Switch from './Switch'; +import { ROUTE_INDEX_USE_CASE_BUILDER } from '../main'; export type ItemProps = DrawerItemProps & { display: 'usecase' | 'tool' | 'summit' | 'none'; @@ -79,26 +80,21 @@ const Drawer: React.FC = (props) => { const useCaseBuilderEnabled: boolean = import.meta.env.VITE_APP_USE_CASE_BUILDER_ENABLED === 'true'; - const tabItems = useMemo(() => { - return [ - { - label: 'GenU', - isActive: true, - }, - { - label: 'ユースケースビルダー', - onClick: () => { - navigate('/use-case-builder'); - }, - }, - ]; - }, [navigate]); - return ( <> - {useCaseBuilderEnabled && } -
+ {useCaseBuilderEnabled && ( + { + navigate(ROUTE_INDEX_USE_CASE_BUILDER); + }} + /> + )} +
+
ユースケース (生成 AI)
diff --git a/packages/web/src/components/DrawerTabs.tsx b/packages/web/src/components/DrawerTabs.tsx deleted file mode 100644 index 1443a1e7..00000000 --- a/packages/web/src/components/DrawerTabs.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import { BaseProps } from '../@types/common'; - -type Props = BaseProps & { - items: { - label: string; - isActive?: boolean; - onClick?: () => void; - }[]; -}; - -const DrawerTabs: React.FC = (props) => { - return ( -
- {props.items.map((item, idx) => { - return ( -
{ - item.onClick ? item.onClick() : null; - }}> -
- {item.label} -
-
- ); - })} -
- ); -}; - -export default DrawerTabs; diff --git a/packages/web/src/components/ModalSystemContext.tsx b/packages/web/src/components/ModalSystemContext.tsx new file mode 100644 index 00000000..3260b82b --- /dev/null +++ b/packages/web/src/components/ModalSystemContext.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import ModalDialog from './ModalDialog'; +import { BaseProps } from '../@types/common'; +import Textarea from './Textarea'; +import Button from './Button'; + +type Props = BaseProps & { + showSystemContextModal: boolean; + saveSystemContext: string; + saveSystemContextTitle: string; + setShowSystemContextModal: (show: boolean) => void; + setSaveSystemContext: (systemContext: string) => void; + setSaveSystemContextTitle: (title: string) => void; + onCreateSystemContext: () => void; +}; + +const ModalSystemContext: React.FC = (props) => { + return ( + <> + { + props.setShowSystemContextModal(false); + }}> +
タイトル
+ +