DifyとAmazon Kendraを組み合わせて社内向けRAGチャットを構築する

## はじめに

Difyは、ノーコード・ローコードで手軽に生成AIアプリを構築できるOSSです。

https://github.com/langgenius/dify

ノーコード・ローコード系の生成AIアプリ開発OSSとしてはほかにもFlowiseやlangflowがありますが、Difyはここ最近急速に注目を集めるようになり、他二つをGithubのStar数で追い抜きました。

[![Star History Chart](https://api.star-history.com/svg?repos=FlowiseAI/Flowise,langflow-ai/langflow,langgenius/dify&type=Date)](https://star-history.com/#FlowiseAI/Flowise&langflow-ai/langflow&langgenius/dify&Date)

そんなDifyですが、非常に生成AIアプリを構築できるものの、2024/05/12現在ではRAGを実現する際に利用できるデータソースが

- 内蔵 Vector Store への個別ドキュメントの追加
- Notion
- パブリックなWeb検索 (Google, DuckDuckGo, etc) や、Wikipedia等のサイトの検索をツールとして利用

に限定されており、社内の大量のナレッジからデータを検索して回答する自社専用RAGを組み込み機能で実現することは難しいです。

そこで、本記事では、Difyのカスタムツール機能を用いてAmazon Kendraを検索可能にし、社内向けのRAGチャットを構築する手順を示していきます。

### RAGチャットを立てたいだけなら : GenUもご検討ください

なお、RAGチャットを構築する方法としては、 [Generative AI Use Cases JP (通称GenU)](https://github.com/aws-samples/generative-ai-use-cases-jp/) が高機能かつ手軽な方法となっています。デプロイ手順や利用方法を丁寧に解説した[ワークショップ](https://catalog.workshops.aws/generative-ai-use-cases-jp/ja-JP)も公開されているので、単に社内ナレッジを用いたRAGを実現したい場合はそちらがおすすめです。

## 本記事について

本記事では、以下の内容を取り扱います。

- 構築済みのKendraインデックスに、Amazon API Gateway + AWS Lambdaを用いてクエリするためのWebAPIを構築する(with AWS SAM)
- DifyのツールとしてKendraを追加し、RAGチャットを実現する

本記事では、Kendraインデックスの構築や、Difyの利用開始方法については取り扱いません。それぞれ、以下の参考記事をご参照ください。

- Kendraインデックスの構築
  - [AWS Hands-on for Beginners「Amazon Kendra で簡単に検索システムを作ろう！」](https://pages.awscloud.com/JAPAN-event-OE-Hands-on-for-Beginners-Amazon-Kendra-2023-reg-event.html) : 無料で視聴可能なオンデマンドセミナー
  - また、前述の[GenUのデプロイ手順](https://catalog.workshops.aws/generative-ai-use-cases-jp/ja-JP)のなかで構築することも可能です。
  - 今回は、サンプルデータとしてAWS関連資料と架空の就業規則(wordファイル)をS3に格納してクロールしています。
- Difyの利用開始
  - DifyはSaaS版を利用することも可能ですが、AWS上でDifyを立ち上げることも可能です。Amazon EC2上でdocker-composeを用いてDifyを稼働させる方法については、以下の記事が参考になります。なお、同記事ではLiteLLMを利用することでEC2のIAMロールでBedrockを呼べるようにしていますが、その方法でBedrockを利用するとワークフロー等でうまく動作しないケースがあったため、私はIAMユーザーのアクセスキーを用いて検証を行っています。

https://qiita.com/moritalous/items/246782c67a256f41f6ed

また、作業環境としてはMacOSを想定しています。ターミナルのコマンド等は作業環境に応じて変更してください。


## 前半 : 構築済みのKendraインデックスに、Amazon API Gateway + AWS Lambdaを用いてクエリするためのWebAPIを構築する(with AWS SAM)

今回の構築では[AWS SAM](https://aws.amazon.com/jp/serverless/sam/)を使います。

### 事前準備

- AWS SAMの事前準備 : [ドキュメント](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/prerequisites.html)の記載に則って実行
- Amazon KendraインデックスのIndex Idを控えておく
  - 例 : `01234567-89ab-cdef-0123-456789abcdef`

### sam initで雛形を作る

ターミナルで以下を実行します。

```bash 
cd <作業用のディレクトリ>
sam init
```

`sam init` では対話的に設定の入力を求められるため、ミニマムな設定で構築をしていきます。以下のもの以外はデフォルト(=何も入力せずEnter)で構いません。
- テンプレートソース : 1 - AWS Quick Start Templates
- クイックスタートテンプレート : 1 - Hello World Example
- ランタイム : 17 - python3.12
- パッケージタイプ : 1 - Zip
- プロジェクト名 : dify-tools-api (任意)

CLIのインタラクションの内容も参考までに記載しておきます。

<details><summary>SAM CLIのインタラクション</summary>

```plain
You can preselect a particular runtime or package type when using the `sam init` experience.
Call `sam init --help` to learn more.

Which template source would you like to use?
	1 - AWS Quick Start Templates
	2 - Custom Template Location
Choice: 1

Choose an AWS Quick Start application template
	1 - Hello World Example
	2 - Data processing
	3 - Hello World Example with Powertools for AWS Lambda
	4 - Multi-step workflow
	5 - Scheduled task
	6 - Standalone function
	7 - Serverless API
	8 - Infrastructure event management
	9 - Lambda Response Streaming
	10 - Serverless Connector Hello World Example
	11 - Multi-step workflow with Connectors
	12 - GraphQLApi Hello World Example
	13 - Full Stack
	14 - Lambda EFS example
	15 - DynamoDB Example
	16 - Machine Learning
Template: 1

Use the most popular runtime and package type? (Python and zip) [y/N]: n

Which runtime would you like to use?
	1 - aot.dotnet7 (provided.al2)
	2 - dotnet8
	3 - dotnet6
	4 - go (provided.al2)
	5 - go (provided.al2023)
	6 - graalvm.java11 (provided.al2)
	7 - graalvm.java17 (provided.al2)
	8 - java21
	9 - java17
	10 - java11
	11 - java8.al2
	12 - nodejs20.x
	13 - nodejs18.x
	14 - nodejs16.x
	15 - python3.9
	16 - python3.8
	17 - python3.12
	18 - python3.11
	19 - python3.10
	20 - ruby3.3
	21 - ruby3.2
	22 - rust (provided.al2)
	23 - rust (provided.al2023)
Runtime: 17

What package type would you like to use?
	1 - Zip
	2 - Image
Package type: 1

Based on your selections, the only dependency manager available is pip.
We will proceed copying the template using pip.

Would you like to enable X-Ray tracing on the function(s) in your application?  [y/N]: n

Would you like to enable monitoring using CloudWatch Application Insights?
For more info, please view https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch-application-insights.html [y/N]: n

Would you like to set Structured Logging in JSON format on your Lambda functions?  [y/N]: n

Project name [sam-app]: dify-tools-api

    -----------------------
    Generating application:
    -----------------------
    Name: dify-tools-api
    Runtime: python3.12
    Architectures: x86_64
    Dependency Manager: pip
    Application Template: hello-world
    Output Directory: .
    Configuration file: dify-tools-api/samconfig.toml
    
    Next steps can be found in the README file at dify-tools-api/README.md
        

Commands you can use next
=========================
[*] Create pipeline: cd dify-tools-api && sam pipeline init --bootstrap
[*] Validate SAM template: cd dify-tools-api && sam validate
[*] Test Function in the Cloud: cd dify-tools-api && sam sync --stack-name {stack-name} --watch
```

</details>

すると、入力したプロジェクト名のフォルダが作成され、プロジェクトの雛形が出来上がります。

### Lambda関数の作成

今回のAPI構築では、2つのLambda関数を構築します。

- retrieve : Kendraへの検索を実行する
- authorizer : API Gatewayの[Lambdaオーソライザー](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html)として、Basic認証のチェックを行う
  - APIを認証なしで公開することはセキュリティリスクがあるため、本記事では簡易にBasic認証で制限することとします。

SAMの生成したフォルダ配下に`retrieve`と`authorizer`の二つのフォルダを作成します。

```bash
cd dify-tools-api # 入力したプロジェクト名のフォルダに移動する
mkdir authorizer retrieve
```

それぞれのフォルダ配下に、各Lambda関数のアプリのコードを`app.py`というファイル名で配置します。

```python:retrieve/app.py
import os
import json
import boto3

INDEX_ID = os.environ.get('INDEX_ID')

def lambda_handler(event, context):
    try:
        body = json.loads(event.get('body', '{}'))
        print(f"body : {body}")
        query = body.get('query')

        if not query:
            return {
                'statusCode': 400,
                'headers': {
                    'Content-Type': 'application/json',
                    'Access-Control-Allow-Origin': '*'
                },
                'body': json.dumps({'error': 'query is not specified'})
            }

        # デフォルト言語が英語なので、言語設定は必ず行う
        attribute_filter = {
            'AndAllFilters': [
                {
                    'EqualsTo': {
                        'Key': '_language_code',
                        'Value': {
                            'StringValue': 'ja'
                        }
                    }
                }
            ]
        }

        kendra_client = boto3.client('kendra')
        response = kendra_client.retrieve(
            IndexId=INDEX_ID,
            QueryText=query,
            AttributeFilter=attribute_filter
        )
        print(response)
        response.pop("ResponseMetadata")

        return {
            'statusCode': 200,
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            'body': json.dumps(response)
        }
    except Exception as e:
        print(e)
        return {
            'statusCode': 500,
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            'body': json.dumps({'error': str(e)})
        }
```

なお、こちらのソースコードは、[GenU](https://github.com/aws-samples/generative-ai-use-cases-jp/tree/main)で使われているretrieve用の関数を参考にしています。

```python:authorizer/app.py
import base64
import os

AUTHORIZED_USERNAME = os.environ.get('BASIC_AUTH_USER')
AUTHORIZED_PASSWORD = os.environ.get('BASIC_AUTH_PASSWORD')

def lambda_handler(event, context):
    print(event)
    authorization_header = event.get('authorizationToken', None)

    if not authorization_header:
        return 'Unauthorized'

    encoded_creds = authorization_header.split(" ")[1]
    plain_creds = base64.b64decode(encoded_creds).decode('utf-8').split(':')
    username = plain_creds[0]
    password = plain_creds[1]

    if not (username == AUTHORIZED_USERNAME and password == AUTHORIZED_PASSWORD):
        return 'Unauthorized'

    auth_response = build_allow_all_policy(event, username)

    return auth_response

def build_allow_all_policy(event, principal_id):
    policy = {
        'principalId': principal_id,
        'policyDocument': {
            'Version': '2012-10-17',
            'Statement': [
                {
                    'Action': 'execute-api:Invoke',
                    'Effect': 'Allow',
                    'Resource': event['methodArn']
                }
            ]
        }
    }
    return policy
```

なお、authorizerの実装はこちらの記事の実装を参考にさせていただきました。
https://dev.classmethod.jp/articles/apigateway-lambda-basicauth/

### SAMテンプレートの作成

これらのLambda関数を用いて、KendraにクエリするAPIを構築するためのSAMテンプレートを記述します。`template.yaml` ファイルを以下のように書き換えます。


```yaml:template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  Dify Tools Api

Parameters:
  IndexId:
    Type: String
  BasicAuthUser:
    Type: String
  BasicAuthPassword:
    Type: String
    NoEcho: true

Globals:
  Function:
    Timeout: 30
    Runtime: python3.12
    Architectures:
      - x86_64
          

Resources:
  DifyApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: dev
      Auth:
        DefaultAuthorizer: MyLambdaAuthorizer
        Authorizers:
          MyLambdaAuthorizer:
            FunctionArn: !GetAtt AuthorizerFunction.Arn
  RetrieveFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: retrieve/
      Handler: app.lambda_handler
      Environment:
        Variables:
          INDEX_ID: !Ref IndexId
      Policies: 
        - Statement:
          - Sid: KendraRetrievalPolicy
            Effect: Allow
            Action: 
            - kendra:Retrieve
            Resource: !Sub "arn:aws:kendra:${AWS::Region}:${AWS::AccountId}:index/${IndexId}"
      Events:
        Retrieve:
          Type: Api
          Properties:
            Path: /retrieve
            Method: post
            RestApiId: !Ref DifyApi
  AuthorizerFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: authorizer/
      Handler: app.lambda_handler
      Environment:
        Variables:
          BASIC_AUTH_USER: !Ref BasicAuthUser
          BASIC_AUTH_PASSWORD: !Ref BasicAuthPassword
          


Outputs:
  DifyApi:
    Description: "Kendra retrieve endpoint URL"
    Value: !Sub "https://${DifyApi}.execute-api.${AWS::Region}.amazonaws.com/dev/retrieve"
  BasicAuthCredential:
    Description: Credential for Basic authentication
    Value: 
      Fn::Base64: !Sub ${BasicAuthUser}:${BasicAuthPassword}
```

### SAM CLIでビルドとデプロイを実行

ここまでできたら、以下のコマンドを実行して、アプリケーションのビルドとデプロイを行います。

```bash
sam build # ビルド
sam validate # テンプレートの検証
sam deploy --guided # デプロイ
```

最後のコマンドでは、各種オプションを対話的に入力していきます。以下のパラメータと最後の実行確認を除き、特に指定がなければデフォルト(=何も入力せずにEnter)で問題ありません。

```
	Parameter IndexId []: 01234567-89ab-cdef-0123-456789abcdef <== Kendra インデックスの ID を入力
	Parameter BasicAuthUser []: admin <== Basic 認証のユーザー名を入力
	Parameter BasicAuthPassword:    <== Basic 認証のパスワードを入力 ※パスワードのため、入力は表示されない
```

CLIのインタラクションの内容も参考までに記載しておきます。

<details><summary>SAM CLIのインタラクション</summary>

```plain
Configuring SAM deploy
======================

	Looking for config file [samconfig.toml] :  Found
	Reading default arguments  :  Success

	Setting default arguments for 'sam deploy'
	=========================================
	Stack Name [dify-tools-api]: 
	AWS Region [ap-northeast-1]: 
	Parameter IndexId []: 01234567-89ab-cdef-0123-456789abcdef
	Parameter BasicAuthUser []: admin
	Parameter BasicAuthPassword: 
	#Shows you resources changes to be deployed and require a 'Y' to initiate deploy
	Confirm changes before deploy [Y/n]: 
	#SAM needs permission to be able to create roles to connect to the resources in your template
	Allow SAM CLI IAM role creation [Y/n]: 
	#Preserves the state of previously provisioned resources when an operation fails
	Disable rollback [y/N]: 
	Save arguments to configuration file [Y/n]: 
	SAM configuration file [samconfig.toml]: 
	SAM configuration environment [default]: 

	Looking for resources needed for deployment:

	Managed S3 bucket: aws-sam-cli-managed-default-samclisourcebucket-xxxxxxxxxxxx
	A different default S3 bucket can be set in samconfig.toml and auto resolution of buckets turned off by setting resolve_s3=False
                                                                                                                                                                                     
        Parameter "stack_name=dify-tools-api" in [default.deploy.parameters] is defined as a global parameter [default.global.parameters].                                           
        This parameter will be only saved under [default.global.parameters] in /Users/mabuchs/Desktop/codes/Common/dify-kendra-sam/repro/dify-tools-api/samconfig.toml.              

	Saved arguments to config file
	Running 'sam deploy' for future deployments will use the parameters saved above.
	The above parameters can be changed by modifying samconfig.toml
	Learn more about samconfig.toml syntax at 
	https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html

	Uploading to dify-tools-api/6db0de0fcb35611fc4e1e5c8cc9dcfe6  756 / 756  (100.00%)
	Uploading to dify-tools-api/2e0c3c7469f2a05e33c986a39a0c345b  594 / 594  (100.00%)

	Deploying with following values
	===============================
	Stack name                   : dify-tools-api
	Region                       : ap-northeast-1
	Confirm changeset            : True
	Disable rollback             : False
	Deployment s3 bucket         : aws-sam-cli-managed-default-samclisourcebucket-xxxxxxxxxxxx
	Capabilities                 : ["CAPABILITY_IAM"]
	Parameter overrides          : {"IndexId": "01234567-89ab-cdef-0123-456789abcdef", "BasicAuthUser": "admin", "BasicAuthPassword": "*****"}
	Signing Profiles             : {}

Initiating deployment
=====================

	Uploading to dify-tools-api/1470a61d1bb4355a39e1b97c784971be.template  2253 / 2253  (100.00%)


Waiting for changeset to be created..

CloudFormation stack changeset
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Operation                                    LogicalResourceId                            ResourceType                                 Replacement                                
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Add                                        AuthorizerFunctionRole                       AWS::IAM::Role                               N/A                                        
+ Add                                        AuthorizerFunction                           AWS::Lambda::Function                        N/A                                        
+ Add                                        DifyApiDeployment85f4beacf9                  AWS::ApiGateway::Deployment                  N/A                                        
+ Add                                        DifyApiMyLambdaAuthorizerAuthorizerPermiss   AWS::Lambda::Permission                      N/A                                        
                                             ion                                                                                                                                  
+ Add                                        DifyApidevStage                              AWS::ApiGateway::Stage                       N/A                                        
+ Add                                        DifyApi                                      AWS::ApiGateway::RestApi                     N/A                                        
+ Add                                        RetrieveFunctionRetrievePermissiondev        AWS::Lambda::Permission                      N/A                                        
+ Add                                        RetrieveFunctionRole                         AWS::IAM::Role                               N/A                                        
+ Add                                        RetrieveFunction                             AWS::Lambda::Function                        N/A                                        
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Changeset created successfully. arn:aws:cloudformation:ap-northeast-1:xxxxxxxxxxxx:changeSet/samcli-deploy1715616080/9c94087f-841c-4027-9a93-e9c1ea5e5ea2


Previewing CloudFormation changeset before deployment
======================================================
Deploy this changeset? [y/N]: y
```

うまく行けば、最後に以下のような出力がされます。  
※ `BasicAuthCredential` は、Basic認証のユーザー名・パスワードを`admin:password` とした場合の例

```
CloudFormation outputs from deployed stack
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Outputs                                                                                                                                                                          
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Key                 DifyApi                                                                                                                                                      
Description         Kendra retrieve endpoint URL                                                                                                                                 
Value               https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/retrieve                                                                                     

Key                 BasicAuthCredential                                                                                                                                          
Description         Credential for Basic authentication                                                                                                                          
Value               YWRtaW46cGFzc3dvcmQ=                                                                                                                                 
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Successfully created/updated stack - dify-tools-api in ap-northeast-1
```

</details>

結果として出力されるURLと認証情報は後で利用するので控えておくようにしてください。

### curlでAPIの実行を確認

無事に動くかどうか、curl で確認してみましょう。  
Basic認証の認証情報、検索ワード、APIのURLはご自身のものに併せて変更してください。

```bash
curl -X POST -H "Content-Type: application/json" \
-H "Authorization:Basic <BasicAuthCredentialで出力された値>" \
-d '{"query":"<任意の検索ワード>"}' \
https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/retrieve
```

うまくいけば、検索結果が返ってきます。curlではUTF-8がエンコードされずに表示されますが、実際の利用時には問題ありません。

## 後半 : DifyのツールとしてKendraを追加し、RAGチャットを実現する

Difyには、エージェントに与えるツールにカスタムツールを追加することができます。OpenAPI SpecificationとしてAPIのドキュメントを記載してDifyに渡してあげることで、Difyが回答に必要なアクションを取るためにAPIを実行することができるようになります。

### OpenAPIドキュメントの作成

OpenAPIドキュメントをイチから作成するのは大変なので、実際のリクエスト・レスポンスを例に、[GenU](https://github.com/aws-samples/generative-ai-use-cases-jp/tree/main)を使ってClaude3 Sonnetに書いてもらって加筆修正してみました。

<details><summary>参考 : APIドキュメントの作成 w/Claude3</summary>

GenUのチャットで、Claude3にお願いしてみます。プロンプトはこちらです。

```
以下の条件を元に、OpenAPI specificationのAPIドキュメントを生成してください。
- APIのベースURIはhttps://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev
- 用意されているpathは /retrieve のみで、メソッドは POST のみ
- Amazon Kendra (AWS の検索エンジン構築用サービス) に検索クエリを実行し、その結果を取得するための API 
- リクエストの例は <request> タグ内に、レスポンスの例は <response> タグ内に記述
- 各フィールドの説明も推察し、OpenAPI スキーマの description として埋め込む
- APIドキュメント内に example としてフィールドの値の例を含める
- リクエスト内の query フィールド以外は `required` アノテーションは不要


<request>
{"query":"服務規程"}
</request>
<response>
{
    "QueryId": "01053d86-80fd-44d1-9823-56054194755f",
    "ResultItems": [
...(中略)...
</response>
```

結果、以下のようにきちんとyamlを生成してくれています。

![image-16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3790935/666cdc5b-2d71-5af2-738f-1d06738e0598.png)


このyamlを2点のみ修正しました。

- `operationId` を付与
- Difyに使わせる際の注意事項を示すため、`description`にいくらか説明を追加

```
      description: |-
        Execute search query on internal search engine built on Amazon Kendra.
        It contains data in below categories:
          - Amazon Web Services
          - Office regulations of our company
        This only contains Japanese documents hence query string should be Japanese.
      operationId: kendraRetrieve
```

</details>

結果、できあがったAPIドキュメントがこちらです。以下の部分は適宜修正してください。
- `url` の部分はご自身の作成したAPIのURLに置き換えてください。
- `description` には、今回作成したKendraインデックス内にどういうドキュメントが含まれているかを説明しています。Difyで構築するエージェントがどういうケースでKendraを使うとよいのかを理解させるために入力しています。Kendraに含めているドキュメントに合わせて改変してください。

```yaml
openapi: 3.0.0
info:
  title: Dify Tools API
  version: 0.0.1

servers:
  - url: https://<API ID>.execute-api.<region>.amazonaws.com/dev

paths:
  /retrieve:
    post:
      summary: 検索クエリを Amazon Kendra に実行し、結果を取得する
      description: |-
        Execute search query on internal search engine built on Amazon Kendra.
        It contains data in below categories:
          - Amazon Web Services
          - Office regulations of our company
        This only contains Japanese documents hence query string should be Japanese.
      operationId: kendraRetrieve
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SearchRequest'
      responses:
        '200':
          description: 検索結果
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchResponse'

components:
  schemas:

    SearchRequest:
      type: object
      properties:
        query:
          type: string
          description: 検索クエリ
          example: "服務規程"
      required:
        - query

    SearchResponse:
      type: object
      properties:
        QueryId:
          type: string
          description: 検索クエリの一意の ID
          example: "01053d86-80fd-44d1-9823-56054194755f"
        ResultItems:
          type: array
          description: 検索結果の項目一覧
          items:
            $ref: '#/components/schemas/ResultItem'

    ResultItem:
      type: object
      properties:
        Id:
          type: string
          description: 検索結果項目の一意の ID
          example: "01053d86-80fd-44d1-9823-56054194755f-f9892cb3-d093-471c-81ba-7f4b5409425a"
        DocumentId:
          type: string
          description: ドキュメントの ID
          example: "s3://example-bucket-name/docs/kitei_01-1.doc"
        DocumentTitle:
          type: string
          description: ドキュメントのタイトル
          example: "kitei_01-1.doc"
        Content:
          type: string
          description: 検索にヒットした部分のドキュメントコンテンツ
          example: "（賃 金） 第５０条　賃金に関する事項については、「賃金規程」に定める。..."
        DocumentURI:
          type: string
          description: ドキュメントの URI
          example: "https://example-bucket-name.s3.ap-northeast-1.amazonaws.com/docs/kitei_01-1.doc"
        DocumentAttributes:
          type: array
          description: ドキュメントの属性一覧
          items:
            $ref: '#/components/schemas/DocumentAttribute'
        ScoreAttributes:
          $ref: '#/components/schemas/ScoreAttributes'

    DocumentAttribute:
      type: object
      properties:
        Key:
          type: string
          description: 属性のキー
          example: "_source_uri"
        Value:
          $ref: '#/components/schemas/AttributeValue'

    AttributeValue:
      type: object
      properties:
        StringValue:
          type: string
          description: 文字列値
          example: "https://example-bucket-name.s3.ap-northeast-1.amazonaws.com/docs/kitei_01-1.doc"

    ScoreAttributes:
      type: object
      properties:
        ScoreConfidence:
          type: string
          description: スコアの信頼度
          example: "NOT_AVAILABLE"
```

### Difyのカスタムツール登録

このyamlを使って、DifyにカスタムツールとしてKendra検索APIを認識させます。

Difyの画面から、上部メニュー「ツール」 > 左メニュー「カスタムツールを作成」に進みます。

![image-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3790935/41e4acf6-cc89-90c1-b194-2e288b1af393.png)

カスタムツールの作成ペインが表示されます。名前は適当に入力し(「Kendra」などで構いません)、スキーマの入力欄に前述のyamlをペーストします。  
さらに、Basic認証を利用するため、「認証情報」のボタンをクリックします。

![image-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3790935/52a187a3-4f7b-40ba-b604-1924ae9ba3c7.png)

認証タイプとしては「APIキー」、認証タイプとして「ベーシック」を選択し、値としてSAM実行時に出力された認証情報を入力し(画像は `admin:password` の場合)、保存します。

![image-4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3790935/ce6066e8-7083-e7e6-f6a6-3ed36ff39178.png)

ツールの設定が問題ないか確認するために、「テスト」ボタンでテストを行っていきます。

![image-5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3790935/eb269093-6301-2244-4b01-b7f0d92567b3.png)

「query」パラメータに検索ワードを入力し、「テスト」ボタンから検索を実行してみます。

![2024-05-14_11-27-47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3790935/6d9ebdd5-95cd-7a7c-9370-0eedc6913b9b.png)

DifyからKendraに検索できることが確認できました。最後に、ツールを保存します。

![image-8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3790935/4fc78a20-84cd-51e5-0a5e-cfe8ab78225c.png)

これで、DifyでKendraを使わせる準備が整いました。

### DifyのエージェントにKendraを使わせてRAGチャットを実現

それでは、このツールを使ってRAGチャットができるエージェントを作っていきましょう。スタジオ画面から、アプリを作成していきます。

![image-9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3790935/ffcd858f-41f7-a664-458e-d86d101b8bd3.png)

タイプとしてはエージェントを選択し、アプリの名前を設定して作成します。

![image-10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3790935/e0b42c66-6583-175d-a992-07d8d74cb7ec.png)

「手順」(instruction)には以下のプロンプトを入れています。

```
あなたは日本語でユーザーの問い合わせに対応することが出来るAIアシスタントです。回答は必ず日本語で返す必要があります。
回答時には、その根拠となる文書のURIと原文を末尾に注記してください。
```

そして、ツールの追加を行います。

![image-12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3790935/b23e5400-b45c-67d9-1532-1af19a2eed0e.png)

ツールの選択画面で「カスタム」を選択し、Kendra > kendraRetieve を見付けて追加します。追加したら追加画面を閉じます。

![image-11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3790935/528e1f61-069d-30b7-525b-8308a347d0d1.png)


これで作成が完了しました。実際に、Kendraに入れたドキュメントを使って回答してほしい質問をしてみます。

![image-14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3790935/e47e80ca-22b8-dc49-2b7e-43b7b5307e09.png)

Kendraに検索クエリを実行して回答してくれていることがわかります。

### 終わりに

Difyで社内ナレッジを使ったRAGを実現できるように、Amazon Kendraをインテグレーションする方法をご紹介しました。今後Difyの組み込み機能としてKendra等が統合される可能性に期待したいと思いますが、それを待たずとも本記事の方法でKendraの統合を試してみてください。

#### おまけ：所感など
- Difyのツールの作成方法として、本家のドキュメントには[稼働させるDifyのソースコードに直接手を入れる方法](https://docs.dify.ai/tutorials/quick-tool-integration)が説明されていますが、この方法だとどうしてもハードルが高いです。一方で、GUI上から追加するカスタムツールはOpenAPIで記述できるAPIしか追加できず、ワークフロー上の「コード」のノードもboto3が使えないので、ちょっとしたツールを作るにもソースに手を入れる or APIを生やす必要があるのはやや障壁が高いかなと感じました。
  - ただ、今回のようにAWS SAMなどを活用してAPI Gateway + Lambdaといった構成にすれば、簡単にAPIを作成することができるため、その障壁も低くすることができるかなと感じています。
- Difyのカスタムツールを使う際のAPIキーとしては「Basic」「Bearer」「カスタム」の3つの選択肢があります。が、Bearerトークンは有効期限が短いため、トークンの再発行等を考慮した場合、どう実用できるのかの想定が難しいと感じました。APIキー情報を内部のどこかのDBに持っていると思うので(未確認)、定期的にトークンをリフレッシュする処理を動かすなどが必要でしょうか。