mastra やってみる

Xのタイムラインに mastra というTypeScriptのAIエージェント開発フレームワークが流れてきた。
今回はこれを試したい。

参考

導入 - プレイグラウンド

1
> npx npm:create-mastra@latest

いろいろ選択させられるが、推奨設定で選択する。
.env.development にAPIキーを書くことを求められるので、OpenAIのAPIキーを書き起動する。

1
> npm run dev

でプレイグラウンドアプリが立ち上がる。

明日の東京の天気を聞いたら、現在の天気だけ答えるらしい。

1
現在の天気情報のみを提供できます。東京の現在の天気をお調べしますね。東京の現在の天気は、晴れです。気温は5.7°Cで、体感温度は3.8°Cです。湿度は88%で、風速は3.1 m/s、最大風速は5.4 m/sです。

天気予報を調べても合っているのかは釈然としないが立ち上がったので一旦良いことにする。
公式ドキュメントの記載を見ると、以下のようにリクエストを投げるように記述されているようだ。

1
https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,wind_gusts_10m,weather_code

ひとまずこういったエージェントを作るためのものであるらしい。

公式サイトの導入では以下のような記述がある。

It’s designed to give you the primitives you need to build AI applications and features.

「プリミティブを提供する」というのはピンと来ないが、原始的なパーツの提供ということであろうか。

ソースを見ると、動作としては次のように動作をするように見える。

sequenceDiagram
participant u as User
participant ag as Agent
participant api as API
    u ->> ag: 質問
    ag ->> ag: 質問の解釈を行い回答できるものか判断
    ag --> api: APIへリクエスト
    api --> ag: APIからのレスポンス
    ag --> u: 文章生成して回答

質問の解釈を行い回答できるものか判断 の部分については実験すると見えてくる。
人気映画を聞くと断るからである。

1
2
3
質問:最近の人気映画は何ですか?

回答:申し訳ありませんが、現在の人気映画についての情報は提供できません。しかし、映画レビューサイトや映画館のウェブサイトで最新の人気映画を確認することができます。何か他にお手伝いできることがあれば教えてください。

これについては、Agent側の実装が次のように書かれており、明確に役割を規定しているためと考えられる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { openai } from '@ai-sdk/openai';
import { Agent } from '@mastra/core/agent';
import { weatherTool } from '../tools';

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
- If giving a location with multiple parts (e.g. "New York, NY"), use the most relevant part (e.g. "New York")
- 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'),
tools: { weatherTool },
});

ここを見ると、「場所が指定されない場合は質問せよ」という指示が記述されているので、試すと確かに聞き返す。
また、存在しない地名を問うたら、エデンの天気は答えてくれたが黄泉平坂の天気は答えてくれなかった。
ちょっと面白い。

自前で作ってみる

続けて、自前で実装を試みる。

1
2
3
4
5
6
7
8
9
> npm install npm:mastra @ai-sdk/openai
npm warn deprecated @aws-sdk/signature-v4@3.374.0: This package has moved to @smithy/signature-v4
npm warn deprecated @aws-sdk/protocol-http@3.374.0: This package has moved to @smithy/protocol-http

added 395 packages in 59s

39 packages are looking for funding
run `npm fund` for details

試しに、bash script を提案するスクリプトを作成してみる。
その上で現在のカレントディレクトリを見るためのAPIを提供してみる。

ディレクトリ構成ついては、先のプレイグラウンドを参考に組む。

命令のプロンプトは、先のサンプルをchatgptに食わせて、bash scriptを提案するように指示した。

agents\bashHelperAgent.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { openai } from "@ai-sdk/openai";
import { Agent } from "@mastra/core/agent";
import { readDirTool } from "../tools/readdir.ts";

export const bashHelperAgent = new Agent({
name: "Bash Helper Agent",
instructions: `
You are a Bash command assistant that provides accurate and efficient command-line instructions.

Your primary function is to help users by suggesting appropriate Bash commands. When responding:

Always keep responses concise and relevant.
If a user requests directory-related information, use readDirTool to fetch the current directory contents.
Ensure commands are safe and avoid destructive operations unless explicitly requested.
Provide brief explanations for complex commands.
If multiple solutions exist, suggest the most efficient and commonly used one.
And if you are asked a question that is not about computers, ask back!
`,
model: openai("gpt-4o-mini"),
tools: { readDirTool },
});

ツール部分は、カレントディレクトリを取得するものを作成する。

tools\readdir.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
const fs = require('fs');

export const readDirTool = createTool({
id: "read-dir",
description: "Get current dir",
outputSchema: z.object({
dirs: z.string(),
}),
execute: async ({ context }) => {
return await getDir();
},
});

const getDir = () => {
return { dirs: fs.readdirSync("./") };
};

ここは公式のサンプルを参考に。

main.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { bashHelperAgent } from "./agents/bashHelperAgent.ts";

async function main() {
const order =
"現在のディレクトリにあるファイルを表示し、全体の構成をマークダウン形式で解説してください。また調査に使うシェルスクリプトを示してください。";
console.log(`Order: ${order}`);

const response = await bashHelperAgent.generate([{
role: "user",
content: order,
}]);
console.log(`Anser: ${response.text}`);
}

main();

動作させると次のようになる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
> npx bun ./main.ts
Order: 現在のディレクトリにあるファイルを表示し、全体の構成をマークダウン形式で解説してください。また調査に使うシェルス クリプトを示してください。
Anser: 現在のディレクトリには以下のファイルとフォルダがあります:

### ディレクトリ構成

- **.env.development**: 環境変数を定義するファイル。
- **.vscode**: Visual Studio Codeの設定ファイルが格納されるディレクトリ。
- **agents**: エージェント関連のコードやリソースが含まれるディレクトリ。
- **main.ts**: アプリケーションのエントリーポイントとなるTypeScriptファイル。
- **node_modules**: プロジェクトの依存関係がインストールされるディレクトリ。
- **package-lock.json**: 依存関係のバージョンを固定するためのファイル。
- **package.json**: プロジェクトのメタデータや依存関係を定義するファイル。
- **src**: ソースコードが格納されるディレクトリ。
- **tools**: ツールやスクリプトが含まれるディレクトリ。

### 調査に使うシェルスクリプト

以下のシェルスクリプトを使用して、現在のディレクトリの内容を表示できます。

```bash
#!/bin/bash

# 現在のディレクトリの内容を表示
echo "現在のディレクトリの内容:"
ls -al
```

このスクリプトを実行すると、現在のディレクトリにあるすべてのファイルとフォルダが詳細に表示されます。

質問に正しく答えているように見える。
ディレクトリの構成については嘘をついている可能性を鑑みて、ファイルを増やす減らしてみる。
これも出力に反映されており、嘘をついている様子もない。

ためしに、流行りの料理を聞いてみる。

1
2
3
> npx bun ./main.ts
Order: 流行りの料理は?
Anser: 料理に関する情報は提供できませんが、最近の流行やトレンドについて知りたいことがあれば教えてください!

拒否された。
これは、先のプロンプトで設定した以下の一文が効いていと考えられる。

1
And if you are asked a question that is not about computers, ask back!

この一文を抜くと以下のように回答した。

1
2
3
4
5
6
7
8
9
10
> npx bun ./main.ts
Order: 流行りの料理は?
Anser: 最近の流行りの料理には、以下のようなものがあります:

1. **ビーガン料理** - 植物ベースの食材を使った料理が人気です。
2. **発酵食品** - キムチや納豆など、健康志向の高まりから注目されています。
3. **スーパーフード** - キヌアやアサイーなど、栄養価の高い食材を使った料理。
4. **エスニック料理** - タイ料理やメキシコ料理など、異国の味が人気です。

これらの料理は、健康や新しい味の探求に応じて多くの人に支持されています。

と回答した。
が、禁止しているプロンプトでも繰り返すと微妙に答えてくれたりするケースもある。
このあたりはプロンプト力が問われるものであろう。

RAG 導入

次に、RAGを導入してみる。
サンプルでは特定のドキュメントをurl指定している。
ここではインターネット上には無い妙な知識を教えて、これに基づいて答えさせてみる。

とりあえず、必要なモジュールを取得。

1
> npm install @mastra/rag ai

エージェントには、ベクタ検索するツールを与える。
プロンプトはサンプルのレシピを答えさせるものを書き換えて司書エージェントとした。

agents/librarianAgent.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { openai } from "@ai-sdk/openai";
import { Agent } from "@mastra/core/agent";
import { createVectorQueryTool } from "@mastra/rag";

const vectorQueryTool = createVectorQueryTool({
vectorStoreName: "libSqlVectorStore",
indexName: "books",
model: openai.embedding("text-embedding-3-small"),
});

export const librarianAgent = new Agent({
name: "LibrarianAgent",
instructions: `
You are a helpful book recommendation assistant that suggests books based on the user's mood and preferences.

Use the provided vector query tool to find relevant information from your knowledge base,
and provide accurate, well-supported answers based on the retrieved content.
Focus on the specific content available in the tool and acknowledge if you cannot find sufficient information to answer a question.
Base your responses only on the content provided, not on general knowledge.`,
model: openai("gpt-4o-mini"),
tools: { vectorQueryTool },
});

メインの処理。
サンプルのpostgres の用意は面倒そうだったのでドキュメントにあるlibsql(sqlite) で代替。
cahtgpt に架空の本のタイトル著者、雰囲気を提案させたデータを喰わせて、データベースとし、それに基づいて本を提案させる。

useRag.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import { librarianAgent } from "./agents/librarianAgent.ts";
import { MDocument } from "@mastra/rag";
import { Mastra } from "@mastra/core";
import { LibSQLVector } from "@mastra/core/vector/libsql";
import { openai } from "@ai-sdk/openai";
import { embedMany } from "ai";

const libSqlVectorStore = new LibSQLVector(
{
connectionUrl: "file:books.db",
},
);

export const mastra = new Mastra({
agents: { librarianAgent },
vectors: { libSqlVectorStore },
});

const library = `
| No. | タイトル | 著者 | 雰囲気 |
|----|----------|------|--------|
| 1 | 影の王国 | 山本 悠 | ダークファンタジー、荘厳 |
| 2 | 星降る図書館 | エミリー・ノートン | 優雅、幻想的 |
| 3 | 霧の迷宮 | フェリックス・ヴァンス | ミステリアス、知的 |
| 4 | 夢の航海者たち | 佐々木 仁 | 冒険、ノスタルジック |
| 5 | 静寂の都市 | ルイーズ・カーター | サスペンス、静謐 |
| 6 | 赤い月の告白 | 中嶋 圭 | ミステリー、切ない |
| 7 | 永遠に響くメロディ | リサ・ベルモント | ロマンス、感動的 |
| 8 | 死神の図書室 | アレックス・マグナス | ダーク、知的 |
| 9 | 雪と薔薇の誓い | 黒崎 玲 | 切ない、耽美 |
| 10 | 100年後の郵便局 | ベネディクト・ハワード | 未来的、叙情的 |
| 11 | 月夜に囁くもの | 小林 透 | サスペンス、幻想的 |
| 12 | 彼方の星を夢見て | ハンナ・エヴァンス | 叙情的、希望 |
| 13 | 魔法使いと時計塔 | エドワード・チェン | 不思議、優雅 |
| 14 | 青の孤独 | 遠藤 涼 | 哀愁、青春 |
| 15 | ノクターンの亡霊 | リチャード・スコット | ダーク、ミステリアス |
| 16 | 砂漠に眠る秘密 | 山崎 彩 | ロマンティック、冒険 |
| 17 | 時の狭間に消えた町 | セシリア・モントレー | 不思議、切ない |
| 18 | 名前のない探偵 | 西村 薫 | ハードボイルド、知的 |
| 19 | 春の終わりに | エレノア・グレイ | 儚い、ノスタルジック |
| 20 | 時計仕掛けの夜 | アンドレイ・フィッシャー | スチームパンク、ミステリアス |
| 21 | 翠色の森へ | 東堂 美咲 | 幻想的、優しい |
| 22 | 遠雷の記憶 | ロバート・D・ハリス | ミステリー、静か |
| 23 | 冬の魔法と少年 | ジェシカ・ロウ | 温かい、幻想的 |
| 24 | 彼女の消えた夏 | 森田 亮 | サスペンス、切ない |
| 25 | 銀の鍵と夜の扉 | ミシェル・カーヴァー | ファンタジー、神秘的 |
| 26 | 忘れられた楽譜 | 本田 雅人 | 音楽、叙情的 |
| 27 | 幻影の王 | フィリップ・バーナード | ミステリアス、壮大 |
| 28 | 鏡の中の少女 | 山田 佳奈 | ホラー、幻想的 |
| 29 | 風が運ぶ物語 | アンナ・ホワイト | 優雅、心温まる |
| 30 | 夜明けの約束 | 高橋 誠 | 感動的、希望 |
| 31 | 運命の交差点 | ルイーズ・マクシー | サスペンス、ロマンス |
| 32 | さよなら、星の王国 | 桜井 真 | 切ない、幻想的 |
| 33 | 消えた王子と七つの鍵 | ハリー・ノース | 冒険、ダークファンタジー |
| 34 | 青い蝶の秘密 | 白石 涼 | ミステリー、詩的 |
| 35 | 湖畔の魔女 | マルティナ・オルセン | 不思議、優雅 |
| 36 | 闇の狩人 | 織田 隆 | ダーク、サスペンス |
| 37 | 秘密の花園と亡霊たち | ジャスティン・フィンレイ | 幻想的、怖い |
| 38 | 時の迷子 | 加藤 昴 | SF、切ない |
| 39 | 雨の中の手紙 | ソフィア・リード | ロマンス、哀愁 |
| 40 | 遥かなる夜の果て | 大塚 渉 | 哲学的、静寂 |
| 41 | 迷宮の書庫 | ヘンリー・ウィルソン | 知的、ミステリアス |
| 42 | 金色の雲の下で | 伊藤 奈央 | 叙情的、希望 |
| 43 | 星降る夜の誓い | キャサリン・スミス | ロマンス、切ない |
| 44 | 砂時計の国 | リカルド・フローレス | SF、幻想的 |
| 45 | 黒猫の秘密 | 真島 亜美 | ミステリー、可愛い |
| 46 | 黄昏の楽園 | レベッカ・ハンター | 哀愁、ノスタルジック |
| 47 | 夜空のパズル | 斉藤 悠 | 叙情的、静かな感動 |
| 48 | 見えざる手紙 | ヴィクター・ホール | サスペンス、哲学的 |
| 49 | 暗黒の楽園 | ケヴィン・ロバーツ | ホラー、神秘的 |
| 50 | 君が消えた日 | 長谷川 亮 | 感動的、切ない |
`;

const doc = MDocument.fromText(library);
const chunks = await doc.chunk({
strategy: "recursive",
size: 30,
overlap: 10,
separator: "\n",
});

const { embeddings } = await embedMany({
model: openai.embedding("text-embedding-3-small"),
values: chunks.map((chunk) => chunk.text),
});

const vectorStore = mastra.getVector("libSqlVectorStore");

async function main() {
await vectorStore.createIndex({
indexName: "books",
dimension: 1536,
});

await vectorStore.upsert({
indexName: "books",
vectors: embeddings,
metadata: chunks.map((chunk) => ({
text: chunk.text,
source: "transformer-books",
})),
});

const agent = mastra.getAgent("librarianAgent");

const order =
"今寂しい気分です。オススメの本をタイトルと著者を説明し提案して下さい。";
console.log(`Order: ${order}`);

const response = await agent.generate([{
role: "user",
content: order,
}]);
console.log(`Anser: ${response.text}`);
}

main();

実行してみると、次のようになる。

1
2
3
4
5
6
7
8
9
10
11
> npx bun ./useRag.ts
Order: 今寂しい気分です。オススメの本をタイトルと著者を説明し提案して下さい。
Anser: 寂しい気分におすすめの本をいくつかご紹介します。

1. **青の孤独** - **遠藤 涼**
- この作品は、哀愁と青春をテーマにした物語です。主人公が孤独を感じながらも、自分自身を見つめ直し成長していく様子が描 かれています。感情の深さに共感できる一冊です。

2. **斉藤 悠**の作品
- 斉藤悠の本は、叙情的で静かな感動を与えてくれる作品が多いです。彼の作品は、心の内面を丁寧に描写しており、寂しさを感 じる時に寄り添ってくれる内容が特徴です。

これらの本は、あなたの気持ちに寄り添い、心を癒してくれるかもしれません。ぜひ手に取ってみてください。

出力に、与えたタイトルと著者が使用されていて一見良さそうだが、それ以上に内容などを解説しており、作っている部分は一定程度見受けられる。
これもまたチューニングが必要であろう。

ちなみに、天気を聞いたらちゃんと回答を断られた。

自身のRAGの知識は浅いものだが、与えたデータを参照しそれを元にした回答をさせるということができた。


というわけで、mastraを試してみた。
1つ目はツールでローカルの環境を取得する機能を作成し、それを利用して質問に答えるエージェントを作成した。
これ、最近タイムラインを賑わしているMCPとは何が違うのかなと感じたので調べてみる。
mastraは、MCP 機能もサポートしており、ドキュメントもある。
以下の様に記述がある。

Model Context Protocol (MCP) is a standardized way for AI models to discover and interact with external tools and resources.

とあり、標準化された方法の提供にフォーカスしており、ある種コンパチブルな形で外部ツールが機能へアクセスを提供するものであるらしい。
(だからjson-schemaとか言ってたのかとここで理解する。今回の場合、これ専用に作り込んでおり、標準化もされていないからこれはMCPとは言わないはず。)

mastraを触ったらMCPの理解が少し深まった。

2つ目はRAGだが、これはパラメータや渡すデータ自体を見直すことが十分に必要そうである。

このブログ、だいたい何でもDeno でやるブログである。
mastra もDeno でやろうとしたらかなり引っかかってしまい、Node.js でやることにした。

公式にある実行方法を参考にすると npx bun ~ で実行とあり、ヌルっとbun 初体験であった。

メモリやワークフローも提供されるのでこの辺りも順次試したい。

では。