はじめに
はじめまして、いのせです。
本記事では、Mastraの概要とGetting Startedを実際に触った一連の流れを説明します。
Mastraとは
Mastraは、TypeScript(JavaScript)で書かれたオープンソースのAIエージェント開発フレームワークです。大規模言語モデル(LLM)を活用し、チャットボットや自律的にタスクを実行するAIエージェントを簡単に作成・運用できるよう設計されています。
主な特徴
- ワークフロー: 複雑なAIタスクを段階的に処理
- エージェント: 自律的に動作するAIエージェントの構築
- RAG: 検索拡張生成(Retrieval-Augmented Generation)の実装
- 統合機能: 外部サービスとの連携
- 評価機能: AIシステムの性能評価
環境構築
前提条件
- Node.js 20以上
- npm または yarn
- TypeScriptの基本知識
1. プロジェクト作成
# Mastraプロジェクトの作成
npm create mastra@latest
cd my-mastra-app
# 依存関係のインストール
npm install
# 開発サーバー起動
npm run dev
my-mastra-app/
├── src/
│ └── mastra/
│ ├── agents/ # AIエージェント定義
│ │ └── weather-agent.ts
│ ├── tools/ # ツール定義
│ │ └── weather-tool.ts
│ ├── workflows/ # ワークフロー定義
│ │ └── weather-workflow.ts
│ └── index.ts # Mastraのエントリーポイントを定義
├── mastra.config.ts
├── .env # OPENAI_API_KEY を格納
└── package.json
各ファイルの作成
ツール
src/mastra/tools/weather-tool.ts
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
interface WeatherResponse {
current: {
time: string;
temperature_2m: number;
apparent_temperature: number;
relative_humidity_2m: number;
wind_speed_10m: number;
wind_gusts_10m: number;
weather_code: number;
};
}
export const weatherTool = createTool({
id: "get-weather",
description: "Get current weather for a location",
inputSchema: z.object({
location: z.string().describe("City name"),
}),
outputSchema: z.object({
temperature: z.number(),
feelsLike: z.number(),
humidity: z.number(),
windSpeed: z.number(),
windGust: z.number(),
conditions: z.string(),
location: z.string(),
}),
execute: async ({ context }) => {
return await getWeather(context.location);
},
});
const getWeather = async (location: string) => {
const geocodingUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(location)}&count=1`;
const geocodingResponse = await fetch(geocodingUrl);
const geocodingData = await geocodingResponse.json();
if (!geocodingData.results?.[0]) {
throw new Error(`Location '${location}' not found`);
}
const { latitude, longitude, name } = geocodingData.results[0];
const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,wind_gusts_10m,weather_code`;
const response = await fetch(weatherUrl);
const data: WeatherResponse = await response.json();
return {
temperature: data.current.temperature_2m,
feelsLike: data.current.apparent_temperature,
humidity: data.current.relative_humidity_2m,
windSpeed: data.current.wind_speed_10m,
windGust: data.current.wind_gusts_10m,
conditions: getWeatherCondition(data.current.weather_code),
location: name,
};
};
function getWeatherCondition(code: number): string {
const conditions: Record<number, string> = {
0: "Clear sky",
1: "Mainly clear",
2: "Partly cloudy",
3: "Overcast",
45: "Foggy",
48: "Depositing rime fog",
51: "Light drizzle",
53: "Moderate drizzle",
55: "Dense drizzle",
56: "Light freezing drizzle",
57: "Dense freezing drizzle",
61: "Slight rain",
63: "Moderate rain",
65: "Heavy rain",
66: "Light freezing rain",
67: "Heavy freezing rain",
71: "Slight snow fall",
73: "Moderate snow fall",
75: "Heavy snow fall",
77: "Snow grains",
80: "Slight rain showers",
81: "Moderate rain showers",
82: "Violent rain showers",
85: "Slight snow showers",
86: "Heavy snow showers",
95: "Thunderstorm",
96: "Thunderstorm with slight hail",
99: "Thunderstorm with heavy hail",
};
return conditions[code] || "Unknown";
}
エージェント
src/agents/weather-agent.ts
import { openai } from "@ai-sdk/openai";
import { Agent } from "@mastra/core/agent";
import { weatherTool } from "../tools/weather-tool";
export const weatherAgent = new Agent({
name: "Weather Agent",
instructions: `You are a helpful weather assistant that provides accurate weather information.
Your primary function is to help users get weather details for specific locations. When responding:
- Always ask for a location if none is provided
- If the location name isn't in English, please translate it
- Include relevant details like humidity, wind conditions, and precipitation
- Keep responses concise but informative
Use the weatherTool to fetch current weather data.`,
model: openai("gpt-4o-mini"),
tools: { weatherTool },
});
ワークフロー
src/mastra/workflows/weather-workflow.ts
import { openai } from "@ai-sdk/openai";
import { Agent } from "@mastra/core/agent";
import { createStep, createWorkflow } from "@mastra/core/workflows";
import { z } from "zod";
const llm = openai("gpt-4o-mini");
const agent = new Agent({
name: "Weather Agent",
model: llm,
instructions: `
あなたは天気に基づく計画立案に優れた地域活動・旅行の専門家です。天気データを分析し、実用的な活動の推奨事項を提供してください。
予報の各日について、以下の通りに正確に構造化して回答してください:
📅 [曜日、月 日、年]
═══════════════════════════
🌡️ 天気概要
• 状況:[簡潔な説明]
• 気温:[X°C/Y°F から A°C/B°F]
• 降水確率:[X%]
🌅 午前の活動
屋外:
• [活動名] - [具体的な場所/ルートを含む簡潔な説明]
最適な時間:[具体的な時間帯]
注意:[関連する天気の考慮事項]
🌞 午後の活動
屋外:
• [活動名] - [具体的な場所/ルートを含む簡潔な説明]
最適な時間:[具体的な時間帯]
注意:[関連する天気の考慮事項]
🏠 屋内の代替案
• [活動名] - [具体的な会場を含む簡潔な説明]
適している条件:[この代替案を選ぶきっかけとなる天気条件]
⚠️ 特別な考慮事項
• [関連する天気警報、UV指数、風の状況など]
ガイドライン:
- 1日あたり2-3の時間指定屋外活動を提案
- 1-2の屋内バックアップオプションを含める
- 降水確率が50%を超える場合は、屋内活動を優先
- すべての活動は場所に特化したものでなければならない
- 具体的な会場、トレイル、または場所を含める
- 気温に基づいて活動の強度を考慮
- 説明は簡潔だが情報豊富に保つ
一貫性のため、示されている絵文字とセクションヘッダーを使用して、この正確なフォーマットを維持してください。
`,
});
const forecastSchema = z.object({
date: z.string(),
maxTemp: z.number(),
minTemp: z.number(),
precipitationChance: z.number(),
condition: z.string(),
location: z.string(),
});
function getWeatherCondition(code: number): string {
const conditions: Record<number, string> = {
0: "Clear sky",
1: "Mainly clear",
2: "Partly cloudy",
3: "Overcast",
45: "Foggy",
48: "Depositing rime fog",
51: "Light drizzle",
53: "Moderate drizzle",
55: "Dense drizzle",
61: "Slight rain",
63: "Moderate rain",
65: "Heavy rain",
71: "Slight snow fall",
73: "Moderate snow fall",
75: "Heavy snow fall",
95: "Thunderstorm",
};
return conditions[code] || "Unknown";
}
const fetchWeather = createStep({
id: "fetch-weather",
description: "指定された都市の天気予報を取得します",
inputSchema: z.object({
city: z.string().describe("天気を取得する都市"),
}),
outputSchema: forecastSchema,
execute: async ({ inputData }) => {
if (!inputData) {
throw new Error("Input data not found");
}
const geocodingUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(inputData.city)}&count=1`;
const geocodingResponse = await fetch(geocodingUrl);
const geocodingData = (await geocodingResponse.json()) as {
results: { latitude: number; longitude: number; name: string }[];
};
if (!geocodingData.results?.[0]) {
throw new Error(`Location '${inputData.city}' not found`);
}
const { latitude, longitude, name } = geocodingData.results[0];
const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=precipitation,weathercode&timezone=auto,&hourly=precipitation_probability,temperature_2m`;
const response = await fetch(weatherUrl);
const data = (await response.json()) as {
current: {
time: string;
precipitation: number;
weathercode: number;
};
hourly: {
precipitation_probability: number[];
temperature_2m: number[];
};
};
const forecast = {
date: new Date().toISOString(),
maxTemp: Math.max(...data.hourly.temperature_2m),
minTemp: Math.min(...data.hourly.temperature_2m),
condition: getWeatherCondition(data.current.weathercode),
precipitationChance: data.hourly.precipitation_probability.reduce(
(acc, curr) => Math.max(acc, curr),
0,
),
location: inputData.city,
};
return forecast;
},
});
const planActivities = createStep({
id: "plan-activities",
description: "天気条件に基づいてアクティビティを提案します",
inputSchema: forecastSchema,
outputSchema: z.object({
activities: z.string(),
}),
execute: async ({ inputData }) => {
const forecast = inputData;
if (!forecast) {
throw new Error("Forecast data not found");
}
const prompt = `${forecast.location}の以下の天気予報に基づいて、適切なアクティビティを提案してください:
${JSON.stringify(forecast, null, 2)}
`;
const response = await agent.stream([
{
role: "user",
content: prompt,
},
]);
let activitiesText = "";
for await (const chunk of response.textStream) {
process.stdout.write(chunk);
activitiesText += chunk;
}
return {
activities: activitiesText,
};
},
});const weatherWorkflow = createWorkflow({
id: "weather-workflow",
inputSchema: z.object({
city: z.string().describe("天気を取得する都市"),
}),
outputSchema: z.object({
activities: z.string(),
}),
})
.then(fetchWeather)
.then(planActivities);
weatherWorkflow.commit();
export { weatherWorkflow };
エージェントの登録
src/mastra/index.ts
import { Mastra } from "@mastra/core";
import { PinoLogger } from "@mastra/loggers";
import { LibSQLStore } from "@mastra/libsql";
import { weatherWorkflow } from "./workflows/weather-workflow";
import { weatherAgent } from "./agents/weather-agent";
export const mastra = new Mastra({
workflows: { weatherWorkflow },
agents: { weatherAgent },
storage: new LibSQLStore({
// stores telemetry, evals, ... into memory storage, if it needs to persist, change to file:../mastra.db
url: ":memory:",
}),
logger: new PinoLogger({
name: "Mastra",
level: "info",
}),
});
開発サーバーの起動
以下のコマンドを実行してサーバーを起動します。
npm run dev
実行結果
おわりに
今回はMastraのGettingStartedを一通り動かしてみました。
「Mastraの勉強をしよう!」と思っても日本語での記事はほぼなく、なんやかんやで公式ドキュメントを使いました。
Mastraの先駆者となれるように、これからもたくさん触っていこうと思います🔥