Aller au contenu principal

Intégrer Mastra à votre projet Electron

Dans ce guide, vous allez créer avec Mastra un Agent d’IA capable d’appeler des Tools, puis le connecter à une application de bureau Electron en l’appelant directement depuis le serveur Mastra.

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

Avant de commencer
Lien direct vers Avant de commencer

  • Vous aurez besoin d’une clé API provenant d’un Provider de modèles 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 application Electron (facultatif)
Lien direct vers Créer une application Electron (facultatif)

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

Initialisez une nouvelle application Electron avec electron-vite :

npm create @quick-start/electron@latest electron-chat -- --template react-ts --skip

Cette commande crée une application Electron nommée electron-chat avec React et TypeScript. Accédez au répertoire du projet :

cd electron-chat

Modifier les paramètres CSP
Lien direct vers Modifier les paramètres CSP

Pour que l’application Electron puisse appeler le serveur Mastra, vous devez adapter les paramètres de la politique de sécurité du contenu (CSP).

Ouvrez src/renderer/index.html et mettez à jour la balise <meta http-equiv="Content-Security-Policy"> afin d’inclure http://localhost:4111 dans la directive connect-src :

src/renderer/index.html
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; connect-src http://localhost:4111; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
/>
info

Pour les déploiements en production, adaptez cette valeur à l’URL réelle de votre serveur.

Modifier main.css
Lien direct vers edit-maincss

Ouvrez src/renderer/src/assets/main.css et remplacez son contenu par :

src/renderer/src/assets/main.css
@import './base.css';

body {
background: var(--ev-c-white);
color: var(--ev-c-black);
}

Initialiser Mastra
Lien direct vers Initialiser Mastra

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

npx mastra@latest init

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

  • index.ts — configuration de Mastra, y compris la mémoire
  • tools/weather-tool.ts — Tool qui récupère la météo d’un lieu donné
  • agents/weather-agent.ts — Agent météo doté d’une instruction qui utilise le Tool

Dans les étapes suivantes, vous appellerez weather-agent.ts depuis votre interface de chat.

Installer AI SDK UI et AI Elements
Lien direct vers Installer AI SDK UI et AI Elements

Installez AI SDK UI ainsi que l’adaptateur Mastra :

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

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

Ouvrez src/mastra/index.ts et ajoutez une chatRoute() à votre configuration. Elle crée une route d’API que votre frontend Electron peut appeler pour recevoir des réponses de chat compatibles avec AI SDK ; vous l’utiliserez ensuite avec useChat().

src/mastra/index.ts
import { Mastra } from '@mastra/core/mastra'
// Existing imports...
import { chatRoute } from '@mastra/ai-sdk'

export const mastra = new Mastra({
// Existing config...
server: {
cors: {
origin: '*', // Restrict this to your app's origin in production
allowMethods: ['*'],
allowHeaders: ['*'],
},
apiRoutes: [
chatRoute({
path: '/chat/:agentId',
}),
],
},
})
info

CORS est nécessaire, car le moteur de rendu Electron se charge depuis une origine différente de celle du serveur Mastra. Pour les déploiements en production, limitez l’origine à celle de votre application.

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

Ouvrez src/renderer/src/App.tsx et remplacez son contenu par le composant de chat :

src/renderer/src/App.tsx
import { useState } from 'react'
import { useChat } from '@ai-sdk/react'
import { DefaultChatTransport, type ToolUIPart } from 'ai'

const STATE_TO_LABEL_MAP: Record<string, string> = {
'input-streaming': 'Streaming input...',
'input-available': 'Input ready',
'approval-requested': 'Approval requested',
'approval-responded': 'Approval responded',
'output-available': 'Complete',
'output-error': 'Error',
}

export default function App(): React.JSX.Element {
const [input, setInput] = useState('')

const { messages, sendMessage } = useChat({
transport: new DefaultChatTransport({
api: `http://localhost:4111/chat/weather-agent`,
}),
})

const handleSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
e.preventDefault()
if (!input.trim()) return
sendMessage({ text: input })
setInput('')
}

return (
<main
style={{
maxWidth: '48rem',
marginLeft: 'auto',
marginRight: 'auto',
padding: '1.5rem',
width: '100%',
height: '100vh',
}}
>
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{ flex: '1 1 0%', minHeight: 0, overflowY: 'auto' }} data-name="conversation">
<div
data-name="conversation-content"
style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}
>
{messages.map((message, messageIndex) => (
<div key={messageIndex}>
{message.parts.map((part, partIndex) => {
if (part.type === 'text') {
return (
<div
key={partIndex}
data-name="message"
style={{
display: 'flex',
width: '100%',
maxWidth: '95%',
flexDirection: 'column',
gap: '0.5rem',
...(message.role === 'user'
? { marginLeft: 'auto', justifyContent: 'flex-end' }
: {}),
}}
>
<div
data-name="message-content"
style={{
display: 'flex',
width: 'fit-content',
maxWidth: '100%',
minWidth: 0,
flexDirection: 'column',
gap: '0.5rem',
overflow: 'hidden',
fontSize: '0.875rem',
...(message.role === 'user'
? {
marginLeft: 'auto',
borderRadius: '0.5rem',
backgroundColor: '#dbeafe',
paddingLeft: '1rem',
paddingRight: '1rem',
paddingTop: '0.75rem',
paddingBottom: '0.75rem',
}
: {}),
}}
>
<div
data-name="message-response"
style={{ width: '100%', height: '100%' }}
>
{part.text}
</div>
</div>
</div>
)
}
if (part.type.startsWith('tool-')) {
const toolPart = part as unknown as ToolUIPart
return (
<div
key={partIndex}
data-name="tool"
style={{
marginBottom: '1.5rem',
width: '100%',
borderRadius: '0.5rem',
border: '1px solid #d1d5db',
boxShadow: '0 1px 3px 0 rgba(0,0,0,0.1)',
}}
>
<details
data-name="tool-header"
style={{ width: '100%', padding: '0.75rem', cursor: 'pointer' }}
>
<summary style={{ fontWeight: 500, fontSize: '0.875rem' }}>
{toolPart.type.split('-').slice(1).join('-')} -{' '}
{STATE_TO_LABEL_MAP[toolPart.state ?? 'output-available']}
</summary>
<div data-name="tool-content">
<div
data-name="tool-input"
style={{
overflow: 'hidden',
paddingTop: '1rem',
paddingBottom: '1rem',
}}
>
<div
style={{
fontWeight: 500,
color: '#6b7280',
fontSize: '0.75rem',
textTransform: 'uppercase',
letterSpacing: '0.05em',
}}
>
Parameters
</div>
<pre
style={{
width: '100%',
overflowX: 'auto',
borderRadius: '0.375rem',
border: '1px solid #d1d5db',
backgroundColor: '#f9fafb',
padding: '0.75rem',
fontSize: '0.875rem',
}}
>
<code>{JSON.stringify(toolPart.input, null, 2)}</code>
</pre>
</div>
<div
data-name="tool-output"
style={{
overflow: 'hidden',
paddingTop: '1rem',
paddingBottom: '1rem',
}}
>
<div
style={{
fontWeight: 500,
color: '#6b7280',
fontSize: '0.75rem',
textTransform: 'uppercase',
letterSpacing: '0.05em',
}}
>
{toolPart.errorText ? 'Error' : 'Result'}
</div>
<pre
style={{
width: '100%',
overflowX: 'auto',
borderRadius: '0.375rem',
border: '1px solid #d1d5db',
backgroundColor: '#f9fafb',
padding: '0.75rem',
fontSize: '0.875rem',
}}
>
<code>{JSON.stringify(toolPart.output, null, 2)}</code>
</pre>
{toolPart.errorText && (
<div data-name="tool-error" style={{ color: '#dc2626' }}>
{toolPart.errorText}
</div>
)}
</div>
</div>
</details>
</div>
)
}
return null
})}
</div>
))}
</div>
</div>
<form
style={{
width: '100%',
display: 'grid',
gridTemplateColumns: '1fr auto',
gap: '1.5rem',
flexShrink: 0,
paddingTop: '1rem',
}}
onSubmit={handleSubmit}
data-name="prompt-input"
>
<input
name="chat-input"
style={{
borderRadius: '0.5rem',
border: '1px solid #d1d5db',
boxShadow: '0 1px 2px 0 rgba(0,0,0,0.05)',
height: '2.5rem',
padding: '0 0.75rem',
}}
placeholder="City name"
value={input}
onChange={e => setInput(e.target.value)}
/>
<button
style={{
backgroundColor: '#2563eb',
color: 'white',
boxShadow: '0 10px 15px -3px rgba(0,0,0,0.1)',
border: '1px solid #60a5fa',
paddingLeft: '1rem',
paddingRight: '1rem',
whiteSpace: 'nowrap',
borderRadius: '0.5rem',
fontSize: '0.875rem',
fontWeight: 500,
transition: 'all',
flexShrink: 0,
outline: 'none',
}}
type="submit"
>
Send
</button>
</form>
</div>
</main>
)
}

Cela connecte useChat() au point de terminaison /chat/weather-agent, lui envoie les instructions et reçoit la réponse en streaming par fragments.

Tester votre Agent
Lien direct vers Tester votre Agent

Pour tester votre Agent avec l’interface de chat, exécutez simultanément le serveur Mastra et l’application Electron.

  1. Démarrez le serveur de développement Mastra :

    npx mastra dev
  2. Dans un autre terminal, démarrez l’application Electron :

    npm run dev
  3. Une fenêtre Electron s’ouvre avec l’interface de chat

  4. Posez une question sur la météo. Si votre clé API est correctement configurée, vous obtiendrez une réponse

Étapes suivantes
Lien direct vers Étapes suivantes

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

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

  • En savoir plus sur les Agents
  • Donnez à votre Agent ses propres Tools
  • Ajoutez une mémoire semblable à celle des humains à votre Agent

Lorsque vous êtes prêt, découvrez plus en détail l’intégration de Mastra avec AI SDK UI et React, ainsi que le déploiement de votre Agent sur n’importe quelle plateforme :