はじめに
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
 ┣ 📜.envexe化する
exe化に使うツール
どうやってexe化するのか調べるとNode.jsの「pkg」や「nexe」を使う方法が出てきた。
「pkg」は試したがうまくいかなかったので不採用❌
「nexe」でexe化に成功したのでこちらを紹介する⭕️
nexeをインストール
公式の説明のとおり以下のコマンドでインストールする。
npm install nexe -g
【もしエラーが出たらsudoを付ける】
sudo npm install nexe -gWindowsの場合追加作業が必要
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.indexexeを実行してみる
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の仮想環境を作る」などの対応が必要そう(未検証)









