はじめに
TypeScript版のLangChainで作ったプログラムをexe化する方法を解説する😊
そもそも何のためにexe化するのか
LangChainで作った試作プログラムを非エンジニアの人に見てもらいたい😊
非エンジニアに環境構築させたくない
さっそく試作プログラムを見てもらおう!と思ったが、Node.jsを使っているためそのままフォルダを渡しても実行できない💦
かといって非エンジニアに「Node.jsの環境構築をしてね」というのは気が引ける💦
作るもの
- 独自データ(名探偵コナンの映画「⿊鉄の⿂影」の内容)について答えてくれるプログラムをexe化する
- 画面(GUI)は作らない → CLI
- 質問内容はコマンドライン引数で指定する
結論
✅nexeを使えばexe化できる。
✅しかしLangChainで使用しているhnswlibのバグ(?)があり、exeファイル1つだけ配布しても実行できない。
✅「exeファイル」 + 「いくつかのフォルダ」 があれば実行できる。
必要なもの
- Open AIのAPIキー
- Node.jsの開発環境
今から何をexe化する?
以前に作ったサンプルプログラムをexe化する✅
ディレクトリ構成
主要なファイルの構成を紹介(Node.jsのライブラリなどは省略)
📦TSexe (プロジェクトフォルダの名前はTSexeとした)
┣ 📂index // 事前に作ったインデックス(前回の記事で解説済み)
┃ ┣ 📜args.json
┃ ┣ 📜docstore.json
┃ ┗ 📜hnswlib.index
┣ 📂src
┃ ┣ 📜index.ts // exe化したいソース(前回の記事から一部修正。後述)
┣ 📜.env // 環境変数用ファイル。OpneAIのAPIキーが書いてある。
exe化するソース
上記のソースから少しだけ変更しているので、ソースの全貌を記載する。
変更後のindex.ts
// モデル
import { ChatOpenAI } from "langchain/chat_models/openai";
// 埋め込み
import { OpenAIEmbeddings } from "langchain/embeddings/openai";
// ベクトル検索エンジン
import { HNSWLib } from "langchain/vectorstores/hnswlib";
// チェーン
// import { RetrievalQAChain } from "langchain/chains";
import { ConversationalRetrievalQAChain } from "langchain/chains";
// メモリー
import { BufferMemory } from "langchain/memory";
// .envの読み込み
import fs from 'fs';
const envFilePath = '.env'; // .envファイルのパスを指定(ルートディレクトリにある場合)
// ----------------------------------------------------------------------
// .envファイルを同期的に読み込む関数
function readEnvFile(filePath: string): { [key: string]: string } {
const data = fs.readFileSync(filePath, 'utf-8');
const lines = data.split('\n');
const envData: { [key: string]: string } = {};
for (const line of lines) {
const trimmedLine = line.trim(); // 先頭と末尾の空白を取り除く
if (trimmedLine && !trimmedLine.startsWith('#')) { // 空行とコメント行をスキップ
const [key, value] = trimmedLine.split('=');
envData[key] = value.replace(/(^"|"$)/g, ''); // ダブルクオーテーションを取り除く
}
}
return envData;
}
// ----------------------------------------------------------------------
// 実行
export const runLlm = async () => {
// APIキー読み込み
try {
// .envファイルを読み込む
const envData = readEnvFile(envFilePath);
const apiKey = envData['OPENAI_API_KEY'];
// APIキーを環境変数にセットする
process.env.OPENAI_API_KEY = apiKey;
} catch (error) {
console.error('.envファイルの読み込みに失敗しました:', error);
}
// 作成済みのインデックスを読み込む
const vectorStore = await HNSWLib.load(
path.join(__dirname, 'index'), // indexフォルダ
new OpenAIEmbeddings()
);
// モデル
const model = new ChatOpenAI({});
// チェーン
const chain = ConversationalRetrievalQAChain.fromLLM(
model,
vectorStore.asRetriever(),
{
qaChainOptions: {type: "stuff"},
memory: new BufferMemory({
memoryKey: "chat_history",
inputKey: "question",
outputKey: "text",
returnMessages: true,
}),
// verbose: true,
}
);
// 質問する
const res = await chain.call({
question: process.argv[2], // コマンドライン引数から質問文を取得
});
// 回答を表示する
console.log( res['text'] );
};
runLlm();
変更点1:APIキーの読み込み
APIキーの読み込み処理を変更した!
前回作ったソースはdotenvでOpenAIのAPIキーを読み込んでいたが、exe化すると動かなかった💦
対処方法として.envファイルからAPIキーを読み込む処理を自作した。
✅変更前
src/index.ts
require("dotenv").config();
✅変更後
src/index.ts
// .envファイルを同期的に読み込む関数
function readEnvFile(filePath: string): { [key: string]: string } {
const data = fs.readFileSync(filePath, 'utf-8');
const lines = data.split('\n');
const envData: { [key: string]: string } = {};
for (const line of lines) {
const trimmedLine = line.trim(); // 先頭と末尾の空白を取り除く
if (trimmedLine && !trimmedLine.startsWith('#')) { // 空行とコメント行をスキップ
const [key, value] = trimmedLine.split('=');
envData[key] = value.replace(/(^"|"$)/g, ''); // ダブルクオーテーションを取り除く
}
}
return envData;
}
...
...
...
// APIキー読み込み
try {
// .envファイルを読み込む
const envData = readEnvFile(envFilePath);
const apiKey = envData['OPENAI_API_KEY'];
// APIキーを環境変数にセットする
process.env.OPENAI_API_KEY = apiKey;
} catch (error) {
console.error('.envファイルの読み込みに失敗しました:', error);
}
変更点2:質問をコマンドライン引数から取得
コマンドライン引数で任意の質問ができるようにした!
✅変更前
src/index.ts
// 質問する
const res = await chain.call({
query: "ピンガはどんなキャラクターですか?",
});
✅変更後
src/index.ts
// 質問する
const res = await chain.call({
question: process.argv[2], // コマンドライン引数から質問文を取得
});
トランスパイルしておく
先程のソースはTypeScriptなので、トランスパイルしてJavaScriptファイルを生成しておく。
以下のコマンドを実行する✅
tsc
📦TSexe
┣ 📂dist
┃ ┣ 📜index.js // index.tsをトランスパイルしたファイルができる
┣ 📂index
┣ 📜args.json
┃ ┣ 📜docstore.json
┃ ┗ 📜hnswlib.index
┣ 📂src
┃ ┣ 📜index.ts
┣ 📜.env
exe化する
exe化に使うツール
どうやってexe化するのか調べるとNode.jsの「pkg」や「nexe」を使う方法が出てきた。
「pkg」は試したがうまくいかなかったので不採用❌
「nexe」でexe化に成功したのでこちらを紹介する⭕️
nexeをインストール
公式の説明のとおり以下のコマンドでインストールする。
npm install nexe -g
【もしエラーが出たらsudoを付ける】
sudo npm install nexe -g
Windowsの場合追加作業が必要
Windows環境では以下の手順が必要。
- Power Shellを管理者権限で起動し、以下の4つのコマンドを実行する。
Set-ExecutionPolicy Unrestricted -Force iex ((New-Object System.Net.WebClient).DownloadString('https://boxstarter.org/bootstrapper.ps1')) get-boxstarter -Force Install-BoxstarterPackage https://raw.githubusercontent.com/nodejs/node/master/tools/bootstrap/windows_boxstarter -DisableReboots
- Visual Studio2019をインストールし、C++デスクトップアプリ開発をインストールする。
- 再びPower Shellに戻り以下のコマンドを実行する。
npm config edit
- すると.npmrcが開くので最下行に以下を追記すれば作業完了!
msvs_version=2019 python=python3.8
参考サイト
exe化したいJavaScriptファイルを移動
exe化したいindex.jsをルートディレクトリに移動する。
📦TSexe
┣ 📂dist
┃ ┣ 📜index.js // 移動元
┣ 📂index
┃ ┣ 📜args.json
┃ ┣ 📜docstore.json
┃ ┗ 📜hnswlib.index
┣ 📂src
┃ ┣ 📜index.ts
┣ 📜.env
┣ 📜index.js // 移動先
nexeを使ってexe化
✅以下のコマンドでindex.jsをexe化できる。
nexe ./index.js --build --verbose --python python3 -r .env -r index
これでルートディレクトリにTSexe.exe
(フォルダ名と同じ名前のexe)が生成される😊
(Macだと拡張子なしのTSexe
というファイル名になる)
📦TSexe
┣ 📂dist
┃ ┣ 📜index.js
┣ 📂index
┣ 📜args.json
┃ ┣ 📜docstore.json
┃ ┗ 📜hnswlib.index
┣ 📂src
┃ ┣ 📜index.ts
┣ 📜.env
┣ 📜index.js
┣ 📜TSexe.exe // exeができる😊
【補足】コマンドのオプションの解説
オプション | 解説 |
---|---|
./index.js | ルートディレクトリにあるindex.jsをexe化する。 |
--build --python python3 | Node.jsのバージョン16以降を使う場合に必要(?) |
--verbose | 詳細なログを出力 |
-r .env | .envをexeに含める。 |
-r index | indexをexeに含める。 |
exeを配布する
早速作ったexeを非エンジニアに配布して使ってもらう😊
✅実行してもらうには「exeファイル」と一緒に「node_modules」と「index」を配布する必要がある
「node_modules」と「index」は元のソースにあるものをコピペしてくるだけでOK!
詳細は後で説明する。
配布するフォルダ
📦myApp(任意の名前)
┣ 📜TSexe.exe
┣ 📂node_modules // exeと同階層に置いておく。以下の3フォルダ以外は削除してOK。
┃ ┣ 📂bindings
┃ ┣ 📂file-uri-to-path
┃ ┗ 📂hnswlib-node
┣ 📂index // exeと同階層に置いておく
┃ ┣ 📜args.json
┃ ┣ 📜docstore.json
┃ ┗ 📜hnswlib.index
exeを実行してみる
3パターンの実行方法を紹介する😊
Windowsのコマンドプロンプトの場合
macのターミナルの場合
注意点
セキュリティは非考慮
社内での利用を想定しているのでセキュリティは考慮していない。
コマンドでの操作
今回は最低限の労力で配布したかったので画面は作っていない。
トラブルシューティング
hnswlibを使うとエラーになる
exeを実行するとhnswlibがインポートできないエラーが発生した。
Error: Could not import hnswlib-node. Please install hnswlib-node as a dependency with, e.g. `npm install -S hnswlib-node`.
原因
hnswlibは存在しているはずなのにエラーが出ていて原因が分からない💦
対処方法
試行錯誤した結果、node_modules内の「bindings」「file-uri-to-path」「hnswlib-node」の3つがあればエラーにならず実行できることが判明した!
とりあえずexeと同じ階層にnode_modulesを置くことで対応した。
┣ 📜TSexe.exe
┣ 📂node_modules // exeと同階層に置いておく。以下の3フォルダ以外は削除してOK。
┃ ┣ 📂bindings
┃ ┣ 📂file-uri-to-path
┃ ┗ 📂hnswlib-node
インデックスが見つからない
exeを実行すると同じ階層に「index」フォルダがあるのに、フォルダ存在しないというエラーが発生した。(Macのときだけエラーが出た。環境の問題…?)
Error while listing files: [Error: ENOENT: no such file or directory, scandir '/Users/xxxx/yyyy/index'] {
errno: -2,
code: 'ENOENT',
syscall: 'scandir',
path: '/Users/xxxx/yyyy/index'
}
原因
exe化するときにオプション-r index
を付けてindexフォルダを含めたはずなのにindexが見つからず原因が分からない💦
【補足】exe化のログにもindexフォルダが含まれている💭
✔ Including file: .env
✔ Including file: index/args.json
✔ Including file: index/docstore.json
✔ Including file: index/hnswlib.index
✔ Included 4 file(s)
ログのとおりならexeにindexが含まれているはずなのにindexが見つからない原因が不明💦
もしかするとnexeの仕様でフォルダは-r
オプションで含められない…?と推測したが真偽は不明💭
対処方法
とりあえずexeと同じ階層にindexフォルダを置くことで対応した。
┣ 📜TSexe.exe
┣ 📂index // exeと同階層に置いておく
┃ ┣ 📜args.json
┃ ┣ 📜docstore.json
┃ ┗ 📜hnswlib.index
異なるOSで実行できない
公式によるとexe化するときオプション--target
を付ければ異なるOS用の実行ファイルが作れるはず。
例:Windows環境でLinux用の実行ファイルを作る
しかしWindows環境で--target linux-x64
を付けてもLinuxでは使えなかった😢
原因
--build
を付けると他のOSをターゲットに指定できないらしい😢
参考:https://github.com/nexe/nexe/issues/948
対処方法
現状直接の対処はなさそう…
どうしてもLinuxで実行したい場合は、面倒だがLinux環境でビルドする必要がある。
「Linuxが入ったPCでビルド」or「Linuxの仮想環境を作る」などの対応が必要そう(未検証)