LangChain.jsでLangChain.jsレポジトリを読み込む方法をLangChain.jsに教えてもらった

## はじめに

ご存知の通り、**2023年6月** 現在、世の中は **生成AI・LLMブーム** の真っ只中です。
なかでも **OpenAI API** と **LangChain** は、**LLM** の実用的な手段として最も注目されているといって良いでしょう。

では、**このビッグウェーブに乗り遅れない** ためにはどうすればいいでしょうか。

少し考えてみると **「LangChain を使ってLangChainレポジトリを読み込む方法をLangChainに教えてもらえるようにする」** ことから始めるのが最も効率的と言えるでしょう。間違いない。

本記事では、その方法について解説していきます。

## バージョン情報

- Node: v18.15.0
- ライブラリ
  - langchain: 0.0.92 （今回はJavaScript版を用いますが、Python版もあります）
  - typescript: 5.0.4
  - openai: 3.2.1

## とりあえずドキュメント

まずはドキュメントを見ていきましょう。

https://js.langchain.com/docs/

検索ボックスを押すと、なんと **対話形式でドキュメントを検索** することができます。これはさすがですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531685/b511b313-7bc0-0ec3-bbfc-88adc9c6db9f.png)

**一見目標がすでに達成されている** ようにも感じますが、 LangChain は非常に更新の早いライブラリのためドキュメントが信用出来ないかもしれません。**きっとそうに違いありません**。レポジトリを読み込む必要があるはずなのです。

画像の範囲外ですが、スクロールすると、参考になるドキュメントのリンクも教えてくれます。そちらも見ておきましょう。以下になります。

https://js.langchain.com/docs/modules/indexes/document_loaders/examples/web_loaders/github

まあとにかく`GitHubLoader` を使えば良いわけですね。

## ここで事前知識を唐突に

今回のケースでは、LangChain にいくつかある機能のうち **LangChain Indexes** を用いることになります。

ドキュメントは以下になります。

https://js.langchain.com/docs/modules/indexes/

また、概要を把握する上では、以下の記事が非常にわかりやすいです。

https://zenn.dev/umi_mori/books/prompt-engineer/viewer/langchain_indexes

簡単に言えば、GitHubレポジトリやPDFなどの **あるデータソースから情報を得る** のに便利な機能になります。

ドキュメントをざっと見ると、今回は以下のような手順で利用していけば良さそうでした。
1. **Document Loader を用いて、データを読み込む**
    - 上述の `GitHubLoader` がこれ（Web Loaders と呼ばれているものの1つ）
    - そのほかに、CSVやPDFを読み込む File Loaders と呼ばれるものが用意されている
1. **Text Splitter を用いて、データを適切な大きさに分割する**
1. **分割したデータをベクトル化して、Vector Store に格納**
1. **Retriever を用いて、Vectore Store から検索し返答**
    - 専門外なので詳細は不明ですが少なくとも LangChain では、[「文字列クエリを受け取ってドキュメントのリストを返すインターフェースを持つもの」を Retriever と呼んでいる](https://docs.langchain.com/docs/components/indexing/retriever)ようです。
    - あくまで今回は Vectore Store を Retriever として使用するというだけで、他のケースも存在するようです。


## 試してみる

※ 事前に OpenAI API のアクセスキーと、GitHub Personal Access Token は取得済みです。また環境変数として、`OPEN_AI_API_KEY`, `GITHUB_ACCESS_TOKEN` を設定できるようにしています


後から、一部補足しますが、全体としては今回このようなコードにしてみました。
設定や型定義、実行時間を測定する処理などは入ってますが、上に書いた各ステップそのままですね。

```ts:main.ts
import { OpenAIEmbeddings } from "langchain/embeddings/openai";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { RetrievalQAChain } from "langchain/chains";
import { OpenAIChat } from "langchain/llms/openai";
import { ChainValues } from "langchain/schema";
import {
  GithubRepoLoader,
  GithubRepoLoaderParams,
} from "langchain/document_loaders/web/github";

// 実行のための設定をベタ書き（「質問文」や「読み込むレポジトリのURL」など）
const question = "How to configure distributed map state?";  
const ignorePattern = /^(?!.*\.(js|ts)$).*$/;
const config: GhchainConfig = {
  question: question,
  repositoryUrl: "https://github.com/serverless-operations/serverless-step-functions",
  chatApiModel: "gpt-3.5-turbo", 
  loadRepositoryOptions: {
    branch: "master",
    recursive: true,
    unknown: "warn",
    ignoreFiles: [ignorePattern],
  },
};

// 入出力の型定義
type GhchainConfig = {
  question: string;
  chatApiModel: string;
  repositoryUrl: string;
  loadRepositoryOptions: GithubRepoLoaderParams;
};
type GhchainOutput = { Q: string; response: ChainValues };

// 処理定義
const ghchain = async (config: GhchainConfig): Promise<GhchainOutput> => {
  const { question, repositoryUrl, chatApiModel } = config;

  // 1. Document Loader を用いて、データを読み込む
  console.time("Load GitHub Repository");
  const loader = new GithubRepoLoader(repositoryUrl, {
    ...config.loadRepositoryOptions,
  });
  const docs = await loader.load();
  console.timeEnd("Load GitHub Repository");

  // 2. Text Splitter を用いて、データを適切な大きさに分割する
  // 3. 分割したデータをベクトル化して、Vector Store に格納
  console.time("Split & Embed Documents");
  const splitter = new RecursiveCharacterTextSplitter({});
  const splittedDocs = await splitter.splitDocuments(docs);
  const vectorStore = await MemoryVectorStore.fromDocuments(
    splittedDocs,
    new OpenAIEmbeddings({ openAIApiKey: process.env.OPEN_AI_API_KEY })
  );
  console.timeEnd("Split & Embed Documents");

  // 4. Retriever を用いて、Vectore Store から検索し返答
  console.time("Create Answer");
  const model = new OpenAIChat({
    openAIApiKey: process.env.OPEN_AI_API_KEY,
    modelName: chatApiModel,
  });
  const chain = RetrievalQAChain.fromLLM(model, vectorStore.asRetriever());
  const response = await chain.call({ query: question });
  console.timeEnd("Create Answer");
  return { Q: question, response: response };
};

// 処理の実行
const main = async (config: GhchainConfig) => {
  const result = await ghchain(config);
  console.log({ Q: result.Q, A: result.response.text });
};

main(config);
```

### 補足（読み飛ばしても問題ない）
#### `chatApiModel` について

コード上では `gpt-3.5-turbo` を指定していますが、もちろん `gpt-4` も指定可能です。御存知の通り回答の精度が高まります（が、お金もかかります）。

#### 各処理後の値

各ステップで得られた `docs`, `splittedDocs`, `vectorStore` がどういう形式か気になっている人も多いと思うので確認しておきます。

まず、 `GithubRepoLoader` でloadした `docs` ですが、`pageContent` （ファイルの中身） と `metadata` からなるオブジェクトのリストであることがわかりますね。`metadata`からファイルパスもわかります。

今回は `.js`, `.ts` ファイルはすべて読む設定にしているため、`.eslintrc.js` のようなファイルも読まれてしまっていますが、ここは `ignore` の設定次第で制御可能です。

```json:docs
[
  {
    "pageContent": "module.exports = {\n  extends: 'airbnb',\n  plugins: [],\n  rules: {\n    'func-names': 'off',\n    'prefer-destructuring': 'off',\n    'no-plusplus': 'off',\n    'no-template-curly-in-string': 'off',\n    'no-restricted-syntax': 'off',\n    strict: 'off',\n    'prefer-rest-params': 'off',\n    'react/require-extension': 'off',\n    'import/no-extraneous-dependencies': 'off',\n  },\n  env: {\n    mocha: true,\n  },\n};\n",
    "metadata": {
      "source": ".eslintrc.js"
    }
  },
  {
    "pageContent": "module.exports = { extends: ['@commitlint/config-conventional'] };\n",
    "metadata": {
      "source": "commitlint.config.js"
    }
  },
  {
    "pageContent": "'use strict';\n\nconst _ = require('lodash');\nconst BbPromise = require('bluebird');\n\nmodule.exports = {\n  compileApiKeys() {\n    const apiKeys = _.get(this.serverless.service.provider.apiGateway, 'apiKeys')\n      || this.serverless.service.provider.apiKeys;\n    if (apiKeys) {\n      if (!Array.isArray(apiKeys)) {\n        throw new this.serverless.classes.Error('apiKeys property must be an array');\n      }\n\n      _.forEach(apiKeys, (apiKey, i) => {\n        const apiKeyNumber = i + 1;\n\n        if (typeof apiKey !== 'string') {\n          throw new this.serverless.classes.Error('API Keys must be strings');\n        }\n\n        const apiKeyLogicalId = this.provider.naming\n          .getApiKeyLogicalId(apiKeyNumber);\n\n        _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {\n          [apiKeyLogicalId]: {\n            Type: 'AWS::ApiGateway::ApiKey',\n            Properties: {\n              Enabled: true,\n              Name: apiKey,\n              StageKeys: [{\n                RestApiId: { Ref: this.apiGatewayRestApiLogicalId },\n                StageName: this.provider.getStage(),\n              }],\n            },\n            DependsOn: this.apiGatewayDeploymentLogicalId,\n          },\n        });\n      });\n    }\n    return BbPromise.resolve();\n  },\n};\n",
    "metadata": {
      "source": "lib/deploy/events/apiGateway/apiKeys.js"
    }
  },
```

次に `splittedDocs` ですが、基本的な形式は同じですね。ただ、その名の通りファイルが一定の大きさごとに分割され、その位置のメタデータが含まれます。今回はデフォルト値ですが `chunkSize` オプションなどがあるようなので、必要に応じて設定してください。

```json:splittedDocs
[
  {
    "pageContent": "'use strict';\n\nconst _ = require('lodash');\nconst BbPromise = require('bluebird');\n\nmodule.exports = {\n  compileApiKeys() {\n    const apiKeys = _.get(this.serverless.service.provider.apiGateway, 'apiKeys')\n      || this.serverless.service.provider.apiKeys;\n    if (apiKeys) {\n      if (!Array.isArray(apiKeys)) {\n        throw new this.serverless.classes.Error('apiKeys property must be an array');\n      }\n\n      _.forEach(apiKeys, (apiKey, i) => {\n        const apiKeyNumber = i + 1;\n\n        if (typeof apiKey !== 'string') {\n          throw new this.serverless.classes.Error('API Keys must be strings');\n        }\n\n        const apiKeyLogicalId = this.provider.naming\n          .getApiKeyLogicalId(apiKeyNumber);",
    "metadata": {
      "source": "lib/deploy/events/apiGateway/apiKeys.js",
      "loc": {
        "lines": {
          "from": 1,
          "to": 23
        }
      }
    }
  },
  {
    "pageContent": "const apiKeyLogicalId = this.provider.naming\n          .getApiKeyLogicalId(apiKeyNumber);\n\n        _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {\n          [apiKeyLogicalId]: {\n            Type: 'AWS::ApiGateway::ApiKey',\n            Properties: {\n              Enabled: true,\n              Name: apiKey,\n              StageKeys: [{\n                RestApiId: { Ref: this.apiGatewayRestApiLogicalId },\n                StageName: this.provider.getStage(),\n              }],\n            },\n            DependsOn: this.apiGatewayDeploymentLogicalId,\n          },\n        });\n      });\n    }\n    return BbPromise.resolve();\n  },\n};",
    "metadata": {
      "source": "lib/deploy/events/apiGateway/apiKeys.js",
      "loc": {
        "lines": {
          "from": 23,
          "to": 44
        }
      }
    }
  },
```

最後に `vectorStore` です。`memoryVectors` というキーに、先程の `content` と `metadata` 以外に、数値ベクトルが格納されているのがわかりますね。あとはembeddingを行った設定などが入るようです（APIキーだけは手動で適当な値に置き換えています）

```json:vectorStore
{
  "embeddings": {
    "caller": {
      "maxConcurrency": null,
      "maxRetries": 6,
      "queue": {
        "_events": {},
        "_eventsCount": 0,
        "_intervalCount": 1,
        "_intervalEnd": 0,
        "_pendingCount": 0,
        "_carryoverConcurrencyCount": false,
        "_isIntervalIgnored": true,
        "_intervalCap": null,
        "_interval": 0,
        "_queue": {
          "_queue": []
        },
        "_concurrency": null,
        "_throwOnTimeout": false,
        "_isPaused": false
      }
    },
    "modelName": "text-embedding-ada-002",
    "batchSize": 512,
    "stripNewLines": true,
    "client": {
      "basePath": "https://api.openai.com/v1",
      "configuration": {
        "apiKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        "baseOptions": {
          "headers": {
            "User-Agent": "OpenAI/NodeJS/3.2.1",
            "Authorization": "Bearer xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
          }
        }
      }
    },
    "clientConfig": {
      "apiKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    }
  },
  "memoryVectors": [
    {
      "content": "module.exports = {\n  extends: 'airbnb',\n  plugins: [],\n  rules: {\n    'func-names': 'off',\n    'prefer-destructuring': 'off',\n    'no-plusplus': 'off',\n    'no-template-curly-in-string': 'off',\n    'no-restricted-syntax': 'off',\n    strict: 'off',\n    'prefer-rest-params': 'off',\n    'react/require-extension': 'off',\n    'import/no-extraneous-dependencies': 'off',\n  },\n  env: {\n    mocha: true,\n  },\n};",
      "embedding": [
        0.023345746,
        0.008848897,
        0.01596505,
        -0.013114621,
        -0.017142259,
        0.03277663,

        （長いので省略）

        -0.009741724,
        0.009358139,
        -0.023583833
      ],
      "metadata": {
        "source": ".eslintrc.js",
        "loc": {
          "lines": {
            "from": 1,
            "to": 18
          }
        }
      }
    },
```

### 実行結果

**「（AWS の Step Functions で）Distributed Map を設定するにはどうすればよい？」** という質問を、Serverless Framework の plugin である [serverless-step-functions](  https://github.com/serverless-operations/serverless-step-functions) というレポジトリを読ませて聞いています。

**この Distributed Map という設定は 2022年12月に発表された** ものです。
（参考: [その時に書いたQiitaの記事](https://qiita.com/SaitomTech/items/0fe63e0fa1d1762a631f)）

ChatGPT は検索等のプラグインを用いない場合、**2021年9月までの情報しか持たない** ことから、この質問に正確に答えられていることは、GitHub レポジトリから正しく情報を取得できた証拠となります。

気になる結果は以下になります。`ItemProcessor` の `ProcessorConfig` の `Mode` を `DISTRIBUTED` に設定しろと言っており、こちらは正しい回答です（[AWS公式](https://docs.aws.amazon.com/step-functions/latest/dg/concepts-asl-use-map-state-distributed.html#distributed-map-example)）。

```
Load GitHub Repository: 22.209s
Split & Embed Documents: 6.304s
Create Answer: 2.738s
{
  Q: 'How to configure distributed map state?',
  A: "To configure distributed map state, use the ProcessorConfig object with Mode set to 'DISTRIBUTED' within the ItemProcessor property of a Map state's definition."
}
```

### これで目標達成かと思いきや

勢いそのままに [hwchase17/langchainjs](https://github.com/hwchase17/langchainjs) レポジトリで試してみるのですが、**一向にレポジトリのロードが終わりません**。

原因を探し、解決していきましょう。

## 原因究明のためオリジナルのコードを見に行く

[langchain/src/document_loaders/web/github.ts](https://github.com/hwchase17/langchainjs/blob/main/langchain/src/document_loaders/web/github.ts) が該当のファイルになります。

コードを見てみると、どうやら **各ファイル全てに対しGitHub API を再帰的に叩いている** ことがわかります。（推測ではありますが）これが原因で、大きめなレポジトリではロードが終わらなかったと考えられるでしょう。

```ts:langchain/src/document_loaders/web/github.ts
 private async processDirectory(
    path: string,
    documents: Document[]
  ): Promise<void> {
    try {
      const files = await this.fetchRepoFiles(path);

      for (const file of files) {
        if (!(await this.shouldIgnore(file.path, file.type))) {
          if (file.type !== "dir") {
            try {
              const fileContent = await this.fetchFileContent(file);
              const metadata = { source: file.path };
              documents.push(
                new Document({ pageContent: fileContent, metadata })
              );
            } catch (e) {
              this.handleError(
                `Failed to fetch file content: ${file.path}, ${e}`
              );
            }
          } else if (this.recursive) {
            await this.processDirectory(file.path, documents);
          }
        }
      }
    } catch (error) {
      this.handleError(`Failed to process directory: ${path}, ${error}`);
    }
  }

  private async fetchRepoFiles(path: string): Promise<GithubFile[]> {
    const url = `https://api.github.com/repos/${this.owner}/${this.repo}/contents/${path}?ref=${this.branch}`;
    const response = await fetch(url, { headers: this.headers });
    const data = await response.json();
    if (!response.ok) {
      throw new Error(
        `Unable to fetch repository files: ${response.status} ${JSON.stringify(
          data
        )}`
      );
    }

    if (!Array.isArray(data)) {
      throw new Error("Unable to fetch repository files.");
    }

    return data as GithubFile[];
  }
```

## どう直す？

結構困るところですが、Python版のLangChainを使った場合には、GitHubレポジトリから読み出して、それなりに動かせている人がいることを知りました。Python版との違いが参考になりそうです。

https://www.youtube.com/watch?v=xEjpMTMy24Y

というわけで、Python版のコードを見てみます。
[langchain/document_loaders/git.py](https://github.com/hwchase17/langchain/blob/master/langchain/document_loaders/git.py) ですね。

これを見ると、GitHub API は使用せず、git cloneしてから、そのファイルを読み出していることがわかります。この方法ならファイル数が増えても確かに大丈夫そうです。

```py:langchain/document_loaders/git.py
import os
from typing import Callable, List, Optional

from langchain.docstore.document import Document
from langchain.document_loaders.base import BaseLoader


class GitLoader(BaseLoader):
    """Loads files from a Git repository into a list of documents.
    Repository can be local on disk available at `repo_path`,
    or remote at `clone_url` that will be cloned to `repo_path`.
    Currently supports only text files.

    Each document represents one file in the repository. The `path` points to
    the local Git repository, and the `branch` specifies the branch to load
    files from. By default, it loads from the `main` branch.
    """

    def __init__(
        self,
        repo_path: str,
        clone_url: Optional[str] = None,
        branch: Optional[str] = "main",
        file_filter: Optional[Callable[[str], bool]] = None,
    ):
        self.repo_path = repo_path
        self.clone_url = clone_url
        self.branch = branch
        self.file_filter = file_filter

    def load(self) -> List[Document]:
        try:
            from git import Blob, Repo  # type: ignore
        except ImportError as ex:
            raise ImportError(
                "Could not import git python package. "
                "Please install it with `pip install GitPython`."
            ) from ex

        if not os.path.exists(self.repo_path) and self.clone_url is None:
            raise ValueError(f"Path {self.repo_path} does not exist")
        elif self.clone_url:
            repo = Repo.clone_from(self.clone_url, self.repo_path)
            repo.git.checkout(self.branch)
        else:
            repo = Repo(self.repo_path)
            repo.git.checkout(self.branch)

        docs: List[Document] = []

        for item in repo.tree().traverse():
            if not isinstance(item, Blob):
                continue

            file_path = os.path.join(self.repo_path, item.path)

            ignored_files = repo.ignored([file_path])  # type: ignore
            if len(ignored_files):
                continue

            # uses filter to skip files
            if self.file_filter and not self.file_filter(file_path):
                continue

            rel_file_path = os.path.relpath(file_path, self.repo_path)
            try:
                with open(file_path, "rb") as f:
                    content = f.read()
                    file_type = os.path.splitext(item.name)[1]

                    # loads only text files
                    try:
                        text_content = content.decode("utf-8")
                    except UnicodeDecodeError:
                        continue

                    metadata = {
                        "source": rel_file_path,
                        "file_path": rel_file_path,
                        "file_name": item.name,
                        "file_type": file_type,
                    }
                    doc = Document(page_content=text_content, metadata=metadata)
                    docs.append(doc)
            except Exception as e:
                print(f"Error reading file {file_path}: {e}")

        return docs
```

## 直した

詳細省きますが、`simple-git` ライブラリを用いて、以下のようなファイルを作成することで、git clone形式にした上で同じインターフェースなものを実現できました。

```ts:github.ts
import * as glob from "glob";
import * as path from "path";
import { simpleGit, SimpleGit, CleanOptions } from "simple-git";
import { lstatSync, readFileSync } from "fs";
import { rm } from "fs/promises";
import binaryExtensions from "binary-extensions";
import { Document } from "langchain/document";
import { BaseDocumentLoader } from "langchain/document_loaders/base";
import { UnknownHandling } from "langchain/document_loaders/fs/directory";

const extensions = new Set(binaryExtensions);

const extname = (path: string) => `.${path.split(".").pop()}`;
function isBinaryPath(name: string) {
  return extensions.has(extname(name).slice(1).toLowerCase());
}

interface GithubFile {
  path: string;
  type: "file" | "dir";
}

export interface GithubRepoLoaderParams {
  branch?: string;
  recursive?: boolean;
  unknown?: UnknownHandling;
  accessToken?: string;
  ignoreFiles?: (string | RegExp)[];
}

export class GithubRepoLoader
  extends BaseDocumentLoader
  implements GithubRepoLoaderParams
{
  private readonly owner: string;

  private readonly repo: string;

  private readonly initialPath: string;

  public branch: string;

  public recursive: boolean;

  public unknown: UnknownHandling;

  public accessToken?: string;

  public ignoreFiles: (string | RegExp)[];

  constructor(
    githubUrl: string,
    {
      accessToken = typeof process !== "undefined"
        ? // eslint-disable-next-line no-process-env
          process.env?.GITHUB_ACCESS_TOKEN
        : undefined,
      branch = "main",
      recursive = true,
      unknown = UnknownHandling.Warn,
      ignoreFiles = [],
    }: GithubRepoLoaderParams = {}
  ) {
    super();
    const { owner, repo, path } = this.extractOwnerAndRepoAndPath(githubUrl);
    this.owner = owner;
    this.repo = repo;
    this.initialPath = path;
    this.branch = branch;
    this.recursive = recursive;
    this.unknown = unknown;
    this.accessToken = accessToken;
    this.ignoreFiles = ignoreFiles;
  }

  public async load(): Promise<Document[]> {
    const documents: Document[] = [];
    await this.cloneRepo();
    await this.processDirectory(this.initialPath, documents);
    await rm(this.repo, { recursive: true });
    return documents;
  }

  private extractOwnerAndRepoAndPath(url: string): {
    owner: string;
    repo: string;
    path: string;
  } {
    const match = url.match(
      /https:\/\/github.com\/([^/]+)\/([^/]+)(\/tree\/[^/]+\/(.+))?/i
    );

    if (!match) {
      throw new Error("Invalid GitHub URL format.");
    }

    return { owner: match[1], repo: match[2], path: match[4] || "" };
  }

  private shouldIgnore(path: string): boolean {
    return this.ignoreFiles.some((pattern) => {
      if (typeof pattern === "string") {
        return path === pattern;
      }

      try {
        return pattern.test(path);
      } catch {
        throw new Error(`Unknown ignore file pattern: ${pattern}`);
      }
    });
  }

  private async cloneRepo() {
    const git: SimpleGit = simpleGit().clean(CleanOptions.FORCE);
    await git.clone(
      `https://${this.accessToken}@github.com/${this.owner}/${this.repo}`,
      {
        "--branch": this.branch,
      }
    );
  }

  private async processDirectory(
    path: string,
    documents: Document[]
  ): Promise<void> {
    try {
      const files = this.fetchRepoFiles(path);
      const notDirFiles = files.filter((file) => file.type !== "dir");
      for (const file of notDirFiles) {
        try {
          if (!isBinaryPath(file.path) && !this.shouldIgnore(file.path)) {
            const fileContent = this.fetchFileContent(file);
            const metadata = { source: file.path };
            documents.push(
              new Document({ pageContent: fileContent, metadata })
            );
          }
        } catch (e) {
          this.handleError(`Failed to fetch file content: ${file.path}, ${e}`);
        }
      }
    } catch (error) {
      this.handleError(`Failed to process directory: ${path}, ${error}`);
    }
  }

  private fetchRepoFiles(filePath: string): GithubFile[] {
    const files = glob.sync(`${this.repo}/${filePath}/**/*`, { nodir: false });
    return files.map((file) => ({
      path: path.relative(`${this.repo}/${filePath}`, file),
      type: lstatSync(file).isDirectory() ? "dir" : "file",
    }));
  }

  private fetchFileContent(file: GithubFile): string {
    return readFileSync(`${this.repo}/${file.path}`, "utf-8");
  }

  private handleError(message: string): void {
    switch (this.unknown) {
      case UnknownHandling.Ignore:
        break;
      case UnknownHandling.Warn:
        console.warn(message);
        break;
      case UnknownHandling.Error:
        throw new Error(message);
      default:
        throw new Error(`Unknown unknown handling: ${this.unknown}`);
    }
  }
}

```

これで後は `main.ts` のimportを書き換えるだけですみます。

```ts:main.ts
// 変更前
import {
  GithubRepoLoader,
  GithubRepoLoaderParams,
} from "langchain/document_loaders/web/github";

// 変更後
import { GithubRepoLoader, GithubRepoLoaderParams } from "./github";
```

## ついに本番: LangChain.jsに聞いてみる

```
Load GitHub Repository: 2.235s
Split & Embed Documents: 41.412s
Create Answer: 2.894s
{
  Q: 'How to load langchain-js GitHub repository?',
  A: 'You can use the GithubRepoLoader from langchain/document_loaders/web/github and provide the repository URL, branch name, and any other loader options.'
}
```

`GitHubLoader` を使えば良いそうです！

なるほど！！！！！！！！！！！！！！！！！！！！！！！！！！


## 余談

### その1

この方法では **Issue の情報が取れない**のが辛いところです。
何かしらのライブラリでトラブった時にとりあえず Issue を検索してみることはよくあるので。

まあ、いずれは GitHub 公式（Copilot Chat？）でこのあたりの機能諸々すべて対応してくれると思うので、今後の発展待ちですかね :nerd: 

このあたり良い感じの解決方法ご存じの方は是非コメント欄で教えてくださいー

### その2

今回は本家 ChatGPT のような対話形式には非対応ですが、[LangChain Memory](https://js.langchain.com/docs/modules/memory/) という機能があるようなので、そちらを使えば対話も可能そうです。

### その3

gpt-4 を使うともっといい感じにコード付きで答えてくれます。すごいですね。

```
Load GitHub Repository: 1.590s
Split & Embed Documents: 29.701s
Create Answer: 18.287s
{
  Q: 'How to load langchain-js GitHub repository?',
  A: `To load the langchain-js GitHub repository, you can use the GithubRepoLoader from the "langchain/document_loaders/web/github" module. Here's an example:\n` +
    '\n' +
    '```javascript\n' +
    'import { GithubRepoLoader } from "langchain/document_loaders/web/github";\n' +
    '\n' +
    'export const run = async () => {\n' +
    '  const loader = new GithubRepoLoader(\n' +
    '    "https://github.com/hwchase17/langchainjs",\n' +
    '    { branch: "main", recursive: false, unknown: "warn" }\n' +
    '  );\n' +
    '  const docs = await loader.load();\n' +
    '  console.log({ docs });\n' +
    '};\n' +
    '```'
}
```

### その4

今回は `MemoryVectorStore` にベクトルを格納していますが、**外部DBを使用することが可能** です。この実装ではEmbeddingにもAPIを使用しているため余計な金銭的コストがかかるうえ、毎回の計算に時間がかかります。外部DBの使用でこの問題を解決させられるものと思われます。