Flowiseのカスタムノードを作る

# 1.はじめに
OSSで利用できるローコードAIエージェント開発ツール [**Flowise**](https://flowiseai.com/) ですが、すこし凝った事を実現しようとすると、用意されているノードだけでは不十分な事があります。

そういった場合に、自分でカスタムノードを作ることができるので、その対応方法をまとめす。

# 2.作成するもの
Flowiseでは様々なベクトルストアノード実装（ElasticsearchやOpenSearch等）が用意されていますが、Azure AI Searchに対応したノードがありませんでした。

この記事ではAzure AI Searchに対応したベクトルストアノードを作成します。

# 3.準備
## 3.1 Flowiseをリポジトリから取得
Githubの[公式リポジトリ](https://github.com/FlowiseAI/Flowise)からフォークします。

## 3.2 `pnpm`をインストール
Flowiseではパッケージマネージャに`pnpm`を用いているので、これをインストールします。
```
$ npm i -g pnpm
```

# 4.カスタムノードの作成
カスタムノードといっても、LangChainがAzure AI Searchに対応しているベクトルストアのクラスを用意しているので、これを利用するカスタムノードを作るだけです。

Flowiseでは大きく３つのパッケージがあります。
- `components` ・・・各ノード
- `server` ・・・サーバー側
- `ui` ・・・ビュー全般

今回はノードを追加するので`components` 配下のサブパッケージに変更を加えます。主なノードは[LangChainのコンセプト](https://js.langchain.com/docs/concepts/)と対応しているため

- LangChainのコンポーネント（`VectorStore`や`DocumentLoader`等）をつくる
- FlowiseのNodeでラップする

という流れになります。
## 4.1 必要なパッケージのインストール
Azure AI Searchへアクセスするために、`@azure/search-documents`パッケージをnpmを通じてインストールします。


## 4.2 カスタムノードのソース
各ノードは`packages/components/nodes` 配下にあるので、ここにカスタムノードを作成します。
今回はvectorestoresにある`OpenSearch`の実装をマルマル真似ていきます。
LangChainのcommunityパッケージにすでにAzure AI Searchのベクトルストアが存在するので、今回はこれをそのまま使います。
もし独自のデータストアなどを利用したい場合はLangChainのVectorStoreクラスを継承したものを自作する形になります。

ポイントは

- `INode`インターフェイスを実装する
- `inputs`フィールドにパラメータを定義する
- `init`メソッドをオーバライドし、そこで**LangChain**の**VectorStoreクラス**あるいは**VectorStoreRetriverクラス** を生成して返す（今回のサンプルではそれらを継承した`AzureAISearchVectorStore`クラス）

```typescript:/packages/nodes/vectorstores/AzureAISearch/AzureAISearch.ts
class AzureAISearch_VectorStores implements INode {
    label: string
    name: string
    version: number
    description: string
    author: string
    type: string
    icon: string
    category: string
    badge: string
    baseClasses: string[]
    inputs: INodeParams[]
    outputs: INodeOutputsValue[]
    credential: INodeParams

    constructor() {
        //ノードの属性を設定する
        this.label = 'Azure AISearch'
        this.name = 'azureAISearch'
        this.version = 1.0
        this.type = 'AzureAISearch'
        this.icon = 'Azure.svg'
        this.category = 'Vector Stores'
        this.description = `Upsert embedded data and perform similarity search upon query using Azure AISearch`
        this.author = '<作成者>'
        this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever']
        this.credential = {
            label: 'Connect Credential',
            name: 'credential',
            type: 'credential',
            credentialNames: ['azureAISearchUrl']
        }
        //FlowiseのWebUIに表示される各パラメータの定義
        this.inputs = [
            {
                label: 'Document',
                name: 'document',
                type: 'Document',
                list: true,
                optional: true
            },
            {
                label: 'Embeddings',
                name: 'embeddings',
                type: 'Embeddings'
            },
            {
                label: 'Index Name',
                name: 'indexName',
                type: 'string'
            },
            {
                label: 'Top K',
                name: 'topK',
                description: 'Number of top results to fetch. Default to 4',
                placeholder: '4',
                type: 'number',
                additionalParams: true,
                optional: true
            },
            {
                label: 'Search Type',
                name: 'searchType',
                type: 'options',
                description: 'Search type, Default to similarity',
                options: [
                    { label: 'Similarity', name: 'similarity', description: '類似度検索' },
                    { label: 'Similarity_hybrid', name: 'similarity_hybrid', description: '類似度検索ハイブリッド' },
                    { label: 'semantic_hybrid', name: 'semantic_hybrid', description: 'セマンティック検索ハイブリッド' }
                ],
                additionalParams: true,
                optional: true
            }
        ]
        //ノードの出力の定義。
        this.outputs = [
            {
                label: 'AzureAISearch Retriever',
                name: 'retriever',
                baseClasses: this.baseClasses
            },
            {
                label: 'AzureAISearch Vector Store',
                name: 'vectorStore',
                baseClasses: [this.type, ...getBaseClasses(AzureAISearchVectorStore)]
            }
        ]
    }

    //@ts-ignore
    vectorStoreMethods = {
        //FlowiseのWebUIからUpsertされたときの処理。
        async upsert(nodeData: INodeData, options: ICommonObject): Promise<Partial<IndexingResult>> {
            const docs = nodeData.inputs?.document as Document[]
            const embeddings = nodeData.inputs?.embeddings as Embeddings
            const indexName = nodeData.inputs?.indexName as string

            const credentialData = await getCredentialData(nodeData.credential ?? '', options)
            const endpoint = getCredentialParam('endpoint', credentialData, nodeData)
            const key = getCredentialParam('apikey', credentialData, nodeData)

            const flattenDocs = docs && docs.length ? flatten(docs) : []
            const finalDocs = []
            for (let i = 0; i < flattenDocs.length; i += 1) {
                if (flattenDocs[i] && flattenDocs[i].pageContent) {
                    finalDocs.push(new Document(flattenDocs[i]))
                }
            }

            try {
                await AzureAISearchVectorStore.fromDocuments(finalDocs, embeddings, {
                    endpoint,
                    key,
                    indexName
                })
                return { numAdded: finalDocs.length, addedDocs: finalDocs }
            } catch (e) {
                throw new Error(e)
            }
        }
    }
    //ノードの初期化処理
    async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
        const embeddings = nodeData.inputs?.embeddings as Embeddings
        const indexName = nodeData.inputs?.indexName as string
        const output = nodeData.outputs?.output as string
        const topK = nodeData.inputs?.topK as string
        const k = topK ? parseFloat(topK) : 4
        const searchTypeStr = nodeData.inputs?.searchType as string
        let searchType: AzureAISearchQueryType
        if (searchTypeStr == 'similarity') {
            searchType = AzureAISearchQueryType.Similarity
        } else if (searchTypeStr == 'similarity_hybrid') {
            searchType = AzureAISearchQueryType.SimilarityHybrid
        } else if (searchTypeStr == 'semantic_hybrid') {
            searchType = AzureAISearchQueryType.SemanticHybrid
        } else {
            searchType = AzureAISearchQueryType.Similarity
        }

        const credentialData = await getCredentialData(nodeData.credential ?? '', options)
        const endpoint = getCredentialParam('endpoint', credentialData, nodeData)
        const key = getCredentialParam('apikey', credentialData, nodeData)

        const vectorStore = new AzureAISearchVectorStore(embeddings, {
            endpoint,
            key,
            indexName,
            search: {
                type: searchType
            }
        })

        if (output === 'retriever') {
            const retriever = vectorStore.asRetriever(k)
            return retriever
        } else if (output === 'vectorStore') {
            ;(vectorStore as any).k = k
            return vectorStore
        }
        return vectorStore
    }
}

module.exports = { nodeClass: AzureAISearch_VectorStores }

```
## 4.3 クレデンシャルのソース
Azure AI Searchにアクセスするための認証情報を保存するクラスです。
```typescript:/packages/credentials/AzureAiSearchUrl.credential.ts
import { INodeParams, INodeCredential } from '../src/Interface'

class AzureAISearchUrl implements INodeCredential {
    label: string
    name: string
    version: number
    description: string
    inputs: INodeParams[]

    constructor() {
        this.label = 'Azure AISearch'
        this.name = 'azureAISearchUrl'
        this.version = 1.0
        this.inputs = [
            {
                label: 'Azure AISearch Endpoint',
                name: 'endpoint',
                type: 'string'
            },
            {
                label: 'API Key',
                name: 'apikey',
                type: 'string',
                placeholder: '<AZUREAISEARCH_APIKEY>',
                optional: true
            }
        ]
    }
}

module.exports = { credClass: AzureAISearchUrl }

```

## 4.4 ビルド
ビルドします。
```
$ pnpm install
$ pnpm build
```

# 5. 実行
環境変数`SHOW_COMMUNITY_NODES`をtrueにして、サーバーを起動させます。
```
$ SHOW_COMMUNITY_NODES=true pnpm start
```
追加したカスタムノードが一覧に表れてフローが作れれば成功です。

_(図1)Azure AI Searchベクトルストア カスタムノードでフローを作った図_
![Flowise1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951174/0d7613b4-810e-30b5-8376-430610dc4801.png)

_(図2)Azure AI Searchベクトルストア カスタムノードの追加設定_
![Flowise2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951174/d4974c52-8cd5-d1a2-fc2d-7cc0ea168153.png)

# 6.さいごに

LangChainの知識がある程度あれば、比較的容易にFlowiseを拡張することができました。
Flowiseを使えば、手軽にRAGシステムやAIエージェントシステムなどをグラフィカルに手早く作るることができるので、手早く実証実験などを行う事ができます。

一方、この手のローコードツールにありがちな **凝ったことができない**や**再利用性が悪い**という問題もカスタムノードを実装することである程度軽減できます。

業務独自のデータソースや汎用機との接続が行えるノードなどを作ることで、生成AIでできることの幅が広がりそうです。