Mastra→Gemini→Chromaの組み合わせを使ってPDFをRAG化し、専用チャットエージェントを作ります。Notebook.lmのようなものです。
以下、速足で。
・DockerへのChromaのセットアップ
・PDFをGeminiを使ってベクトル化してChromaに登録
・Mastraのチャットに取り込み
DockerへのChromaのセットアップ
version: "3"
services:
chroma:
image: chromadb/chroma:latest
ports:
- "8001:8000"
volumes:
- /share/Container/chroma:/chroma/chroma_data
environment:
- PERSISTENT_DATA=True
- CHROMA_SERVER_CORS_ALLOW_ORIGINS='["*"]'
- CHROMA_SERVER_MODE=default
- CHROMA_SERVER_ENABLE=rest
restart: unless-stopped
これで、ポート8001に立ち上がります。
ブラウザで以下を開いてみてください。SwaggerViewerが開きます。
http://[立ち上げたサーバのドメイン名]jp:8001/docs/
PDFをGeminiを使ってベクトル化してChromaに登録
ベクトル化にはGemini、分割にはMastraを使いました。
PDFからテキストの抽出には、pdf-parseライブラリを使いました。
const GOOGLE_GENERATIVE_AI_API_KEY = process.env.GOOGLE_GENERATIVE_AI_API_KEY;
const CHROMA_ENDPOINT = process.env.CHROMA_ENDPOINT;
const COLLECTION_NAME = "second_collection";
const { GoogleGenAI } = require('@google/genai');
const { ChromaVector } = require('@mastra/chroma');
const { createVectorQueryTool, createDocumentChunkerTool, MDocument } = require('@mastra/rag');
const fs = require('fs').promises;
const pdfParse = require('pdf-parse');
Chromaにコレクションの作成
var chromaVector = new ChromaVector(
{ path: CHROMA_ENDPOINT }
);
var collections = await chromaVector.listIndexes();
if( !collections.includes(COLLECTION_NAME) ){
var result = await chromaVector.createIndex({
indexName: COLLECTION_NAME,
dimension: 3072
});
}
console.log(await chromaVector.listIndexes());
PDFファイルの読み込み、テキストの抽出
const dataBuffer = await fs.readFile('sample.pdf');
var data = await pdfParse(dataBuffer);
console.log(data.text);
テキストの分割とベクトル化と登録
async function getGeminiEmbedding(text) {
const response = await ai.models.embedContent({
model: 'gemini-embedding-001',
contents: text
});
return response.embeddings;
}
const ai = new GoogleGenAI({
apiKey: GOOGLE_GENERATIVE_AI_API_KEY
});
const doc = MDocument.fromText(data.text);
const chunker = createDocumentChunkerTool({
doc: doc,
params: {
strategy: "recursive",
maxSize: 512,
overlap: 50,
separators: ["\n", "\n\n", " "],
},
});
const { chunks } = await chunker.execute();
console.log("Number of chunks:", chunks.length);
for( let chunk of chunks ){
console.log(chunk.text);
const embeddings = await getGeminiEmbedding(chunk.text);
await chromaVector.upsert({
indexName: COLLECTION_NAME,
documents: [chunk.text],
vectors: [embeddings[0].values],
metadata: [ { source: "PDF" } ],
});
}
Mastraのチャットに取り込み
MastraとGeminiと相性が悪いのか、MastraのRAG機能とGeminiを組み合わせた動作はエラーとなってしまいました。
あるべき姿と、回避した姿の両方を示しておきます。
あるべき姿
準備
const { google } = require("@ai-sdk/google");
const { Agent } = require('@mastra/core/agent');
const { Memory } = require('@mastra/memory');
const { LibSQLStore } = require('@mastra/libsql');
const { ChromaVector } = require('@mastra/chroma');
const { createVectorQueryTool } = require('@mastra/rag');
const { RuntimeContext } = require('@mastra/core/runtime-context');
const CHROMA_ENDPOINT = process.env.CHROMA_ENDPOINT;
const COLLECTION_NAME = "second_collection";
const runtimeContext = new RuntimeContext();
let agent;
let memory;
let vectorQueryTool;
const chromaVector = new ChromaVector(
{ path: CHROMA_ENDPOINT }
);
vectorQueryTool = createVectorQueryTool({
vectorStoreName: "chromaVector",
vectorStore: chromaVector,
description: "「M5StackとJavascriptではじめるIoTデバイス制御」の情報で、ESP32をJavascriptで制御するためのライブラリについての説明です。",
indexName: COLLECTION_NAME,
model: google.textEmbeddingModel("gemini-embedding-001"),
});
memory = new Memory({
storage: new LibSQLStore({
url: "file:" + process.env.THIS_BASE_PATH + "/data/mastra/mastra.db"
}),
options: {
threads: {
generateTitle: true
}
}
});
agent = new Agent({
name: 'Chat Agent',
instructions: `自由に会話をします。`,
model: google('gemini-1.5-pro-latest'),
tools: { vectorQueryTool },
memory: memory
});
console.log("Mastra Agent started");
チャットとの組み合わせ
body.messageにRAGに登録したPDFの内容に対して調べたい言葉を入れます。
const response = await agent.generate(
[
{
role: "user", // "system" "assistant" "user"
content: body.message,
},
],
);
console.log(response);
回避版
準備
const { google } = require("@ai-sdk/google");
const { Agent } = require('@mastra/core/agent');
const { Memory } = require('@mastra/memory');
const { LibSQLStore } = require('@mastra/libsql');
const { ChromaVector } = require('@mastra/chroma');
const { createVectorQueryTool } = require('@mastra/rag');
const { RuntimeContext } = require('@mastra/core/runtime-context');
const CHROMA_ENDPOINT = process.env.CHROMA_ENDPOINT;
const COLLECTION_NAME = "second_collection";
const runtimeContext = new RuntimeContext();
let agent;
let memory;
let vectorQueryTool;
const chromaVector = new ChromaVector(
{ path: CHROMA_ENDPOINT }
);
vectorQueryTool = createVectorQueryTool({
vectorStoreName: "chromaVector",
vectorStore: chromaVector,
description: "「M5StackとJavascriptではじめるIoTデバイス制御」の情報で、ESP32をJavascriptで制御するためのライブラリについての説明です。",
indexName: COLLECTION_NAME,
model: google.textEmbeddingModel("gemini-embedding-001"),
});
memory = new Memory({
storage: new LibSQLStore({
url: "file:" + process.env.THIS_BASE_PATH + "/data/mastra/mastra.db"
}),
options: {
threads: {
generateTitle: true
}
}
});
agent = new Agent({
name: 'Chat Agent',
instructions: `自由に会話をします。`,
model: google('gemini-1.5-pro-latest'),
memory: memory
});
console.log("Mastra Agent started");
チャットとの組み合わせ
const queryResult = await vectorQueryTool.execute({
context: { queryText: body.message, topK: 10 },
runtimeContext,
});
const contextText = queryResult.sources
.map((r, i) => `(${i + 1}) ${r.document}`)
.join('\n');
const response = await agent.generate(
[
{
role: "user", // "system" "assistant" "user"
content: body.message + " [参考情報]" + contextText,
},
],
);
console.log(response);
参考
https://mastra.ai/ja/docs
https://ai.google.dev/gemini-api/docs/embeddings?hl=ja
Chromaライブラリで直接登録する場合
https://docs.trychroma.com/docs/overview/introduction
以上