LangChain(TypeScript)のRetrievalQAChainとOpenAI APIで、自前のドキュメントに関する質問に答えてくれるプログラムを作ってみた。
概要
OpenAIのChatGPTさん、めっちゃ頭良いけど、自分しか持って無くてインターネットに公開していない情報については回答してくれないので、自分しか持ってないドキュメントの内容に関する質問に回答してくれるプログラムを作ってみたので、その備忘録。
GitHubリポジトリ
使う技術
Node.js(バージョン18.14.2)
TypeScript
LangChain(TypeScript/JavaScript 版)
ダミーファイルを作成
そんなすぐに、良い感じのドキュメントを用意できないので、BingのAIさんに、いくつか社内用語を出力してもらって、それをテキストファイルとして保存しました。
テキストファイルはすべて documentsディレクトリに保存しています。
ディレクトリ構成
こんな感じ
create-vector フォルダ
documentsフォルダ内のテキストをvectorストアに保存するプログラム
call-query フォルダ
保存済のvectorストアを読み込み、質問の回答を得るプログラム
documents フォルダ
テキストファイルを保存しておくフォルダ(ここでは架空の企業の社内用語と説明のテキストファイル)
database フォルダ
ベクターストアの内容を保存しておくフォルダ
今回はベクターストアとして、ローカルに保存できるHNSWLibを利用します。
実行の流れ
ドキュメントの内容をベクターストアに保存するプログラムを実行する ↓ 保存済のvectorストアを読み込み、質問の回答を得るプログラムを実行する
プログラムのコード
create-vector-store/index.ts
documentsフォルダ内のドキュメントの内容をベクターストアに保存するプログラム
RecursiveCharacterTextSplitterで、ドキュメントの内容を100token以下になるまで分割しVectorStoreに保存しています。
import path from 'path'; import dotenv from 'dotenv'; import { OpenAIEmbeddings } from 'langchain/embeddings/openai'; import { DirectoryLoader } from 'langchain/document_loaders/fs/directory'; import { TextLoader } from "langchain/document_loaders/fs/text"; import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"; import { HNSWLib } from "langchain/vectorstores/hnswlib"; // .envファイルから環境変数を読み込み dotenv.config(); const main = async function () { console.log("start"); // 自前で準備したドキュメントを保存しているディレクトリの絶対パス const documentPath = path.join(__dirname, '../documents/'); const directoryLoader = new DirectoryLoader(documentPath, { '.txt': (path) => { return new TextLoader(path) } }); const documents = await directoryLoader.load(); const splitter = new RecursiveCharacterTextSplitter({ chunkSize: 100, chunkOverlap: 10, }); const texts = await splitter.splitDocuments(documents); const embeddings = new OpenAIEmbeddings({ openAIApiKey: process.env.OPENAI_API_KEY }); const vectorStore = await HNSWLib.fromDocuments(texts, embeddings); const databasePath = path.join(__dirname, '../database/'); await vectorStore.save(databasePath); console.log("end"); }; main();
call-query/index.ts
保存済のvectorストアを読み込み、質問の回答を得るプログラム
ここで、保存済のベクターストアから読み取り、RetrievalQAChainを使って自前のドキュメントの内容に対する質問に関する質問の回答を得ます。
import path from 'path'; import dotenv from 'dotenv'; import { OpenAIEmbeddings } from 'langchain/embeddings/openai'; import { OpenAI } from "langchain/llms/openai"; import { HNSWLib } from "langchain/vectorstores/hnswlib"; import { RetrievalQAChain } from "langchain/chains"; // .envファイルから環境変数を読み込み dotenv.config(); const main = async function () { // 実行時引数が指定されているか確認 if (process.argv.length < 3) { return; } const databasePath = path.join(__dirname, '../database/'); const embeddings = new OpenAIEmbeddings(); // 保存済のベクターストアから読み込み const vectorStore = await HNSWLib.load(databasePath, embeddings); const model = new OpenAI({ openAIApiKey: process.env.OPENAI_API_KEY }); const chain = RetrievalQAChain.fromLLM(model, vectorStore.asRetriever()); // 実行時引数から質問内容を取得 const query = process.argv[2]; const { text } = await chain.call({ query }); // 質問内容と回答内容をコンソールに表示 console.log(`Q: ${ query }\nA: ${ text }`); }; main();
実行手順
ベクターストアの作成
$ cd <プロジェクトディレクトリ>/create-vector-store/ $ npm run start
質問をする
$ cd <プロジェクトディレクトリ>/create-vector-store/ $ npm run start --query=<質問>
実行結果例
ちゃんとdocumentsフォルダに保存しているドキュメントの内容に沿った回答をもらえました。
また、試しにdocumentsフォルダ内のドキュメントには記載の無い質問をすると、ちゃんと "分からない" 旨の回答がかえってきました。素直だな。
まとめ
とりあえず、自前で準備したドキュメントに関する回答を得ることができた。
今回はDirectoryLoaderの中で、テキストファイルに対してのみ読み取りを行っているが、これをPDFやWord、PowerPointに対応させれば、より汎用的に使えるようになるかもしれないなと思いました。
Visual Studio Code でpackage.jsonが複数ある場合に各scriptsの内容をタスクランナーから実行する方法
何がしたいのか
package.jsonがカレントディレクトリになく、 且つ package.jsonが複数のサブディレクトリにまたがって複数存在する場合に、 VSCodeのタスクランナーからそれぞれのpackage.jsonの scripts に指定されているscriptを実行したい!!!!
したい!!!
どうしてそう思った
Webのフロントもサーバーも両方TypeScriptで書いていて、 フロントとサーバーを別々のフォルダで管理して、pacakge.jsonもそれぞれ別々に用意していると、 以下のようなフォルダ構成なることが考えられる。
というか、なった!
こういう構成にするのが馬鹿だって石投げつけられたら、もうそのまま石投げつけられますが!
root/ ←ここから VSCode で開いている │ ├ client/ ← フロントエンド用プロジェクト Angular2とかやる方 │ ├ index.ts │ ├ webpack.config.json │ │ ...etc │ └ package.json ← フロントエンド用の packatge.json こっちにフロントのビルド用scriptが書いてある │ └ server/ ← バックエンド用プロジェクト Express 使ってAPI作る ├ app.ts │ ...etc └ package.json ← バックエンド用の packatge.json こっちにサーバーのビルド用scriptが書いてある
で、俺は開いた。VSCodeのタスクランナーを。意気揚々と。Command + shift + P からの task からの Enter ターンッ !! npm を選択して Enter ターンッ!!!
あれ?カレントディレクトリに package.json にない場合どうするんだ??? というか複数のpackage.jsonを扱いたい場合どうするんだ? 絶望しました。
まぁ、VSCodeには端末機能あるからそこでコマンド叩けばいいんだけど、やっぱりタスクランナーがあるんだったらそれでやりたい!!!
pacakge.jsonのscriptsの内容
client/package.json
フロント側のpackage.json webpack使ってビルドしてる。 webpackの方で、ビルドした.jsファイルを server/public/script/ に吐き出してる。 とりあえず VSCodeのタスクランナーから build と watch を叩きたい
{ // 省略 "scripts": { "build": "webpack --progress --colors --config webpack.config.dev.js", "watch": "webpack --progress --colors --watch --config webpack.config.dev.js", "build-product": "webpack --progress --colors --config webpack.config.prod.js", "watch-product": "webpack --progress --colors --watch --config webpack.config.prod.js" }, // 省略 }
server/package.json
サーバー側のpackage.json 一番使ってるのが dev だから最悪これだけでもタスクランナーから叩きたい
{ // 省略 "scripts": { "start": "node ./app.js", "dev": "tsc --watch & node-dev ./app.js", "build": "tsc", "watch": "tsc --watch" }, // 省略 }
結論
できた!
できた!
こうする!
これを(タスクの種類で npm を選んだ場合の初期状態)
{ // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format "version": "0.1.0", "command": "npm", "isShellCommand": true, "showOutput": "always", "suppressTaskName": true, "tasks": [ { "taskName": "install", "args": ["install"] }, { "taskName": "update", "args": ["update"] }, { "taskName": "test", "args": ["run", "test"] } ] }
こうする!
{ // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format "version": "0.1.0", "command": "sh", "isShellCommand": true, "showOutput": "always", "suppressTaskName": true, "args": ["-c"], "tasks": [ { "taskName": "build", "args": ["npm -prefix ./client run build && npm -prefix ./server run build;"] }, { "taskName": "watch", "args": ["npm -prefix ./client run watch & npm -prefix ./server run dev;"] } ] }
ポイント1
shコマンドを指定する。npmじゃなくてsh。発想の転換
"command": "sh",
ポイント2
-cオプションをつける。 shellとか詳しくないけど、ようは端末で打った内容をそのまま実行するためのオプションらしい。
"args": ["-c"]
ポイント3
args にもう全部書いちゃう!
"args": ["npm -prefix ./client run build && npm -prefix ./server run build;"]
つまりこういうことだね。知らんけど。
$ sh -c npm -prefix ./client run build && npm -prefix ./server run build;
ポイント4
-prefix をつける。
npm -prefix ./client run build
https://docs.npmjs.com/cli/prefix
こういうことらしいです。
なるほどね ← 分かってない
ポイント5
&& とか & とか使って、フロント側の npm run と サーバー側の npm run をまとめて実行してやる。
今回は build の方は実行した一発だけでいいから &&(http://itpro.nikkeibp.co.jp/article/COLUMN/20060224/230604/) watchの方は両方はバックグラウンドで動かしたいから &(http://itpro.nikkeibp.co.jp/article/COLUMN/20060224/230589/) を使った。← よくわかってない。
npm -prefix ./client run build && npm -prefix ./server run build;
npm -prefix ./client run watch & npm -prefix ./server run dev;
まぁ、普段は IntelliJ使ってるんだけどな!!!!!!!!!!!
Visual Studio Code で TypeScript2.0を書いたらハマったこと
前回の記事から2ヶ月?くらいたっていて、気がついたらTypeScript2.0がリリースされていた…
ので、Visual Studio Code で TypeScript2.0 を書こうとしたらハマったところと解決策のメモ。 まぁ、普段は IntelliJ IDEA で書いているだけど。
まず、以下のようなコードを TypeScript で書いて、Visual Studio Code のタスクランナーでコンパイル使用としたら怒られた。
'use strict'; class Singleton { /** instanceを保持します。 */ private static instance: Singleton; /** * コンストラクタ * TypeScript2.0からconstructor に private が指定できるようになった。 */ private constructor() { // do nothing. } /** * インスタンス取得メソッド * @return {Singleton} Singletonのインスタンス */ public static getInstance() : Singleton { if (!Singleton.instance) { Singleton.instance = new Singleton(); } return Singleton.instance; } public sayHello() : void { console.log('Hello World'); } } Singleton.getInstance().sayHello();
どこで怒られたかっていうと、コンストラクタのところ。
/** * コンストラクタ * TypeScript2.0からconstructor に private が指定できるようになった。 */ private constructor() { // do nothing. }
コンストラクタに private が指定できるようになったのは TypeScript2.0 からで、どうもVisual Studio Code が内部的に使っているTypeScriptは1.8らしい。
なので、Visual Studio Code 側の設定を変えてあげる必要がある。「ワークスペース設定」を開き…
以下のように、ワークスペースの設定でTypeScriptを自分がインストールしたものを指定してあげる。
すると、
うまくいった!めでたしめでたし。
webpack + Babel + React(ES2015使用)の設定
やりたいこと。 * ReactをJSXを使用して書きたい。 * EcmaScriptの構文も使用して書きたい * .jsファイルと、.jsxファイルに変更があった場合自動的にビルドしてほしい。 * 自動的にソースマップも出力してほしい。
webpack.config.js
// コマンドメモ // -p : minify // --watch : ファイルの変更を監視 ビルドの自動化 // $ webpack -p --progress --colors --watch var path = require('path'); var webpack = require('webpack'); module.exports = { // エントリーポイント entry: './script/src/main.jsx', // ソースマップ出力 devtool: "#source-map", // 出力先設定 output: { path: __dirname, filename: './script/dest/bundle.js' }, module: { loaders: [ // .jsxファイル用 { test: /.jsx?$/, loader: 'babel-loader', exclude: /node_modules/, query: { presets: ['es2015', 'react'] } }, // .jsファイル用 { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/, query: { presets: ['es2015'] } } ] }, resolve: { // ここに登録した拡張子は import時に省略できる extensions: ['', '.js', '.jsx'] } };
以下のコマンドで実行
$ webpack -p --progress --colors --watch
React + gulp サンプル
Material Design Lite
React
React使いつつ、マテリアルデザインも使いたいなーと思って、Material Design Lite を組み合わせてみた時のメモ。
ビルドツールには gulp を使用したので、まず gulpfile.js から。
var gulp = require('gulp'); var uglify = require('gulp-uglify'); var plumber = require('gulp-plumber'); var browserify = require('browserify'); var source = require("vinyl-source-stream"); var reactify = require('reactify'); var buffer = require('vinyl-buffer'); gulp.task('dist', function(){ var bundler = browserify('./script/src/main.jsx', { debug: true }); return bundler .transform(reactify) .bundle() .pipe(plumber()) .pipe(source('build.js')) .pipe(buffer()) .pipe(uglify()) .pipe(gulp.dest('./script/dist/')); }); gulp.task('watch', function() { gulp.watch('./script/src/*.js', ['dist']); gulp.watch('./script/src/*.jsx', ['dist']); gulp.watch('./script/src/**/*.js', ['dist']); gulp.watch('./script/src/**/*.jsx', ['dist']); }); gulp.task('default', ['dist', 'watch']);
次に HTMLファイル。 body要素の最後で、ビルド後のjsファイルを読み込むようにする。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>test</title> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <link href="./style/material.min.css" rel="stylesheet"> <link href="./style/style.css" rel="stylesheet"> </head> <body> <noscript>JavaScriptを有効にして下さい。</noscript> <div id="appView"></div> <script src="./script/dist/build.js"></script> </body> </html>
エントリーポイントとなる main.jsx 3行目で、Material Design LiteのJavaScriptを読み込んでいる。 6行目で読み込んでいる AppView.jsx がルートコンポーネント。
(function () { 'use strict'; var mdl = require('material-design-lite'); var React = require('react'); var ReactDOM = require('react-dom'); var AppView = require('./component/AppView.jsx'); // アプリケーション全体のコンポーネントを表示 ReactDOM.render( <AppView />, document.getElementById('appView') ); }());
自前でクライアントサイドMVCのModelとView用ライブラリ作った
自前でクライアントサイドMVCのModelとView用ライブラリ作った。
コンセプトは、以下のような見慣れたクラス定義の書き方を崩さずかけるようにすること。
/** * サンプルクラス */ var SampleClass = function () { // 何か処理 }; /** * サンプルメソッド */ SampleClass.prototype.sampleMethod = function () { console.log('sampleMethod'); };
今回作ったBellTreeMVを使用する場合、例えばModelを定義する場合以下のように書く。
/** * サンプルModel */ var SampleModel = function () { BellTreeMV.Model.call(this); }; BellTreeMV.inheritPrototype(SampleModel.prototype, BellTreeMV.Model.prototype); SampleModel.prototype.sampleMethod = function () { console.log('sampleMethod'); };
必須なのは
BellTreeMV.Model.call(this);
と
BellTreeMV.inheritPrototype(SampleModel.prototype, BellTreeMV.Model.prototype);
の行のみ。 あとは昔からある見慣れたコンストラクタ関数の定義と、prototypeにメソッドを定義しているだけ。
まだまだライブラリと呼ぶにはあまりに拙く機能もすくないけど、わりと自己満足はできてるし、オブザーバパターンの良い勉強になった。
React + browserify 用 gulpfile メモ
React で browserify を使用する場合用のgulpfileを作ったのでメモ。 99%自分のメモ用。
uglify がファイルを縮小させるやつ。 plumber がビルドで失敗しても、ファイル変更の監視をやめないようにするやつ。 reactify がJSXをJavaScriptに変換するやつ。
var gulp = require('gulp'); var uglify = require('gulp-uglify'); var plumber = require('gulp-plumber'); var browserify = require('browserify'); var source = require("vinyl-source-stream"); var reactify = require('reactify'); var buffer = require('vinyl-buffer'); gulp.task('dist', function(){ var bundler = browserify('./script/src/main.jsx', { debug: true }); return bundler .transform(reactify) .bundle() .pipe(plumber()) .pipe(source('build.js')) .pipe(buffer()) .pipe(uglify()) .pipe(gulp.dest('./script/dist/')); }); gulp.task('watch', function() { gulp.watch('./script/src/*.js', ['dist']); gulp.watch('./script/src/*.jsx', ['dist']); gulp.watch('./script/src/**/*.js', ['dist']); gulp.watch('./script/src/**/*.jsx', ['dist']); }); gulp.task('default', ['dist', 'watch']);