Electron 프로젝트에 Mastra를 통합하세요
이 가이드에서는 Mastra를 사용하여 Tool 호출 AI Agent를 구축한 다음 Mastra 서버에서 직접 Agent를 호출하여 Electron 데스크톱 앱에 연결합니다.
AI SDK UI를 사용하여 아름다운 대화형 채팅 환경을 만듭니다.
시작하기 전에시작하기 전에에 대한 직접 링크
- 지원되는 Model Provider의 API 키가 필요합니다. 선호하는 Provider가 없다면 OpenAI를 사용하세요.
- Node.js
v22.13.0이상 설치
새 Electron 앱 만들기(선택 사항)새 Electron 앱 만들기(선택 사항)에 대한 직접 링크
이미 Electron 앱이 있다면 다음 단계로 건너뛰세요.
다음을 사용하여 새로운 Electron 앱을 스캐폴드합니다.electron-vite:
- 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 서버를 호출하려면 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.cssedit-maincss에 대한 직접 링크
src/renderer/src/assets/main.css를 열고 내용을 다음과 같이 변경합니다.
@import './base.css';
body {
background: var(--ev-c-white);
color: var(--ev-c-black);
}
마스트라 초기화마스트라 초기화에 대한 직접 링크
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를 포함한 마스트라 구성tools/weather-tool.ts- 특정 위치의 날씨를 가져오는 Toolagents/weather-agent.ts- Tool을 사용하는 Prompt가 있는 기상 요원
다음 단계에서는 채팅 UI에서 weather-agent.ts를 호출합니다.
AI SDK UI 및 AI 요소 설치AI SDK UI 및 AI 요소 설치에 대한 직접 링크
Mastra 어댑터와 함께 AI SDK UI를 설치합니다.
- 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 엔드포인트에 연결하여 Prompt를 전송하고 응답을 청크 단위로 스트리밍합니다.
Agent 테스트Agent 테스트에 대한 직접 링크
채팅 인터페이스로 Agent를 테스트하려면 Mastra 서버와 Electron 앱을 모두 실행하세요.
-
마스터라 개발 서버를 시작합니다:
- 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와 논리를 사용하여 프로젝트를 확장할 수 있습니다.