うごく生ゴミプログラマの備忘録

うごく生ゴミ 〜再臨〜

外部ライブラリ使っているAWS Lambda関数(Python)のzipファイル作成シェルスクリプト(Poetry管理プロジェクト)

Poetryで管理しているPythonで書いたAWS Lambdaの関数(外部ライブラリも使っている)を、zipにまとめるスクリプトを書いたので備忘録として残しておく。

フォルダ構成

アプローチ

Poetryで管理している場合、 .venv/lib/python*/site-packages/ に poetry add したライブラリがインストールされるので、site-pckages 下のファイルと、自分で書いたソースコードをガッチャンコしたフォルダを作って、それをzipコマンドでzipにまとめるというやり方をした。

ただし .venv フォルダをプロジェクトディレクトリ内に作成するには、

poetry config virtualenvs.in-project true

で、Poetryの仮想環境をプロジェクトディレクトリ内に作成するよう設定が必要なので注意。

ワイはDockerfileに

RUN poetry config virtualenvs.in-project true

って書いてる。

スクリプト

build.sh の全文

#!/bin/sh

cd `dirname $0`
cd ../

rm -rf ./dist/
mkdir -p ./dist/package/

cp --recursive ./.venv/lib/python*/site-packages/. ./dist/package/
cp --recursive ./src/. ./dist/package/

cd ./dist/package/
zip -r ../package ./

cd ../
rm -rf ./package/

ポイント

srcディレクトリと.venv/lib/python*/site-packages/ を dist/package/ ディレクトリにコピーしている。

cp --recursive ./.venv/lib/python*/site-packages/. ./dist/package/
cp --recursive ./src/. ./dist/package/

zipを作成した後

zipを作成した後は、マネジメントコンソールからアップロードするなり、aws cli からアップロードするなり好きにしましょうという感じ。

ワイは以下のようなシェルスクリプト(deploy.shに該当)を書いて、S3にアップロードしてからLambda関数を更新するようにしてみた。

#!/bin/sh

cd `dirname $0`
cd ../

aws s3 cp ./dist/package.zip s3://<Amazon S3バケット名>/<S3キー>

aws lambda update-function-code \
    --function-name <AWS Lambda関数名> \
    --s3-bucket <バケット名> \
    --s3-key <S3キー>

LangChain(TypeScript)のRetrievalQAChainとOpenAI APIで、自前のドキュメントに関する質問に答えてくれるプログラムを作ってみた。

概要

OpenAIのChatGPTさん、めっちゃ頭良いけど、自分しか持って無くてインターネットに公開していない情報については回答してくれないので、自分しか持ってないドキュメントの内容に関する質問に回答してくれるプログラムを作ってみたので、その備忘録。

GitHubリポジトリ

こちらのリポジトリソースコードはアップロードしています。

github.com

使う技術

  • Node.js(バージョン18.14.2)

  • TypeScript

  • LangChain(TypeScript/JavaScript 版)

  • OpenAI APIAPIキーを発行します)

ダミーファイルを作成

そんなすぐに、良い感じのドキュメントを用意できないので、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に対応させれば、より汎用的に使えるようになるかもしれないなと思いました。

Dockerfileで、PATH通らなかった

最初こう書いてたんだけど、Node.jsのPATHが上手く通ってなくって、

ENV PATH $HOME/.nodebrew/current/bin:$PATH

↓のように書き直したら通りました。

ENV PATH /root/.nodebrew/current/bin:$PATH

github.com

以上でした。おやすみなさい。

技術書典2に出店しました

技術書典2に出店してきました。

techbookfest.org

参加された方とか運営の方とかおつかれさまでした。

今回は『TypeScriptでつくるシングルページアプリケーション』という本のみ販売しました。

TypeScriptでつくるシングルページアプリケーション内のソースは以下のリポジトリで公開しています。 また、今回印刷した30部しか刷らなかったため、すぐに売り切れてしまい、 「githubの方には本文は載ってないんですか?」って結構聞かれたので、リポジトリの方には、本文のソースとしてワードファイルをあげました。

Microsoft Office ワードを持っていない場合、Microsoft Office OnlineやLibre Officeなどで開けば、若干デザインは崩れるかもですが、一応読むことはできると思います。

また、同様の内容の本を4月30日の超技術書典でも販売する予定です。

techbookfest.org

次はポスター的なものとか、机の上にかけるシート的なものとか準備して、もう少しブースっぽく構えたいなーと思いました。

まる。

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"
  },
  // 省略
}

結論

できた!

f:id:jBellTree:20170213143918p:plain

できた!

こうする!

これを(タスクの種類で 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 側の設定を変えてあげる必要がある。「ワークスペース設定」を開き…

f:id:jBellTree:20160926005606p:plain

以下のように、ワークスペースの設定でTypeScriptを自分がインストールしたものを指定してあげる。

f:id:jBellTree:20160926005842p:plain

すると、

f:id:jBellTree:20160926012505p:plain

うまくいった!めでたしめでたし。