Aller au contenu principal

Intégrer Mastra à votre projet Nuxt

Dans ce guide, vous allez créer avec Mastra un Agent d’IA capable d’appeler des Tools, puis le connecter à Nuxt en l’important et en l’appelant directement depuis vos routes serveur.

Vous utiliserez AI SDK UI pour créer avec Vue une expérience de chat élégante et interactive.

Avant de commencer
Lien direct vers Avant de commencer

  • Vous aurez besoin d’une clé d’API fournie par un Provider de modèle pris en charge. Si vous n’avez pas de préférence, utilisez OpenAI.
  • Installez Node.js v22.13.0 ou une version ultérieure

Créer une nouvelle application Nuxt (facultatif)
Lien direct vers Créer une nouvelle application Nuxt (facultatif)

Si vous disposez déjà d’une application Nuxt, passez à l’étape suivante.

Exécutez la commande suivante pour créer une nouvelle application Nuxt :

npm create nuxt@latest mastra-nuxt -- --template minimal --packageManager npm --gitInit --modules

Cette commande crée un projet nommé mastra-nuxt, mais vous pouvez choisir un autre nom.

Initialiser Mastra
Lien direct vers Initialiser Mastra

Accédez à votre projet Nuxt :

cd mastra-nuxt

Exécutez mastra init. Lorsque vous y êtes invité, choisissez un Provider, par exemple OpenAI, puis saisissez votre clé :

npx mastra@latest init

Cette commande crée un dossier mastra contenant un exemple d’Agent météo et les fichiers suivants :

  • index.ts : configuration de Mastra, notamment la Memory
  • tools/weather-tool.ts : Tool permettant de récupérer la météo d’un lieu donné
  • agents/weather-agent.ts : Agent météo dont le prompt utilise le Tool

Vous appellerez weather-agent.ts depuis vos routes serveur Nuxt au cours des étapes suivantes.

Installer AI SDK UI
Lien direct vers Installer AI SDK UI

Installez AI SDK UI avec l’adaptateur Mastra :

npm install @mastra/ai-sdk@latest @ai-sdk/vue ai

Créer une route de chat
Lien direct vers Créer une route de chat

Créez 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
}
})

Le gestionnaire POST accepte un prompt et renvoie la réponse de l’Agent sous forme de flux au format AI SDK, tandis que le gestionnaire GET récupère l’historique des messages dans la Memory afin de réhydrater l’interface utilisateur lorsque le client recharge la page.

Ajouter l’interface de chat
Lien direct vers Ajouter l’interface de chat

Remplacez le contenu de app/app.vue par le code suivant :

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>

Ce composant connecte Chat() à l’endpoint /api/chat : il y envoie les prompts et reçoit la réponse sous forme de flux par fragments.

Il affiche le texte de la réponse avec un style de message personnalisé et présente les appels de Tools dans un élément de détails repliable.

Tester votre Agent
Lien direct vers Tester votre Agent

  1. Lancez votre application Nuxt avec npm run dev
  2. Ouvrez le chat à l’adresse http://localhost:3000
  3. Posez une question sur la météo. Si votre clé d’API est correctement configurée, vous recevrez une réponse

Étapes suivantes
Lien direct vers Étapes suivantes

Félicitations, vous avez créé votre Agent Mastra avec Nuxt ! 🎉

Vous pouvez maintenant enrichir le projet avec vos propres Tools et votre propre logique :

  • En savoir plus sur les Agents
  • Ajouter des Tools à votre Agent
  • Ajouter une Memory similaire à la mémoire humaine à votre Agent

Lorsque vous serez prêt, découvrez comment Mastra s’intègre à AI SDK UI et à Nuxt, ainsi que la manière de déployer votre Agent où vous le souhaitez :