Difyのツール機能を使ってシノニムマップ機能を足してみる

# はじめに
この記事をご覧いただいているということは、Difyに興味をお持ちの方、もしくはすでに使っている方かもしれませんね。たまたま目に留まって開いてみた、という方も大歓迎です！

Dify（ディファイ）は、ノーコードで生成AIアプリを作れるオープンソースのプラットフォームです。プログラミングの知識がなくても、直感的な操作でAIアプリを構築できるため、ビジネスユーザーや非エンジニアの方々にもおすすめです。

今回はそんなDifyというノーコードツールに独自の機能を足してみたいと思います。

# 参考文献など
https://docs.dify.ai/ja-jp/guides/tools

Difyには、`ツール`と呼ばれる機能が存在し、この機能で外部のAPIを呼び出したりできます。
今回はこのAPIを簡単に実装し、シノニムマップを作るところまで行きたいと思います。

# Difyを立ち上げる
https://docs.dify.ai/ja-jp/getting-started/install-self-hosted/docker-compose

上記を参考にDifyを立ち上げてログインできる状態にまでしておきます。

# APIを作る
今回使用するAPIフレームワークはFastAPIです。
こちらでサクッと実装します。

まずは、FastAPIのひな形を作ります。
ここで、`"url": "http://192.168.11.123:8000"`に設定してあるIPの内容を自分のローカルIPと合せてください。
```python
from fastapi import FastAPI

app = FastAPI(
    title="Dify Tool API",
    description="これはDifyのサンプルToolのAPIです。",
    servers=[
        {
            "url": "http://192.168.11.123:8000",  # 自分のローカルIPを指定
            "description": "Local server",
        }
    ],
)

@app.post("/api/synonym")
async def replace_synonym():
    return {"message": "Hello"}

```


# シノニムとは
今回の実装では、単純に置き換えるではなく、形態素解析をして置き換えます。
なぜこんなことをするか。
例えば、「確認」のシノニムマップを「チェック」としていた場合を考えてみましょう。

**部分一致による単純な置換**

単純に部分一致で「確認」を置き換える処理を行った場合、次のような誤動作が発生します。

- **入力**: "不確認状態です"  
- **出力**: "不チェック状態です"  

この例では、「確認」だけを部分一致で置き換えてしまったため、「不確認」という単語が分解されて不自然な日本語になっています。

**シノニム処理による改善**

形態素解析を活用することで、「不確認」全体を適切に解析し、「確認」のみをシノニム辞書に基づいて置き換えることができます。

- **入力**: "不確認状態です"  
- **出力**: "不確認状態です"  

この場合、「不確認」は一つの単語として認識され、シノニム辞書に存在しないため置換されません。

一方で、例えば「確認します」という文が入力された場合は次のように変換されます。

- **入力**: "確認します"  
- **出力**: "チェックします"  

形態素解析を用いることで、文脈に応じた正確な置換が可能となり、不自然な結果を回避できます。


# シノニム部分を実装する


それでは、辞書を作ります。
先ほどの例の`確認:チェック`を入れておきます。
```python
# シノニム辞書の定義
synonym_dict = {
    "確認": "チェック",
}
```



形態素分析器の最小トークンごとに置き換えてそれをつなぎ合わせて返却しようかと思いましたが、接頭辞がついている場合、例えば先ほどの例の"不確認"の場合、 "不"と"確認"に分類されてしまい思うようにいい結果が得られませんので、１文字＋名詞または形容詞の場合はそれを一単語とみなすようにします。
```python 
@Language.component("merge_prefixed_tokens")
def merge_prefixed_tokens(doc):
    """
    接頭辞を動的に判定し、次のトークンと結合する関数。
    """
    with doc.retokenize() as retokenizer:
        for i, token in enumerate(doc[:-1]):
            # 動的に接頭辞を判定（1文字であり、次のトークンが名詞/形容詞の場合）
            if len(token.text) == 1 and doc[i + 1].pos_ in {"NOUN", "ADJ"}:
                span = doc[i:i + 2]
                retokenizer.merge(span)
    return doc

# カスタムコンポーネントをパイプラインに追加
nlp.add_pipe("merge_prefixed_tokens", last=True)

```


API受け取り部分は、そのまま出力されたトークンごとに見て行って、シノニムにヒットするものが置き換えて結合し、文字列データを返します。

```python
@app.post("/api/synonym")
async def replace_synonym(text: str):
    """
    GiNZAを用いて文章を形態素解析し、ヒットした単語をシノニムマップの単語と置き換える
    """
    # GiNZAで形態素解析
    doc = nlp(text)

    # トークンをシノニム辞書で置き換えつつ結合
    replaced_text = "".join(
        synonym_dict.get(token.lemma_, token.text) for token in doc
    )

    return replaced_text
```

**全体コード**
```python
from fastapi import FastAPI
import spacy
from spacy.language import Language

app = FastAPI(
    title="Dify Tool API",
    description="これはDifyのサンプルToolのAPIです。",
    servers=[
        {
            "url": "http://192.168.11.123:8000",  # 自分のローカルIPを指定
            "description": "Local server",
        }
    ],
)

# GiNZAの日本語モデルをロード
nlp = spacy.load("ja_ginza")

@Language.component("merge_prefixed_tokens")
def merge_prefixed_tokens(doc):
    """
    接頭辞を動的に判定し、次のトークンと結合する関数。
    """
    with doc.retokenize() as retokenizer:
        for i, token in enumerate(doc[:-1]):
            # 動的に接頭辞を判定（1文字であり、次のトークンが名詞/形容詞の場合）
            if len(token.text) == 1 and doc[i + 1].pos_ in {"NOUN", "ADJ"}:
                span = doc[i:i + 2]
                retokenizer.merge(span)
    return doc

# カスタムコンポーネントをパイプラインに追加
nlp.add_pipe("merge_prefixed_tokens", last=True)

# シノニム辞書の例
synonym_dict = {
    "確認": "チェック",
}

@app.post("/api/synonym")
async def replace_synonym(text: str):
    """
    GiNZAを用いて文章を形態素解析し、ヒットした単語をシノニムマップの単語と置き換える
    """
    # GiNZAで形態素解析
    doc = nlp(text)

    # トークンをシノニム辞書で置き換えつつ結合
    replaced_text = "".join(
        synonym_dict.get(token.lemma_, token.text) for token in doc
    )

    return replaced_text
```

# 作ったAPIをテストする

```bash
curl --location --request POST 'http://127.0.0.1:8000/api/synonym?text=不確認状態です'
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418662/3ce62cfa-c239-5cde-dbe3-363a815b2da3.png)

上記のようになっていればOK

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418662/261dbb73-9512-7d75-c46f-8a673dae1e08.png)
👆の場合のみ変換されているので要件は満たせている。


よさそう！！

# Difyへのつなぎこみ
## 前準備
**openapiの出力**
まず、Difyで簡単にツールの登録ができるようにopenapiのコードを入れる必要があります。
幸い、FastAPIには、openapiを出力する機能があります。

```
http://127.0.0.1:8000/docs
```

のように/docsを付けるだけで、下記からダウンロードできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418662/be85f36b-f43d-d46c-db2f-6124e808aaa6.png)

:::note warn
注意
今回は単純なものなので、Difyがそのまま読み込めますが、JSONBodyで入力を期待したりすると軒並みエラーを吐きます...
:::

```yaml
{
    "openapi": "3.1.0",
    "info": {
        "title": "Dify Tool API",
        "description": "これはDifyのサンプルToolのAPIです。",
        "version": "0.1.0"
    },
    "servers": [
        {
            "url": "http://192.168.11.123:8000",
            "description": "Local server"
        }
    ],
    "paths": {
        "/api/synonym": {
            "post": {
                "summary": "Replace Synonym",
                "description": "GiNZAを用いて文章を形態素解析し、ヒットした単語をシノニムマップの単語と置き換える",
                "operationId": "replace_synonym_api_synonym_post",
                "parameters": [
                    {
                        "name": "text",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string",
                            "title": "Text"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Successful Response",
                        "content": {
                            "application/json": {
                                "schema": {}
                            }
                        }
                    },
                    "422": {
                        "description": "Validation Error",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/HTTPValidationError"
                                }
                            }
                        }
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "HTTPValidationError": {
                "properties": {
                    "detail": {
                        "items": {
                            "$ref": "#/components/schemas/ValidationError"
                        },
                        "type": "array",
                        "title": "Detail"
                    }
                },
                "type": "object",
                "title": "HTTPValidationError"
            },
            "ValidationError": {
                "properties": {
                    "loc": {
                        "items": {
                            "anyOf": [
                                {
                                    "type": "string"
                                },
                                {
                                    "type": "integer"
                                }
                            ]
                        },
                        "type": "array",
                        "title": "Location"
                    },
                    "msg": {
                        "type": "string",
                        "title": "Message"
                    },
                    "type": {
                        "type": "string",
                        "title": "Error Type"
                    }
                },
                "type": "object",
                "required": [
                    "loc",
                    "msg",
                    "type"
                ],
                "title": "ValidationError"
            }
        }
    }
}
```
こんなものが落とせたでしょうか？
この文字列をコピーします。

## Difyのツールに設定
では次にDifyの設定に進みます。
「ツール」を選択後「カスタムツールを作成する」を押下します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418662/3bfc647a-ce78-37a7-bc9d-a18ec27b3adc.png)

すると、、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418662/ad7e7590-e7c7-9478-5884-180616cf0c3c.png)
こんなものが出てきますので、`名前`を任意の好きな名前に、`スキーマ`を先ほどコピーしたopenapiのYAMLを貼り付けます。
※今回は認証設定をしていないので、`認証方法`は`なし`に設定します。

## Difyでのテスト
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418662/8fa50d48-cdc5-27c9-ab65-834cab1e6127.png)
上記を押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418662/05f1707d-d6be-5cd0-1088-cf101b7f6671.png)
text部分の値に、`確認するものがありません！`と入力すると、`チェックするものがありません！`が帰ってくるはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418662/543ece37-bbad-c833-fbb7-95e527da46ab.png)
ここまで出来たら、保存しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418662/26351190-4cf3-760c-d63e-b995584dcdd0.png)
こんなのができましたね。

## 実際に使用する
実際にスタジオから、チャットボットを作成します。
オプションとして、「Chatflow」を選択して作成してください。※これがないと今回のツールが使えません
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418662/f218babc-fb14-68b7-d226-14f96677786c.png)


今回作ったツールをノードエディタ上に追加し、下記のように入力変数を設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418662/307222e6-dacd-00b6-9a23-4035113e38ea.png)


LLMノードでは下記のようにUSER部分を編集します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418662/7e2a554c-c2fd-890c-c943-ea29b2118bc7.png)


下記のようになればOK
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418662/46f96683-278c-2122-142e-3be1a48fad1d.png)

ちなみに「ユーザーメッセージにsys.queryが必要です」と出ていますが無視できますので大丈夫です。

## プレビュー
プレビューを実行するとこうなります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418662/7fcb8eb9-7534-3b45-c08b-66196df79018.png)
 --
 
以上、Difyでシノニムマップを作ってみたでした。
他にも、この要領でいろんな機能を足して行けそうです。

また、簡易的なAPIひな形を作っておいておきます。
今回のシノニムの機能が入っています。
これをベースに機能を増やしていってもOK

**ちなみに**
JSON Bodyでリクエストを受け取るコードを作りFastAPIでOpenAPIを生成すると読み込まなくなりますが、
このテンプレートで、
```
poetry run generate-openapi
```
で出てきたOpenAPIはDifyにそのまま読み込ませられるようになっていますので、ご活用ください。

https://github.com/umaxiaotian/dify_tools_api