1.はじめに
OSSで利用できるローコードAIエージェント開発ツール Flowise ですが、すこし凝った事を実現しようとすると、用意されているノードだけでは不十分な事があります。
そういった場合に、自分でカスタムノードを作ることができるので、その対応方法をまとめす。
2.作成するもの
Flowiseでは様々なベクトルストアノード実装(ElasticsearchやOpenSearch等)が用意されていますが、Azure AI Searchに対応したノードがありませんでした。
この記事ではAzure AI Searchに対応したベクトルストアノードを作成します。
3.準備
3.1 Flowiseをリポジトリから取得
Githubの公式リポジトリからフォークします。
3.2 pnpm
をインストール
Flowiseではパッケージマネージャにpnpm
を用いているので、これをインストールします。
$ npm i -g pnpm
4.カスタムノードの作成
カスタムノードといっても、LangChainがAzure AI Searchに対応しているベクトルストアのクラスを用意しているので、これを利用するカスタムノードを作るだけです。
Flowiseでは大きく3つのパッケージがあります。
-
components
・・・各ノード -
server
・・・サーバー側 -
ui
・・・ビュー全般
今回はノードを追加するのでcomponents
配下のサブパッケージに変更を加えます。主なノードはLangChainのコンセプトと対応しているため
- 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
クラス)
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にアクセスするための認証情報を保存するクラスです。
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ベクトルストア カスタムノードでフローを作った図
(図2)Azure AI Searchベクトルストア カスタムノードの追加設定
6.さいごに
LangChainの知識がある程度あれば、比較的容易にFlowiseを拡張することができました。
Flowiseを使えば、手軽にRAGシステムやAIエージェントシステムなどをグラフィカルに手早く作るることができるので、手早く実証実験などを行う事ができます。
一方、この手のローコードツールにありがちな 凝ったことができないや再利用性が悪いという問題もカスタムノードを実装することである程度軽減できます。
業務独自のデータソースや汎用機との接続が行えるノードなどを作ることで、生成AIでできることの幅が広がりそうです。