メインコンテンツへ移動

SvelteKit プロジェクトに Mastra を統合する

このガイドでは、Mastra を使用して Tool を呼び出す AI Agent を構築し、ルートから Agent を直接 import して呼び出すことで SvelteKit に接続します。

AI SDK UI を使用して、美しくインタラクティブなチャット体験を作成します。

始める前に
始める前にへの直接リンク

  • サポートされているモデル Provider の API キーが必要です。特に希望がなければ、OpenAI を使用してください。
  • Node.js v22.13.0 以降をインストールしてください

新しい SvelteKit アプリを作成する(任意)
新しい SvelteKit アプリを作成する(任意)への直接リンク

Tailwind を使用する SvelteKit アプリがすでにある場合は、次の手順に進んでください。

次のコマンドを実行して、新しい SvelteKit アプリを作成します。

npx sv create mastra-svelte --template minimal --types ts --add tailwindcss="plugins:forms" --install npm

これにより mastra-svelte というプロジェクトが作成されますが、任意の名前に置き換えられます。後のスタイリングに使用するため、Tailwind も追加されます。

Mastra を初期化する
Mastra を初期化するへの直接リンク

SvelteKit プロジェクトに移動します。

cd mastra-svelte

mastra init を実行します。プロンプトが表示されたら、Provider(OpenAI など)を選択してキーを入力します。

npx mastra@latest init

これにより、天気 Agent のサンプルと次のファイルを含む src/mastra フォルダーが作成されます。

  • index.ts - Memory を含む Mastra の設定
  • tools/weather-tool.ts - 指定した場所の天気を取得する Tool
  • agents/weather-agent.ts- Tool を使用するプロンプトを備えた天気 Agent

次の手順では、SvelteKit のルートから weather-agent.ts を呼び出します。

AI SDK UI をインストールする
AI SDK UI をインストールするへの直接リンク

AI SDK UI と Mastra アダプターをインストールします。

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

チャットルートを作成する
チャットルートを作成するへの直接リンク

src/routes/api/chat/+server.ts を作成します。

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 ルートはプロンプトを受け取り、Agent のレスポンスを AI SDK 形式でストリーミングします。一方、GET ルートは Memory からメッセージ履歴を取得し、クライアントの再読み込み時に UI を復元できるようにします。

GET ハンドラーが呼び出されるようにするには、src/routes/+page.ts ファイルを作成する必要があります。その load() 関数は +page.svelte と並行して実行されます。

src/routes/+page.ts
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 を次の内容に置き換えます。

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>

このページは Chatapi/chat エンドポイントに接続し、そこへプロンプトを送信して、レスポンスをチャンク単位でストリーミングします。

レスポンスのテキストは、カスタムのメッセージコンポーネントと Tool コンポーネントを使用してレンダリングされます。

Agent をテストする
Agent をテストするへの直接リンク

  1. npm run dev で SvelteKit アプリを実行します
  2. http://localhost:5173 でチャットを開きます
  3. 天気について質問してみます。API キーが正しく設定されていれば、レスポンスが返されます

次のステップ
次のステップへの直接リンク

SvelteKit で Mastra Agent を構築できました!🎉

ここから、独自の Tool やロジックを追加してプロジェクトを拡張できます。

  • Agent について詳しく学ぶ
  • Agent に独自の Tool を追加する
  • Agent に人間のような Memory を追加する

準備ができたら、Mastra と AI SDK UI および SvelteKit の統合方法や、Agent を任意の場所へデプロイする方法について詳しく学びましょう。