配列を入力として使用する
この例では、ワークフローで配列入力を処理する方法を示します。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 };
ファイルサマリーワークフローの定義
ファイルサマリーワークフローを2つのステップで定義します。1つ目は特定のファイルのコードを取得するステップ、2つ目はそのコードファイルのためのreadmeを生成するステップです。
workflows/file-summary-workflow.ts
import { createWorkflow, createStep } from "@mastra/core/workflows";
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ジェネレーターのワークフローを定義する
4つのステップからなるreadmeジェネレーターのワークフローを定義します。最初のステップはGitHubリポジトリをクローンすること、次にワークフローを一時停止してreadmeを生成する際に考慮するフォルダーをユーザーから入力してもらうこと、次にそのフォルダー内のすべてのファイルの概要を生成すること、そして最後に各ファイルごとに生成されたドキュメントを1つのreadmeにまとめることです。
import { createWorkflow, createStep } from "@mastra/core/workflows";
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: "Please select folders to generate documentation for:",
});
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(
`Generate a README.md file for the following documentation: ${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クラスでAgentとWorkflowインスタンスを登録する
エージェントとワークフローを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,
},
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.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);
}