配列を入力として
この例では、ワークフローで配列入力を処理する方法を示しています。Mastraは、配列内の各アイテムに対してステップを実行する.foreach()
ヘルパー関数を提供しています。
セットアップ
npm install @ai-sdk/openai @mastra/core simple-git
ドキュメント生成エージェントの定義
コードファイルまたはコードファイルの要約を与えられたときに、LLM呼び出しを活用してドキュメントを生成するドキュメント生成エージェントを定義します。
agents/docs-generator-agent.ts
import { Agent } from "@mastra/core/agent";
import { openai } from "@ai-sdk/openai";
// Create a documentation generator agent for code analysis
const docGeneratorAgent = new Agent({
name: "doc_generator_agent",
instructions: `You are a technical documentation expert. You will analyze the provided code files and generate a comprehensive documentation summary.
For each file:
1. Identify the main purpose and functionality
2. Document key components, classes, functions, and interfaces
3. Note important dependencies and relationships between components
4. Highlight any notable patterns or architectural decisions
5. Include relevant code examples where helpful
Format the documentation in a clear, organized manner using markdown with:
- File overviews
- Component breakdowns
- Code examples
- Cross-references between related components
Focus on making the documentation clear and useful for developers who need to understand and work with this codebase.`,
model: openai("gpt-4o"),
});
export { docGeneratorAgent };
ファイルサマリーワークフローの定義
特定のファイルのコードを取得するステップと、そのコードファイルのREADMEを生成する別のステップという2つのステップでファイルサマリーワークフローを定義します。
workflows/file-summary-workflow.ts
import { createWorkflow, createStep } from "@mastra/core/workflows/vNext";
import { docGeneratorAgent } from "../agents/docs-generator-agent";
import { z } from "zod";
import fs from "fs";
// Step 1: Read the code content from a file
const scrapeCodeStep = createStep({
id: "scrape_code",
description: "Scrape the code from a single file",
inputSchema: z.string(),
outputSchema: z.object({
path: z.string(),
content: z.string(),
}),
execute: async ({ inputData }) => {
const filePath = inputData;
const content = fs.readFileSync(filePath, "utf-8");
return {
path: filePath,
content,
};
},
});
// Step 2: Generate documentation for a single file
const generateDocForFileStep = createStep({
id: "generateDocForFile",
inputSchema: z.object({
path: z.string(),
content: z.string(),
}),
outputSchema: z.object({
path: z.string(),
documentation: z.string(),
}),
execute: async ({ inputData }) => {
const docs = await docGeneratorAgent.generate(
`Generate documentation for the following code: ${inputData.content}`,
);
return {
path: inputData.path,
documentation: docs.text.toString(),
};
},
});
const generateSummaryWorkflow = createWorkflow({
id: "generate-summary",
inputSchema: z.string(),
outputSchema: z.object({
path: z.string(),
documentation: z.string(),
}),
steps: [scrapeCodeStep, generateDocForFileStep],
})
.then(scrapeCodeStep)
.then(generateDocForFileStep)
.commit();
export { generateSummaryWorkflow };
READMEジェネレーターワークフローの定義
READMEジェネレーターワークフローを4つのステップで定義します:1つはGitHubリポジトリをクローンするステップ、1つはワークフローを一時停止してREADME生成時に考慮するフォルダに関するユーザー入力を取得するステップ、1つはフォルダ内のすべてのファイルの要約を生成するステップ、そして最後に各ファイルに対して生成されたすべてのドキュメントを1つのREADMEにまとめるステップです。
import { createWorkflow, createStep } from "@mastra/core/workflows/vNext";
import { docGeneratorAgent } from "../agents/docs-generator-agent";
import { generateSummaryWorkflow } from "./file-summary-workflow";
import { z } from "zod";
import simpleGit from "simple-git";
import fs from "fs";
import path from "path";
// Step 1: Clone a GitHub repository locally
const cloneRepositoryStep = createStep({
id: "clone_repository",
description: "Clone the repository from the given URL",
inputSchema: z.object({
repoUrl: z.string(),
}),
outputSchema: z.object({
success: z.boolean(),
message: z.string(),
data: z.object({
repoUrl: z.string(),
}),
}),
execute: async ({
inputData,
mastra,
getStepResult,
getInitData,
runtimeContext,
}) => {
const git = simpleGit();
// Skip cloning if repo already exists
if (fs.existsSync("./temp")) {
return {
success: true,
message: "Repository already exists",
data: {
repoUrl: inputData.repoUrl,
},
};
}
try {
// Clone the repository to the ./temp directory
await git.clone(inputData.repoUrl, "./temp");
return {
success: true,
message: "Repository cloned successfully",
data: {
repoUrl: inputData.repoUrl,
},
};
} catch (error) {
throw new Error(`Failed to clone repository: ${error}`);
}
},
});
// Step 2: Get user input on which folders to analyze
const selectFolderStep = createStep({
id: "select_folder",
description: "Select the folder(s) to generate the docs",
inputSchema: z.object({
success: z.boolean(),
message: z.string(),
data: z.object({
repoUrl: z.string(),
}),
}),
outputSchema: z.array(z.string()),
suspendSchema: z.object({
folders: z.array(z.string()),
message: z.string(),
}),
resumeSchema: z.object({
selection: z.array(z.string()),
}),
execute: async ({ resumeData, suspend }) => {
const tempPath = "./temp";
const folders = fs
.readdirSync(tempPath)
.filter((item) => fs.statSync(path.join(tempPath, item)).isDirectory());
if (!resumeData?.selection) {
await suspend({
folders,
message: "ドキュメントを生成するフォルダを選択してください:",
});
return [];
}
// Gather all file paths from selected folders
const filePaths: string[] = [];
// Helper function to recursively read files from directories
const readFilesRecursively = (dir: string) => {
const items = fs.readdirSync(dir);
for (const item of items) {
const fullPath = path.join(dir, item);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
readFilesRecursively(fullPath);
} else if (stat.isFile()) {
filePaths.push(fullPath.replace(tempPath + "/", ""));
}
}
};
for (const folder of resumeData.selection) {
readFilesRecursively(path.join(tempPath, folder));
}
return filePaths;
},
});
// Step 4: Combine all documentation into a single README
const collateDocumentationStep = createStep({
id: "collate_documentation",
inputSchema: z.array(
z.object({
path: z.string(),
documentation: z.string(),
}),
),
outputSchema: z.string(),
execute: async ({ inputData }) => {
const readme = await docGeneratorAgent.generate(
`以下のドキュメントに基づいてREADME.mdファイルを生成してください: ${inputData.map((doc) => doc.documentation).join("\n")}`,
);
return readme.text.toString();
},
});
const readmeGeneratorWorkflow = createWorkflow({
id: "readme-generator",
inputSchema: z.object({
repoUrl: z.string(),
}),
outputSchema: z.object({
success: z.boolean(),
message: z.string(),
data: z.object({
repoUrl: z.string(),
}),
}),
steps: [
cloneRepositoryStep,
selectFolderStep,
generateSummaryWorkflow,
collateDocumentationStep,
],
})
.then(cloneRepositoryStep)
.then(selectFolderStep)
.foreach(generateSummaryWorkflow)
.then(collateDocumentationStep)
.commit();
export { readmeGeneratorWorkflow };
Mastraクラスでエージェントとワークフローのインスタンスを登録する
エージェントとワークフローをmastraインスタンスに登録します。これはワークフロー内でエージェントへのアクセスを可能にするために重要です。
index.ts
import { Mastra } from "@mastra/core";
import { PinoLogger } from "@mastra/loggers";
import { docGeneratorAgent } from "./agents/docs-generator-agent";
import { readmeGeneratorWorkflow } from "./workflows/readme-generator-workflow";
import { generateSummaryWorkflow } from "./workflows/file-summary-workflow";
// Create a new Mastra instance and register components
const mastra = new Mastra({
agents: {
docGeneratorAgent,
},
vnext_workflows: {
readmeGeneratorWorkflow,
generateSummaryWorkflow,
},
logger: new PinoLogger({
name: "Mastra",
level: "info",
}),
});
export { mastra };
Readme Generator ワークフローを実行する
ここでは、mastraインスタンスからreadme generatorワークフローを取得し、実行を作成して、必要なinputDataで作成した実行を実行します。
exec.ts
import { promptUserForFolders } from "./utils";
import { mastra } from "./";
// GitHub repository to generate documentation for
const ghRepoUrl = "https://github.com/mastra-ai/mastra";
const run = mastra.vnext_getWorkflow("readmeGeneratorWorkflow").createRun();
// Start the workflow with the repository URL as input
const res = await run.start({ inputData: { repoUrl: ghRepoUrl } });
const { status, steps } = res;
// Handle suspended workflow (waiting for user input)
if (status === "suspended") {
// Get the suspended step data
const suspendedStep = steps["select_folder"];
let folderList: string[] = [];
// Extract the folder list from step data
if (
suspendedStep.status === "suspended" &&
"folders" in suspendedStep.payload
) {
folderList = suspendedStep.payload.folders as string[];
} else if (suspendedStep.status === "success" && suspendedStep.output) {
folderList = suspendedStep.output;
}
if (!folderList.length) {
console.log("No folders available for selection.");
process.exit(1);
}
// Prompt user to select folders
const folders = await promptUserForFolders(folderList);
// Resume the workflow with user selections
const resumedResult = await run.resume({
resumeData: { selection: folders },
step: "select_folder",
});
// Print resumed result
if (resumedResult.status === "success") {
console.log(resumedResult.result);
} else {
console.log(resumedResult);
}
process.exit(1);
}
// Handle completed workflow
if (res.status === "success") {
console.log(res.result ?? res);
} else {
console.log(res);
}