SvelteKit 프로젝트에 Mastra 통합
이 가이드에서는 Mastra를 사용하여 Tool 호출 AI Agent를 구축한 다음 경로에서 직접 Agent를 가져오고 호출하여 SvelteKit에 연결합니다.
AI SDK UI를 사용하여 아름다운 대화형 채팅 환경을 만듭니다.
시작하기 전에시작하기 전에에 대한 직접 링크
- 지원되는 Model Provider의 API 키가 필요합니다. 선호하는 Provider가 없다면 OpenAI를 사용하세요.
- Node.js
v22.13.0이상 설치
새 SvelteKit 앱 만들기(선택 사항)새 SvelteKit 앱 만들기(선택 사항)에 대한 직접 링크
Tailwind를 사용하는 SvelteKit 앱이 이미 있는 경우 다음 단계로 건너뛰세요.
다음 명령을 실행하여 새 SvelteKit 앱을 생성합니다.
- npm
- pnpm
- Yarn
- Bun
npx sv create mastra-svelte --template minimal --types ts --add tailwindcss="plugins:forms" --install npm
pnpm dlx sv create mastra-svelte --template minimal --types ts --add tailwindcss="plugins:forms" --install npm
yarn dlx sv create mastra-svelte --template minimal --types ts --add tailwindcss="plugins:forms" --install npm
bun x sv create mastra-svelte --template minimal --types ts --add tailwindcss="plugins:forms" --install npm
이 명령은 mastra-svelte라는 프로젝트를 생성하지만 원하는 이름으로 바꿀 수 있습니다. 이후 스타일링을 위해 Tailwind도 추가되었습니다.
마스트라 초기화마스트라 초기화에 대한 직접 링크
SvelteKit 프로젝트로 이동합니다.
cd mastra-svelte
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가 있는 기상 요원
다음 단계에서는 SvelteKit 라우트에서 weather-agent.ts를 호출합니다.
AI SDK UI 설치AI SDK UI 설치에 대한 직접 링크
Mastra 어댑터와 함께 AI SDK UI를 설치합니다.
- npm
- pnpm
- Yarn
- Bun
npm install @mastra/ai-sdk@latest @ai-sdk/svelte ai
pnpm add @mastra/ai-sdk@latest @ai-sdk/svelte ai
yarn add @mastra/ai-sdk@latest @ai-sdk/svelte ai
bun add @mastra/ai-sdk@latest @ai-sdk/svelte ai
채팅 경로 만들기채팅 경로 만들기에 대한 직접 링크
만들다src/routes/api/chat/+server.ts:
import type { RequestHandler } from './$types'
import { handleChatStream } from '@mastra/ai-sdk'
import { toAISdkV5Messages } from '@mastra/ai-sdk/ui'
import { createUIMessageStreamResponse } from 'ai'
import { mastra } from '../../../mastra'
const THREAD_ID = 'example-user-id'
const RESOURCE_ID = 'weather-chat'
export const POST: RequestHandler = async ({ request }) => {
const params = await request.json()
const stream = await handleChatStream({
mastra,
agentId: 'weather-agent',
params: {
...params,
memory: {
...params.memory,
thread: THREAD_ID,
resource: RESOURCE_ID,
},
},
})
return createUIMessageStreamResponse({ stream })
}
export const GET: RequestHandler = async () => {
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 Response.json(uiMessages)
}
POST 라우트는 Prompt를 받아 Agent의 응답을 AI SDK 형식으로 스트리밍하고, GET 라우트는 Memory에서 메시지 기록을 가져와 클라이언트가 다시 로드될 때 UI를 hydrate할 수 있게 합니다.
GET 핸들러가 호출되게 하려면 src/routes/+page.ts 파일을 생성해야 합니다. 이 파일의 load() 함수는 +page.svelte와 함께 실행됩니다.
import type { UIDataTypes, UIMessage, UITools } from 'ai'
import type { PageLoad } from './$types'
export const load: PageLoad = async ({ fetch }) => {
const response = await fetch('/api/chat')
const initialMessages = (await response.json()) as UIMessage<unknown, UIDataTypes, UITools>[]
return { initialMessages }
}
채팅 UI 추가채팅 UI 추가에 대한 직접 링크
src/routes/+page.svelte를 다음 내용으로 교체합니다.
<script lang="ts">
import { Chat } from '@ai-sdk/svelte';
import { DefaultChatTransport, type ToolUIPart } from 'ai';
let input = $state('');
const { data } = $props();
let messages = $derived(data.initialMessages)
const chat = new Chat({
transport: new DefaultChatTransport({
api: '/api/chat'
}),
get messages() {
return messages;
}
});
function handleSubmit(event: SubmitEvent) {
event.preventDefault();
chat.sendMessage({ text: input });
input = '';
}
const STATE_TO_LABEL_MAP: Record<ToolUIPart["state"], string> = {
"input-streaming": "Pending",
"input-available": "Running",
// @ts-expect-error - Only available in AI SDK v6
"approval-requested": "Awaiting Approval",
"approval-responded": "Responded",
"output-available": "Completed",
"output-error": "Error",
"output-denied": "Denied",
};
</script>
<main class="max-w-3xl mx-auto p-6 size-full h-screen">
<div class="flex flex-col h-full">
<div class="flex-1 min-h-0 overflow-y-auto" data-name="conversation">
<div data-name="conversation-content" class="flex flex-col gap-8">
{#each chat.messages as message, messageIndex (messageIndex)}
<div>
{#each message.parts as part, partIndex (partIndex)}
{#if part.type === 'text'}
<div data-name="message" class={[message.role === 'user' && 'ml-auto justify-end', 'group flex w-full max-w-[95%] flex-col gap-2', message.role === 'user' ? 'is-user' : 'is-assistant']}>
<div data-name="message-content" class={["is-user:dark flex w-fit max-w-full min-w-0 flex-col gap-2 overflow-hidden text-sm",
"group-[.is-user]:ml-auto group-[.is-user]:rounded-lg group-[.is-user]:bg-blue-100 group-[.is-user]:px-4 group-[.is-user]:py-3 group-[.is-user]:text-foreground",
"group-[.is-assistant]:text-foreground"]}>
<div data-name="message-response" class="size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
{part.text}
</div>
</div>
</div>
{:else if part.type.startsWith('tool-')}
<div data-name="tool" class="not-prose mb-6 w-full rounded-lg border border-gray-300 shadow">
<details data-name="tool-header" class="w-full p-3 hover:cursor-pointer">
<summary class="font-medium text-sm">{(part as ToolUIPart).type.split("-").slice(1).join("-")} - {STATE_TO_LABEL_MAP[(part as ToolUIPart).state ?? 'output-available']}</summary>
<div data-name="tool-content" class="">
<div data-name="tool-input" class="space-y-2 overflow-hidden py-4">
<div class="font-medium text-muted-foreground text-xs uppercase tracking-wide">Parameters</div>
<pre class="w-full overflow-x-auto rounded-md border border-gray-300 bg-gray-50 p-3 text-sm"><code>{JSON.stringify((part as ToolUIPart).input, null, 2)}</code></pre>
</div>
<div data-name="tool-output" class="space-y-2 overflow-hidden py-4">
<div class="font-medium text-muted-foreground text-xs uppercase tracking-wide">{(part as ToolUIPart).errorText ? 'Error' : 'Result'}</div>
<pre class="w-full overflow-x-auto rounded-md border border-gray-300 bg-gray-50 p-3 text-sm"><code>{JSON.stringify((part as ToolUIPart).output, null, 2)}</code></pre>
{#if (part as ToolUIPart).errorText}
<div data-name="tool-error" class="text-red-600">
{(part as ToolUIPart).errorText}
</div>
{/if}
</div>
</div>
</details>
</div>
{/if}
{/each}
</div>
{/each}
</div>
</div>
<form class="w-full grid grid-cols-[1fr_auto] gap-6 shrink-0 pt-4" onsubmit={handleSubmit} data-name="prompt-input">
<input name="chat-input" class="rounded-lg border border-gray-300 shadow h-10" placeholder="City name" bind:value={input} />
<button class="bg-blue-600 text-white shadow-lg border border-blue-400 px-4 whitespace-nowrap rounded-lg text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]" type="submit">Send</button>
</form>
</div>
</main>
이 페이지는 Chat을 api/chat 엔드포인트에 연결하여 Prompt를 전송하고 응답을 청크 단위로 스트리밍합니다.
사용자 정의 메시지 및 Tool 구성 요소를 사용하여 응답 텍스트를 렌더링합니다.
Agent 테스트Agent 테스트에 대한 직접 링크
- 다음을 사용하여 SvelteKit 앱을 실행하세요.
npm run dev - 다음에서 채팅을 엽니다.http://localhost:5173
- 날씨에 대해 물어보세요. API 키가 올바르게 설정되면 응답을 받게 됩니다.
다음 단계다음 단계에 대한 직접 링크
SvelteKit을 사용하여 Mastra Agent를 구축한 것을 축하합니다! 🎉
여기에서 자신만의 Tool와 논리를 사용하여 프로젝트를 확장할 수 있습니다.