Electron プロジェクトに Mastra を統合する
このガイドでは、Mastra を使用して Tool を呼び出す AI Agent を構築し、Mastra のサーバーから Agent を直接呼び出すことで Electron デスクトップアプリに接続します。
AI SDK UI を使用して、美しくインタラクティブなチャット体験を作成します。
始める前に始める前にへの直接リンク
- サポートされているモデル Provider の API キーが必要です。特に希望がなければ、OpenAI を使用してください。
- Node.js
v22.13.0以降をインストールしてください
新しい Electron アプリを作成する(任意)新しい Electron アプリを作成する(任意)への直接リンク
Electron アプリがすでにある場合は、次の手順に進んでください。
electron-vite を使用して、新しい Electron アプリをスキャフォールドします。
- npm
- pnpm
- Yarn
- Bun
npm create @quick-start/electron@latest electron-chat -- --template react-ts --skip
pnpm create @quick-start/electron electron-chat --template react-ts --skip
yarn create @quick-start/electron electron-chat --template react-ts --skip
bunx @quick-start/create-electron@latest electron-chat --template react-ts --skip
これにより、React と TypeScript を使用する electron-chat という新しい Electron アプリが作成されます。プロジェクトディレクトリに移動します。
cd electron-chat
CSP 設定を編集するCSP 設定を編集するへの直接リンク
Electron アプリから Mastra サーバーを呼び出せるように、Content Security Policy(CSP)の設定を調整する必要があります。
src/renderer/index.html を開き、<meta http-equiv="Content-Security-Policy"> タグの connect-src ディレクティブに http://localhost:4111 を追加します。
<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:"
/>
本番環境にデプロイする際は、実際のサーバー URL に合わせてこの設定を調整する必要があります。
main.css を編集するedit-maincssへの直接リンク
src/renderer/src/assets/main.css を開き、内容を次に変更します。
@import './base.css';
body {
background: var(--ev-c-white);
color: var(--ev-c-black);
}
Mastra を初期化するMastra を初期化するへの直接リンク
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 のサンプルと次のファイルを含む src/mastra フォルダーが作成されます。
index.ts- Memory を含む Mastra の設定tools/weather-tool.ts- 指定した場所の天気を取得する Toolagents/weather-agent.ts- Tool を使用するプロンプトを備えた天気 Agent
次の手順では、チャット UI から weather-agent.ts を呼び出します。
AI SDK UI と AI Elements をインストールするAI SDK UI と AI Elements をインストールするへの直接リンク
AI SDK UI と Mastra アダプターをインストールします。
- npm
- pnpm
- Yarn
- Bun
npm install @mastra/ai-sdk@latest @ai-sdk/react ai
pnpm add @mastra/ai-sdk@latest @ai-sdk/react ai
yarn add @mastra/ai-sdk@latest @ai-sdk/react ai
bun add @mastra/ai-sdk@latest @ai-sdk/react ai
チャットルートを作成するチャットルートを作成するへの直接リンク
src/mastra/index.ts を開き、設定に chatRoute() を追加します。これにより、Electron フロントエンドから AI SDK 互換のチャットレスポンスを取得できる API ルートが作成され、次の手順で useChat() と組み合わせて使用できます。
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',
}),
],
},
})
Electron のレンダラーは Mastra サーバーとは異なるオリジンから読み込まれるため、CORS が必要です。本番環境にデプロイする際は、オリジンをアプリケーションの実際のオリジンに制限してください。
チャット UI を追加するチャット UI を追加するへの直接リンク
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>
)
}
このコンポーネントは useChat() を /chat/weather-agent エンドポイントに接続し、そこへプロンプトを送信して、レスポンスをチャンク単位でストリーミングします。
Agent をテストするAgent をテストするへの直接リンク
チャットインターフェースで Agent をテストするには、Mastra サーバーと Electron アプリの両方を実行します。
-
Mastra 開発サーバーを起動します。
- npm
- pnpm
- Yarn
- Bun
npx mastra devpnpm dlx mastra devyarn dlx mastra devbun x mastra dev -
別のターミナルで Electron アプリを起動します。
- npm
- pnpm
- Yarn
- Bun
npm run devpnpm run devyarn devbun run dev -
チャットインターフェースを表示する Electron ウィンドウが開きます
-
天気について質問してみます。API キーが正しく設定されていれば、レスポンスが返されます
次のステップ次のステップへの直接リンク
Electron で Mastra Agent を構築できました!🎉
ここから、独自の Tool やロジックを追加してプロジェクトを拡張できます。
準備ができたら、Mastra と AI SDK UI および React の統合方法や、Agent を任意の場所へデプロイする方法について詳しく学びましょう。