GitHub Organizationのレポジトリからllms.txtを作成する
llms.txtとは?
AIが処理しやすい形にサイトの構造情報をまとめたファイルです。AIクローラーに参照させることでサイトへの負荷を軽減します。
フォーマットは以下のような形です。(そのほか、サポート言語やRate Limitなどの情報を含めることもあります。)
# Title
> Optional description goes here
Optional details go here
## Section name
- [Link title](https://link_url): Optional link details
## Optional
- [Link title](https://link_url)
参考:
サイトの負荷軽減だけではなく、情報検索の効率化にも応用できる可能性があります。
こちらの記事では、RAGとllms.txtを使ったコード生成タスクの性能を比較しています。結果、llms.txtをうまく作ることで、コード生成タスクの性能を向上させられる可能性を示しています。
やりたいこと
GitHub Organizationのレポジトリからllms.txtを作成します。
これによって、社内資産を活用したコード生成を目指します。
準備
GitHub Organizationの設定
今回は個人のレポジトリではなく、Organizationのレポジトリを対象にします。(とはいっても、個人でもほとんど同じです。)
今回は、FreeプランのOrganizationを作成して実験しました。
Personal Access Tokenの作成
GitHubのリソースを編集するため、Personal Access Token(PAT)を作成します。
今回は検証用に多めに権限を付与します。ご利用の環境に応じて、適宜権限を調整してください。
GitHubにアクセスし、右上のアイコンから、Settings -> Developer settings -> Personal access tokens -> Fine-grained tokens -> Generate new tokenを選択します。
権限は以下のように設定しました。
Repository Access: All Repositories
Repository Permissions:
- Actions: Read and Write
- Commit statuses: Read and Write
- Contents: Read and Write
- Environments: Read-Only
- Issues: Read and Write
- Pull requests: Read and Write
- Secrets: Read-Only
- Variables: Read-only
llms.txt管理レポジトリの設定
今回はllms.txtを一つのレポジトリに集約させます。そのための管理レポジトリを作成しました。これはPublicでもPrivateでも構いません。
ツールのインストール
今回はTypeScriptを使用します。必要なライブラリをインストールしましょう。
npm install dotenv axios @langchain/core @langchain/openai
実装
以下の処理を実装します。
- 指定されたOrganizationの全レポジトリを取得する。
- 各レポジトリのファイル一覧を取得する。
- 各ファイルの内容(ファイルコンテンツ)を取得する。
- ファイルコンテンツをもとにllms.txtを作成する。
- llms.txtを指定のレポジトリにpushする。
0. 環境変数の設定
GitHub API、OpenAIのAPIキーおよびOrganization・レポジトリを環境変数で設定します。.envを作成して以下のように設定します。
GITHUB_TOKEN=<GitHubのPersonal Access Tokenを指定>
ORGANIZATION=<GitHub Organization名を指定>
GITHUB_MAIN_REPOSITORY=<手順5で使用する、llms.txt管理レポジトリ名を指定>
OPENAI_API_KEY=<OpenAIのAPIキーを指定>
MODEL_NAME=<使用するOpenAIのモデル名を指定>
1. 指定されたOrganizationの全レポジトリを取得する。
GitHub APIを使用して、指定されたOrganizationの全レポジトリを取得します。
// Organizationのリポジトリ一覧を取得
async function listOrganizationRepositories(organization: string) {
// GITHUB_TOKEN を環境変数から取得
const token = process.env.GITHUB_TOKEN;
if (!token) {
throw new Error('GITHUB_TOKEN 環境変数が設定されていません。');
}
try {
// GET /orgs/{org}/repos エンドポイントを呼び出して organization のリポジトリ一覧を取得する
const response = await axios.get(`https://api.github.com/orgs/${organization}/repos`, {
headers: {
'Authorization': `token ${token}`,
'Accept': 'application/vnd.github+json'
}
});
return response.data;
} catch (error) {
console.error('Organization のリポジトリ一覧の取得に失敗しました:', error);
}
}
2. 各レポジトリのファイル一覧を取得する。
1で取得したレポジトリ内のファイル一覧を取得します。
async function listRepositoryContentsRecursive(organization: string, repository: string, path: string="", aggregatedFiles: any[]=[]): Promise<any[]> {
// GITHUB_TOKEN を環境変数から取得
const token = process.env.GITHUB_TOKEN;
if (!token) {
throw new Error('GITHUB_TOKEN 環境変数が設定されていません。');
}
try {
const response = await axios.get(`https://api.github.com/repos/${organization}/${repository}/contents/${path}`, {
headers: {
'Authorization': `token ${token}`,
'Accept': 'application/vnd.github+json'
}
});
if (Array.isArray(response.data)) {
for (const item of response.data) {
if (item.type === 'file') {
aggregatedFiles.push(item);
} else if (item.type === 'dir') {
// For directories, recursively fetch its contents and filter for files only
aggregatedFiles = await listRepositoryContentsRecursive(organization, repository, item.path, aggregatedFiles);
}
}
}
return aggregatedFiles;
} catch (error) {
console.error('リポジトリのコンテンツ一覧の取得に失敗しました:', error);
return aggregatedFiles;
}
}
3. 各ファイルの内容(ファイルコンテンツ)を取得する。
2で取得したファイル一覧から、ファイルコンテンツを取得します。
async function getFileContent(organization: string, repository: string, path: string) {
// GITHUB_TOKEN を環境変数から取得
const token = process.env.GITHUB_TOKEN;
if (!token) {
throw new Error('GITHUB_TOKEN 環境変数が設定されていません。');
}
try {
const response = await axios.get(`https://api.github.com/repos/${organization}/${repository}/contents/${path}`, {
headers: {
'Authorization': `token ${token}`,
'Accept': 'application/vnd.github+json'
}
});
return response.data;
} catch (error) {
console.error('ファイルの内容の取得に失敗しました:', error);
return null;
}
}
4. ファイルコンテンツをもとにllms.txtを作成する。
生成AIを用いて、ファイルコンテンツからllms.txtを作成します。
async function createLLMsTxt(llm: BaseChatModel, repository_name: string, repository_url: string, file_contents: string): Promise<string> {
const prompt = new PromptTemplate({
template: PROMPT_TEMPLATE,
inputVariables: ['repository_name', 'repository_url', 'file_contents'],
});
const chain = prompt.pipe(llm);
const result = await chain.invoke({
repository_name: repository_name,
repository_url: repository_url,
file_contents: file_contents,
});
const result_str = result.content as string;
const result_match = result_str.match(/<output>\n*([\s\S]*?)\n*<\/output>/);
if (result_match) {
return result_match[1];
} else {
return result_str;
}
}
5. llms.txtを指定のレポジトリにpushする。
最後に、4で作成したllms.txtを指定のレポジトリにpushします。
レポジトリへのpushは以下の手順で行います。
- 現在のコミット情報を取得
- 新しいブランチを作成
- 作成したllms.txtを<repository_name>/llms.txtにpush
- プルリクエストを作成
5.1 現在のコミット情報を取得
async function getLatestCommitSha(organization: string, repository: string, branch: string): Promise<string> {
// 環境変数からトークンを取得
const token = process.env.GITHUB_TOKEN;
if (!token) {
throw new Error('GITHUB_TOKEN 環境変数が設定されていません。');
}
try {
const response = await axios.get(`https://api.github.com/repos/${organization}/${repository}/git/ref/heads/${branch}`, {
headers: {
'Authorization': `token ${token}`,
'Accept': 'application/vnd.github+json'
}
});
return response.data.object.sha;
} catch (error: any) {
console.error('An error occurred:', error.response?.data || error.message);
throw error;
}
}
5.2 新しいブランチを作成
async function createBranch(organization: string, repository: string, newBranch: string, sha: string): Promise<void> {
// 環境変数からトークンを取得
const token = process.env.GITHUB_TOKEN;
if (!token) {
throw new Error('GITHUB_TOKEN 環境変数が設定されていません。');
}
try {
await axios.post(`https://api.github.com/repos/${organization}/${repository}/git/refs`, {
ref: `refs/heads/${newBranch}`,
sha,
}, {
headers: {
'Authorization': `token ${token}`,
'Accept': 'application/vnd.github+json'
}
});
} catch (error: any) {
console.error('An error occurred:', error.response?.data || error.message);
throw error;
}
}
5.3 作成したllms.txtを<repository_name>/llms.txtにpush
async function createFile(organization: string, repository: string, branch: string, filePath: string, content: string, message: string): Promise<void> {
// 環境変数からトークンを取得
const token = process.env.GITHUB_TOKEN;
if (!token) {
throw new Error('GITHUB_TOKEN 環境変数が設定されていません。');
}
try {
const base64Content = Buffer.from(content).toString('base64');
await axios.put(`https://api.github.com/repos/${organization}/${repository}/contents/${filePath}`, {
message,
content: base64Content,
branch,
}, {
headers: {
'Authorization': `token ${token}`,
'Accept': 'application/vnd.github+json'
}
});
} catch (error: any) {
console.error('An error occurred:', error.response?.data || error.message);
throw error;
}
}
5.4 プルリクエストを作成
async function createPullRequest(organization: string, repository: string, title: string, body: string, head: string, base: string): Promise<void> {
// 環境変数からトークンを取得
const token = process.env.GITHUB_TOKEN;
if (!token) {
throw new Error('GITHUB_TOKEN 環境変数が設定されていません。');
}
try {
await axios.post(`https://api.github.com/repos/${organization}/${repository}/pulls`, {
title,
body,
head,
base,
}, {
headers: {
'Authorization': `token ${token}`,
'Accept': 'application/vnd.github+json'
}
});
} catch (error: any) {
console.error('An error occurred:', error.response?.data || error.message);
throw error;
}
}
実行
最後に作成したファイルを実行します。(コード全文は最後に掲載します。)
npx tsx create_llms_txt.ts
結果、llms.txt管理用レポジトリに、各レポジトリ名のフォルダが作成され、その中に該当レポジトリのllms.txtが作成されます。
まとめ
今回は、GitHub Organizationのレポジトリからllms.txtを作成する方法を紹介しました。
生成AIを利用して作成するため、RAGよりも検索データの作成が容易です。
今後、llms.txtを更新する方法や、コード検索に使用する方法も調査していきたいと思います。
また今回は通常のフォーマットでllms.txtを作成しましたが、llmstxt_architectを使うとより精度の高いllms.txtを作成できるかもしれません。(Vibe Codeの記事では、llmstxt_architectを使ってllms.txtを作成しているようです。)
コード全文
import "dotenv/config";
import axios from 'axios';
import { ChatOpenAI } from '@langchain/openai';
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { PromptTemplate } from '@langchain/core/prompts';
const PROMPT_TEMPLATE = `レポジトリ情報をもとに以下の形式でllms.txtを出力してください。
レポジトリ情報:
\`\`\`\`
レポジトリ名: {repository_name}
レポジトリURL: {repository_url}
レポジトリ内のファイル内容:
{file_contents}
\`\`\`\`
llms.txtの出力形式:
以下のように<output>タグ内に必要な情報を記載してください。
<output>
# レポジトリ名[レポジトリURL]
> プロジェクト概要説明
プロジェクトの詳細説明(500文字以内で記載)
## ファイル一覧
- ファイル名1[ファイルパス1]: ファイル1の概要説明(300文字以内で記載)
- ファイル名2[ファイルパス2]: ファイル2の概要説明(300文字以内で記載)
...
</output>
それではタスクを開始してください。
`;
// Organizationのリポジトリ一覧を取得
async function listOrganizationRepositories(organization: string) {
// GITHUB_TOKEN を環境変数から取得
const token = process.env.GITHUB_TOKEN;
if (!token) {
throw new Error('GITHUB_TOKEN 環境変数が設定されていません。');
}
try {
const response = await axios.get(`https://api.github.com/orgs/${organization}/repos`, {
headers: {
'Authorization': `token ${token}`,
'Accept': 'application/vnd.github+json'
}
});
return response.data;
} catch (error) {
console.error('Organization のリポジトリ一覧の取得に失敗しました:', error);
}
}
async function listRepositoryContentsRecursive(organization: string, repository: string, path: string="", aggregatedFiles: any[]=[]): Promise<any[]> {
// GITHUB_TOKEN を環境変数から取得
const token = process.env.GITHUB_TOKEN;
if (!token) {
throw new Error('GITHUB_TOKEN 環境変数が設定されていません。');
}
try {
const response = await axios.get(`https://api.github.com/repos/${organization}/${repository}/contents/${path}`, {
headers: {
'Authorization': `token ${token}`,
'Accept': 'application/vnd.github+json'
}
});
if (Array.isArray(response.data)) {
for (const item of response.data) {
if (item.type === 'file') {
aggregatedFiles.push(item);
} else if (item.type === 'dir') {
// For directories, recursively fetch its contents and filter for files only
aggregatedFiles = await listRepositoryContentsRecursive(organization, repository, item.path, aggregatedFiles);
}
}
}
return aggregatedFiles;
} catch (error) {
console.error('リポジトリのコンテンツ一覧の取得に失敗しました:', error);
return aggregatedFiles;
}
}
async function getFileContent(organization: string, repository: string, path: string) {
// GITHUB_TOKEN を環境変数から取得
const token = process.env.GITHUB_TOKEN;
if (!token) {
throw new Error('GITHUB_TOKEN 環境変数が設定されていません。');
}
try {
const response = await axios.get(`https://api.github.com/repos/${organization}/${repository}/contents/${path}`, {
headers: {
'Authorization': `token ${token}`,
'Accept': 'application/vnd.github+json'
}
});
return response.data;
} catch (error) {
console.error('ファイルの内容の取得に失敗しました:', error);
return null;
}
}
async function encodingFileContent(content: any) {
return Buffer.from(content.content, content.encoding).toString('utf-8');
}
async function createLLMsTxt(llm: BaseChatModel, repository_name: string, repository_url: string, file_contents: string): Promise<string> {
const prompt = new PromptTemplate({
template: PROMPT_TEMPLATE,
inputVariables: ['repository_name', 'repository_url', 'file_contents'],
});
const chain = prompt.pipe(llm);
const result = await chain.invoke({
repository_name: repository_name,
repository_url: repository_url,
file_contents: file_contents,
});
const result_str = result.content as string;
const result_match = result_str.match(/<output>\n*([\s\S]*?)\n*<\/output>/);
if (result_match) {
return result_match[1];
} else {
return result_str;
}
}
async function getLatestCommitSha(organization: string, repository: string, branch: string): Promise<string> {
// 環境変数からトークンを取得
const token = process.env.GITHUB_TOKEN;
if (!token) {
throw new Error('GITHUB_TOKEN 環境変数が設定されていません。');
}
try {
const response = await axios.get(`https://api.github.com/repos/${organization}/${repository}/git/ref/heads/${branch}`, {
headers: {
'Authorization': `token ${token}`,
'Accept': 'application/vnd.github+json'
}
});
return response.data.object.sha;
} catch (error: any) {
console.error('An error occurred:', error.response?.data || error.message);
throw error;
}
}
async function createBranch(organization: string, repository: string, newBranch: string, sha: string): Promise<void> {
// 環境変数からトークンを取得
const token = process.env.GITHUB_TOKEN;
if (!token) {
throw new Error('GITHUB_TOKEN 環境変数が設定されていません。');
}
try {
await axios.post(`https://api.github.com/repos/${organization}/${repository}/git/refs`, {
ref: `refs/heads/${newBranch}`,
sha,
}, {
headers: {
'Authorization': `token ${token}`,
'Accept': 'application/vnd.github+json'
}
});
} catch (error: any) {
console.error('An error occurred:', error.response?.data || error.message);
throw error;
}
}
async function createFile(organization: string, repository: string, branch: string, filePath: string, content: string, message: string): Promise<void> {
// 環境変数からトークンを取得
const token = process.env.GITHUB_TOKEN;
if (!token) {
throw new Error('GITHUB_TOKEN 環境変数が設定されていません。');
}
try {
const base64Content = Buffer.from(content).toString('base64');
await axios.put(`https://api.github.com/repos/${organization}/${repository}/contents/${filePath}`, {
message,
content: base64Content,
branch,
}, {
headers: {
'Authorization': `token ${token}`,
'Accept': 'application/vnd.github+json'
}
});
} catch (error: any) {
console.error('An error occurred:', error.response?.data || error.message);
throw error;
}
}
async function createPullRequest(organization: string, repository: string, title: string, body: string, head: string, base: string): Promise<void> {
// 環境変数からトークンを取得
const token = process.env.GITHUB_TOKEN;
if (!token) {
throw new Error('GITHUB_TOKEN 環境変数が設定されていません。');
}
try {
await axios.post(`https://api.github.com/repos/${organization}/${repository}/pulls`, {
title,
body,
head,
base,
}, {
headers: {
'Authorization': `token ${token}`,
'Accept': 'application/vnd.github+json'
}
});
} catch (error: any) {
console.error('An error occurred:', error.response?.data || error.message);
throw error;
}
}
async function main() {
// Organization 名を指定(必要に応じて変更してください)
const organization = process.env.GITHUB_ORGANIZATION;
if (!organization) {
throw new Error('GITHUB_ORGANIZATION 環境変数が設定されていません。');
}
try {
const repos = await listOrganizationRepositories(organization);
// 結果確認
console.log(`Organization "${organization}" のリポジトリ一覧:`);
repos.forEach((repo: any) => {
console.log(`${repo.name}(${repo.full_name}) - ${repo.html_url}`);
});
console.log("--------------------------------");
const llmsTxtsMap = new Map<string, string>();
for (const repo of repos) {
const contents = await listRepositoryContentsRecursive(organization, repo.name);
// 結果確認
contents.forEach((content: any) => {
console.log(`[${content.type}]${content.name}(${content.path}) - ${content.html_url}`);
});
console.log("--------------------------------");
const repository_name = repo.name;
const repository_url = repo.html_url;
let file_contents = "";
for (const file of contents) {
const content = await getFileContent(organization, repo.name, file.path);
const content_str = await encodingFileContent(content);
file_contents += (
`ファイル名: ${file.name}\n`
+ `ファイルパス: ${file.path}\n`
+ `ファイル内容: \n\`\`\`\n${content_str}\n\`\`\`\n`
+ `\n`
);
}
const llm = new ChatOpenAI({
apiKey: process.env.OPENAI_API_KEY,
model: process.env.MODEL_NAME,
});
const llmsTxt = await createLLMsTxt(llm, repository_name, repository_url, file_contents);
console.log(llmsTxt);
console.log("--------------------------------");
llmsTxtsMap.set(repository_name, llmsTxt);
}
console.log(llmsTxtsMap);
// ベースブランチ(main)の最新コミットSHAを取得
const MAIN_REPOSITORY = process.env.GITHUB_MAIN_REPOSITORY;
const now = new Date();
const BASE_BRANCH = "main";
const formattedDate = `${now.getFullYear()}${(now.getMonth() + 1).toString().padStart(2, '0')}${now.getDate().toString().padStart(2, '0')}-${now.getHours().toString().padStart(2, '0')}${now.getMinutes().toString().padStart(2, '0')}${now.getSeconds().toString().padStart(2, '0')}`;
const NEW_BRANCH = `llms-txt-${formattedDate}`;
if (!MAIN_REPOSITORY) {
throw new Error('GITHUB_MAIN_REPOSITORY 環境変数が設定されていません。');
}
const baseSha = await getLatestCommitSha(organization, MAIN_REPOSITORY, BASE_BRANCH);
console.log(`baseSha: ${baseSha}`);
// 新しいブランチを作成
await createBranch(organization, MAIN_REPOSITORY, NEW_BRANCH, baseSha);
console.log(`newBranch: ${NEW_BRANCH}`);
// 新しいフォルダ内にファイルを作成
for (const [repository_name, llmsTxt] of llmsTxtsMap) {
const filePath = `${repository_name}/llms.txt`;
const COMMIT_MESSAGE = `Update llms.txt`;
await createFile(organization, MAIN_REPOSITORY, NEW_BRANCH, filePath, llmsTxt, COMMIT_MESSAGE);
console.log(`filePath: ${filePath}`);
}
// プルリクエストを作成
const PR_TITLE = `Create first llms.txt`;
const PR_BODY = `Create first llms.txt`;
await createPullRequest(organization, MAIN_REPOSITORY, PR_TITLE, PR_BODY, NEW_BRANCH, BASE_BRANCH);
console.log(`PR_TITLE: ${PR_TITLE}`);
console.log('Pull request created successfully.');
} catch (error: any) {
console.error('An error occurred:', error.response?.data || error.message);
}
}
// 実行
main();
Discussion