在你的 Nuxt 專案中整合 Mastra
本指南將帶你使用 Mastra 建立可呼叫 Tool 的 AI Agent,接著直接從伺服器路由匯入並呼叫 Agent,將它連接至 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
這會建立 mastra 資料夾,其中包含範例天氣 Agent 和下列檔案:
index.ts- 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 adapter:
- 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:
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 handler 會接收提示詞,並以 AI SDK 格式串流回傳 Agent 的回應;GET handler 則從記憶中擷取訊息記錄,讓使用者端重新載入時能還原 UI 狀態。
加入聊天 UI「加入聊天 UI」的直接連結
將 app/app.vue 的內容替換為下列程式碼:
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 端點,將提示詞傳送至該端點,並以分段串流方式回傳回應。
它會使用自訂訊息樣式算繪回應文字,並在可收合的 details 元素中顯示所有 Tool 呼叫。
測試你的 Agent「測試你的 Agent」的直接連結
- 使用
npm run dev執行 Nuxt 應用程式 - 在 http://localhost:3000 開啟聊天介面
- 試著詢問天氣。如果 API 金鑰設定正確,你會收到回應
後續步驟「後續步驟」的直接連結
恭喜你使用 Nuxt 建立了 Mastra Agent!🎉
接下來,你可以使用自己的 Tool 和邏輯擴充專案:
準備好後,進一步了解 Mastra 如何與 AI SDK UI 和 Nuxt 整合,以及如何將 Agent 部署至任何地方: