Nuxt プロジェクトに Mastra を統合する
このガイドでは、Mastra を使用して Tool を呼び出す AI Agent を構築し、サーバールートから Agent を直接 import して呼び出すことで Nuxt に接続します。
AI SDK UI を使用して、Vue で美しくインタラクティブなチャット体験を作成します。
始める前に始める前にへの直接リンク
- サポートされているモデル Provider の API キーが必要です。特に希望がなければ、OpenAI を使用してください。
- Node.js
v22.13.0以降をインストールしてください
新しい Nuxt アプリを作成する(任意)新しい Nuxt アプリを作成する(任意)への直接リンク
Nuxt アプリがすでにある場合は、次の手順に進んでください。
次のコマンドを実行して、新しい Nuxt アプリを作成します。
- npm
- pnpm
- Yarn
- Bun
npm create nuxt@latest mastra-nuxt -- --template minimal --packageManager npm --gitInit --modules
pnpm create nuxt mastra-nuxt --template minimal --packageManager npm --gitInit --modules
yarn create nuxt mastra-nuxt --template minimal --packageManager npm --gitInit --modules
bunx create-nuxt mastra-nuxt --template minimal --packageManager npm --gitInit --modules
これにより mastra-nuxt というプロジェクトが作成されますが、任意の名前に置き換えられます。
Mastra を初期化するMastra を初期化するへの直接リンク
Nuxt プロジェクトに移動します。
cd mastra-nuxt
mastra init を実行します。プロンプトが表示されたら、Provider(OpenAI など)を選択してキーを入力します。
- npm
- pnpm
- Yarn
- Bun
npx mastra@latest init
pnpm dlx mastra@latest init
yarn dlx mastra@latest init
bun x mastra@latest init
これにより、天気 Agent のサンプルと次のファイルを含む mastra フォルダーが作成されます。
index.ts- Memory を含む Mastra の設定tools/weather-tool.ts- 指定した場所の天気を取得する Toolagents/weather-agent.ts- Tool を使用するプロンプトを備えた天気 Agent
次の手順では、Nuxt のサーバールートから weather-agent.ts を呼び出します。
AI SDK UI をインストールするAI SDK UI をインストールするへの直接リンク
AI SDK UI と Mastra アダプターをインストールします。
- npm
- pnpm
- Yarn
- Bun
npm install @mastra/ai-sdk@latest @ai-sdk/vue ai
pnpm add @mastra/ai-sdk@latest @ai-sdk/vue ai
yarn add @mastra/ai-sdk@latest @ai-sdk/vue ai
bun add @mastra/ai-sdk@latest @ai-sdk/vue ai
チャットルートを作成するチャットルートを作成するへの直接リンク
server/api/chat.ts を作成します。
import { handleChatStream } from '@mastra/ai-sdk'
import { toAISdkV5Messages } from '@mastra/ai-sdk/ui'
import { createUIMessageStreamResponse } from 'ai'
import { mastra } from '../../src/mastra'
const THREAD_ID = 'example-user-id'
const RESOURCE_ID = 'weather-chat'
export default defineEventHandler(async event => {
const method = event.method
if (method === 'POST') {
const params = await readBody(event)
const stream = await handleChatStream({
mastra,
agentId: 'weather-agent',
params: {
...params,
memory: {
...params.memory,
thread: THREAD_ID,
resource: RESOURCE_ID,
},
},
})
return createUIMessageStreamResponse({ stream })
}
if (method === 'GET') {
const memory = await mastra.getAgentById('weather-agent').getMemory()
let response = null
try {
response = await memory?.recall({
threadId: THREAD_ID,
resourceId: RESOURCE_ID,
})
} catch {
console.log('No previous messages found.')
}
const uiMessages = toAISdkV5Messages(response?.messages || [])
return uiMessages
}
})
POST ハンドラーはプロンプトを受け取り、Agent のレスポンスを AI SDK 形式でストリーミングします。一方、GET ハンドラーは Memory からメッセージ履歴を取得し、クライアントの再読み込み時に UI を復元できるようにします。
チャット UI を追加するチャット UI を追加するへの直接リンク
app/app.vue の内容を次に置き換えます。
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { Chat } from '@ai-sdk/vue'
import { DefaultChatTransport, type ToolUIPart } from 'ai'
const chat = new Chat({
transport: new DefaultChatTransport({
api: '/api/chat',
}),
})
const STATE_TO_LABEL_MAP: Record<string, string> = {
'input-streaming': 'Pending',
'input-available': 'Running',
'output-available': 'Completed',
'output-error': 'Error',
'output-denied': 'Denied',
}
const input = ref('')
onMounted(async () => {
const res = await fetch('/api/chat')
const data = await res.json()
chat.messages = [...data]
})
function handleSubmit() {
if (!input.value.trim()) return
chat.sendMessage({ text: input.value })
input.value = ''
}
</script>
<template>
<div class="chat-container">
<div class="messages">
<div v-for="message in chat.messages" :key="message.id" class="message-wrapper">
<div v-for="(part, i) in message.parts" :key="`${message.id}-${i}`">
<div v-if="part.type === 'text'" :class="['message', message.role]">
<div class="message-content">{{ part.text }}</div>
</div>
<details v-else-if="part.type?.startsWith('tool-')" class="tool">
<summary class="tool-header">
{{ (part as ToolUIPart).type?.split('-').slice(1).join('-') }} - {{
STATE_TO_LABEL_MAP[(part as ToolUIPart).state ?? 'output-available'] }}
</summary>
<div class="tool-content">
<div class="tool-section">
<div class="tool-label">Parameters</div>
<pre><code>{{ JSON.stringify((part as ToolUIPart).input, null, 2) }}</code></pre>
</div>
<div class="tool-section">
<div class="tool-label">
{{ (part as ToolUIPart).errorText ? 'Error' : 'Result' }}
</div>
<pre><code>{{ JSON.stringify((part as ToolUIPart).output, null, 2) }}</code></pre>
<div v-if="(part as ToolUIPart).errorText" class="tool-error">
{{ (part as ToolUIPart).errorText }}
</div>
</div>
</div>
</details>
</div>
</div>
</div>
<form class="input-form" @submit.prevent="handleSubmit">
<input
v-model="input"
type="text"
placeholder="Ask about the weather..."
:disabled="chat.status !== 'ready'"
class="chat-input"
/>
<button type="submit" class="submit-button" :disabled="chat.status !== 'ready'">Send</button>
</form>
</div>
</template>
<style>
*,
*::before,
*::after {
box-sizing: border-box;
}
*:not(dialog) {
margin: 0;
}
@media (prefers-reduced-motion: no-preference) {
html {
interpolate-size: allow-keywords;
}
}
html {
font-family:
-apple-system,
BlinkMacSystemFont,
avenir next,
avenir,
segoe ui,
helvetica neue,
Adwaita Sans,
Cantarell,
Ubuntu,
roboto,
noto,
helvetica,
arial,
sans-serif;
}
body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
}
input,
button,
textarea,
select {
font: inherit;
}
p,
h1,
h2,
h3,
h4,
h5,
h6 {
overflow-wrap: break-word;
}
p {
text-wrap: pretty;
}
h1,
h2,
h3,
h4,
h5,
h6 {
text-wrap: balance;
}
.chat-container {
max-width: 48rem;
margin: 0 auto;
padding: 1.5rem;
height: 100vh;
display: flex;
flex-direction: column;
}
.messages {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 1rem;
}
.message-wrapper {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.message {
padding: 0.75rem 1rem;
border-radius: 0.5rem;
}
.message.user {
background-color: #3b82f6;
color: white;
margin-left: auto;
max-width: 60%;
}
.message.assistant {
background-color: #f3f4f6;
color: #1f2937;
max-width: 80%;
}
.tool {
border: 1px solid #d1d5db;
border-radius: 0.5rem;
margin: 0.5rem 0;
overflow: hidden;
}
.tool-header {
padding: 0.75rem 1rem;
background-color: #f9fafb;
cursor: pointer;
font-weight: 500;
font-size: 0.875rem;
}
.tool-content {
padding: 1rem;
border-top: 1px solid #d1d5db;
}
.tool-section {
margin-bottom: 1rem;
}
.tool-section:last-child {
margin-bottom: 0;
}
.tool-label {
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
color: #6b7280;
margin-bottom: 0.5rem;
}
.tool pre {
background-color: #f3f4f6;
padding: 0.75rem;
border-radius: 0.375rem;
overflow-x: auto;
font-size: 0.875rem;
}
.tool-error {
color: #dc2626;
margin-top: 0.5rem;
}
.input-form {
display: grid;
grid-template-columns: 1fr auto;
gap: 0.75rem;
padding-top: 1rem;
border-top: 1px solid #e5e7eb;
margin-top: 1rem;
}
.chat-input {
padding: 0.75rem 1rem;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
font-size: 1rem;
}
.chat-input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.chat-input:disabled {
background-color: #f3f4f6;
cursor: not-allowed;
}
.submit-button {
padding: 0.75rem 1.5rem;
background-color: #3b82f6;
color: white;
border: none;
border-radius: 0.5rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
}
.submit-button:hover:not(:disabled) {
background-color: #2563eb;
}
.submit-button:disabled {
background-color: #9ca3af;
cursor: not-allowed;
}
</style>
このコンポーネントは Chat() を /api/chat エンドポイントに接続し、そこへプロンプトを送信して、レスポンスをチャンク単位でストリーミングします。
レスポンスのテキストはカスタムのメッセージスタイルでレンダリングされ、Tool の呼び出しは折りたたみ可能な details 要素に表示されます。
Agent をテストするAgent をテストするへの直接リンク
npm run devで Nuxt アプリを実行します- http://localhost:3000 でチャットを開きます
- 天気について質問してみます。API キーが正しく設定されていれば、レスポンスが返されます
次のステップ次のステップへの直接リンク
Nuxt で Mastra Agent を構築できました!🎉
ここから、独自の Tool やロジックを追加してプロジェクトを拡張できます。
準備ができたら、Mastra と AI SDK UI および Nuxt の統合方法や、Agent を任意の場所へデプロイする方法について詳しく学びましょう。