Antigravity × Vercel AI SDK 統合ガイド
Vercel AI SDK と Antigravity を組み合わせることで、高性能なAIチャットアプリケーションを素早く構築できます。このガイドでは、実践的な統合方法を詳しく解説します。
Vercel AI SDK の概要
Vercel AI SDK は、AI駆動型のユーザーインターフェースを構築するための JavaScript/TypeScript ライブラリです。複数のLLMプロバイダーに対応し、ストリーミング、ツール呼び出し、会話管理などの機能を提供します。
主な機能
- ストリーミングレスポンス: リアルタイムでテキスト生成を表示
- 複数モデル対応: OpenAI、Anthropic など複数のプロバイダーに対応
- ツール呼び出し: LLM から外部 API を呼び出す
- 会話管理: 会話履歴の自動管理
- マルチモーダル: テキスト、画像、ファイル入力に対応
セットアップと初期設定
プロジェクト初期化
# Next.js プロジェクトの作成
npx create-next-app@latest ai-chat-app \
--typescript \
--tailwind \
--eslint
cd ai-chat-app
# Vercel AI SDK とその依存ライブラリをインストール
npm install ai
# Antigravity SDKもインストール
npm install @antigravitylab/sdk
# 環境変数を設定
cat > .env.local << 'EOF'
ANTIGRAVITY_API_KEY=your-api-key-here
NEXT_PUBLIC_API_URL=http://localhost:3000
EOFpackage.json の設定
{
"name": "antigravity-ai-chat",
"version": "1.0.0",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"ai": "^3.0.0",
"@antigravitylab/sdk": "^1.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"next": "^14.0.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/react": "^18.0.0",
"typescript": "^5.0.0",
"tailwindcss": "^3.0.0",
"autoprefixer": "^10.0.0"
}
}Antigravity API キーは、環境変数または Antigravity ダッシュボードから取得できます。
useChat フックの実装
基本的なチャットUIの構築
// app/page.tsx
'use client';
import { useChat } from 'ai/react';
import { useState } from 'react';
export default function ChatPage() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
api: '/api/chat',
initialMessages: [
{
id: 'system',
role: 'assistant',
content: 'こんにちは!Antigravity AIアシスタントです。何かお手伝いできることはありますか?',
},
],
});
return (
<div className="flex flex-col h-screen bg-gray-900 text-white">
{/* メッセージ表示エリア */}
<div className="flex-1 overflow-y-auto p-6 space-y-4">
{messages.map((message) => (
<div
key={message.id}
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className={`max-w-xs lg:max-w-md xl:max-w-lg px-4 py-2 rounded-lg ${
message.role === 'user'
? 'bg-blue-600 text-white'
: 'bg-gray-700 text-gray-100'
}`}
>
{message.content}
</div>
</div>
))}
{isLoading && (
<div className="flex justify-start">
<div className="bg-gray-700 px-4 py-2 rounded-lg">
<div className="flex space-x-2">
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.4s' }}></div>
</div>
</div>
</div>
)}
</div>
{/* 入力フォーム */}
<form onSubmit={handleSubmit} className="border-t border-gray-700 p-4">
<div className="flex gap-4">
<input
type="text"
value={input}
onChange={handleInputChange}
placeholder="メッセージを入力..."
disabled={isLoading}
className="flex-1 px-4 py-2 bg-gray-800 border border-gray-600 rounded-lg focus:outline-none focus:border-blue-500 disabled:opacity-50"
/>
<button
type="submit"
disabled={isLoading}
className="px-6 py-2 bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-colors"
>
送信
</button>
</div>
</form>
</div>
);
}API ハンドラーの実装
// app/api/chat/route.ts
import { Message, StreamingTextResponse } from 'ai';
import Antigravity from '@antigravitylab/sdk';
const client = new Antigravity({
apiKey: process.env.ANTIGRAVITY_API_KEY,
});
export async function POST(req: Request) {
const { messages } = await req.json();
// Antigravity エージェントでストリーミング応答を生成
const stream = await client.streamCompletion({
messages: messages.map((msg: Message) => ({
role: msg.role,
content: msg.content,
})),
model: 'claude-3-opus',
system: `
あなたは Antigravity IDE の専門的なアシスタントです。
ユーザーの質問に対して、正確で実用的な回答を提供してください。
必要に応じて、コード例を含めてください。
`,
temperature: 0.7,
maxTokens: 2048,
});
// Stream を StreamingTextResponse でラップ
return new StreamingTextResponse(stream);
}API キーは環境変数で安全に管理してください。クライアントサイドに公開しないようにしましょう。
useCompletion フックとの統合
テキスト生成UI
// app/components/TextGeneration.tsx
'use client';
import { useCompletion } from 'ai/react';
import { useState } from 'react';
export function TextGenerationPage() {
const { completion, input, handleInputChange, handleSubmit, isLoading } = useCompletion({
api: '/api/completion',
onFinish: (prompt, completion) => {
console.log('生成完了:', completion);
},
});
const [generatedText, setGeneratedText] = useState('');
const handleGenerateCustom = async (prompt: string) => {
const result = await fetch('/api/completion', {
method: 'POST',
body: JSON.stringify({ prompt }),
});
const { text } = await result.json();
setGeneratedText(text);
};
return (
<div className="max-w-4xl mx-auto p-6">
<h1 className="text-3xl font-bold mb-6">テキスト生成ツール</h1>
{/* プリセットテンプレート */}
<div className="grid grid-cols-2 gap-4 mb-8">
<button
onClick={() => handleGenerateCustom('ブログ記事のタイトル案を5つ生成してください')}
className="p-4 bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors"
>
ブログタイトル生成
</button>
<button
onClick={() => handleGenerateCustom('製品説明文を作成してください')}
className="p-4 bg-purple-600 rounded-lg hover:bg-purple-700 transition-colors"
>
製品説明生成
</button>
</div>
{/* 入力フォーム */}
<form onSubmit={handleSubmit} className="mb-8">
<textarea
value={input}
onChange={handleInputChange}
placeholder="カスタムプロンプトを入力..."
className="w-full px-4 py-2 bg-gray-800 border border-gray-600 rounded-lg mb-4"
rows={4}
/>
<button
type="submit"
disabled={isLoading}
className="px-6 py-2 bg-green-600 rounded-lg hover:bg-green-700 disabled:opacity-50"
>
生成
</button>
</form>
{/* 生成結果 */}
{completion && (
<div className="bg-gray-800 p-6 rounded-lg border border-gray-700">
<h2 className="text-xl font-bold mb-4">生成結果</h2>
<p className="text-gray-200 whitespace-pre-wrap">{completion}</p>
</div>
)}
</div>
);
}Completion API ハンドラー
// app/api/completion/route.ts
import { StreamingTextResponse } from 'ai';
import Antigravity from '@antigravitylab/sdk';
const client = new Antigravity({
apiKey: process.env.ANTIGRAVITY_API_KEY,
});
export async function POST(req: Request) {
const { prompt } = await req.json();
const stream = await client.streamCompletion({
messages: [
{
role: 'user',
content: prompt,
},
],
model: 'claude-3-opus',
maxTokens: 1024,
});
return new StreamingTextResponse(stream);
}ツール呼び出し (Function Calling)
ツール定義とハンドラー
// lib/tools.ts
export const tools = {
getCurrentWeather: {
description: '指定した場所の現在の天気を取得',
parameters: {
type: 'object',
properties: {
location: {
type: 'string',
description: '場所(都市名)',
},
unit: {
type: 'string',
enum: ['celsius', 'fahrenheit'],
description: '温度単位',
},
},
required: ['location'],
},
},
getCodeReview: {
description: 'コードをレビューして改善提案を提供',
parameters: {
type: 'object',
properties: {
code: {
type: 'string',
description: 'レビュー対象のコード',
},
language: {
type: 'string',
description: 'プログラミング言語',
},
},
required: ['code', 'language'],
},
},
executeCommand: {
description: 'シェルコマンドを実行',
parameters: {
type: 'object',
properties: {
command: {
type: 'string',
description: '実行するコマンド',
},
cwd: {
type: 'string',
description: 'ワーキングディレクトリ',
},
},
required: ['command'],
},
},
};
// ツール実行関数
export async function executeTool(
toolName: string,
toolInput: Record<string, unknown>
): Promise<string> {
switch (toolName) {
case 'getCurrentWeather':
return await getWeather(toolInput.location as string);
case 'getCodeReview':
return await reviewCode(
toolInput.code as string,
toolInput.language as string
);
case 'executeCommand':
return await executeShellCommand(
toolInput.command as string,
toolInput.cwd as string
);
default:
return `Unknown tool: ${toolName}`;
}
}
async function getWeather(location: string): Promise<string> {
// 実装: 天気APIを呼び出す
return `${location}の現在の気温は20℃です。`;
}
async function reviewCode(code: string, language: string): Promise<string> {
// 実装: コードレビューロジック
return `${language}コードのレビュー: 改善点があります。`;
}
async function executeShellCommand(
command: string,
cwd?: string
): Promise<string> {
// 実装: シェルコマンド実行
return `コマンド実行結果: ${command}`;
}ツール呼び出しの統合
// app/api/chat-with-tools/route.ts
import { Message, StreamingTextResponse } from 'ai';
import Antigravity from '@antigravitylab/sdk';
import { tools, executeTool } from '@/lib/tools';
const client = new Antigravity({
apiKey: process.env.ANTIGRAVITY_API_KEY,
});
export async function POST(req: Request) {
const { messages } = await req.json();
// ツール呼び出し対応のストリーム生成
const response = await client.streamChatWithTools({
messages,
tools,
onToolCall: async (toolName, toolInput) => {
const result = await executeTool(toolName, toolInput);
return {
toolName,
result,
};
},
model: 'claude-3-opus',
});
return new StreamingTextResponse(response);
}ツール呼び出しにより、LLM が外部 API やシステムコマンドと連携できるようになります。
マルチモーダル入力
画像とテキストの入力処理
// app/components/MultimodalChat.tsx
'use client';
import { useState, useRef } from 'react';
import { useChat } from 'ai/react';
export function MultimodalChat() {
const fileInputRef = useRef<HTMLInputElement>(null);
const [attachedFiles, setAttachedFiles] = useState<File[]>([]);
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
api: '/api/chat-multimodal',
});
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(event.target.files || []);
setAttachedFiles(prev => [...prev, ...files]);
};
const handleRemoveFile = (index: number) => {
setAttachedFiles(prev => prev.filter((_, i) => i !== index));
};
const handleSubmitWithFiles = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// ファイルをBase64エンコード
const filesData = await Promise.all(
attachedFiles.map(async file => ({
name: file.name,
type: file.type,
data: await fileToBase64(file),
}))
);
// フォームデータに含める
const formData = new FormData(e.currentTarget);
formData.append('files', JSON.stringify(filesData));
// ここで送信ロジック
setAttachedFiles([]);
};
const fileToBase64 = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(file);
});
};
return (
<div className="flex flex-col h-screen bg-gray-900 text-white">
<div className="flex-1 overflow-y-auto p-6">
{messages.map(msg => (
<div key={msg.id} className="mb-4">
<span className="font-bold">{msg.role}:</span> {msg.content}
</div>
))}
</div>
<form onSubmit={handleSubmitWithFiles} className="border-t border-gray-700 p-4">
{/* アタッチファイルリスト */}
{attachedFiles.length > 0 && (
<div className="mb-4 space-y-2">
{attachedFiles.map((file, index) => (
<div key={index} className="flex items-center justify-between bg-gray-800 p-2 rounded">
<span className="text-sm">{file.name}</span>
<button
type="button"
onClick={() => handleRemoveFile(index)}
className="text-red-400 hover:text-red-300"
>
削除
</button>
</div>
))}
</div>
)}
{/* 入力フィールド */}
<div className="flex gap-4">
<button
type="button"
onClick={() => fileInputRef.current?.click()}
className="px-4 py-2 bg-gray-700 rounded hover:bg-gray-600"
>
ファイル追加
</button>
<input
ref={fileInputRef}
type="file"
multiple
onChange={handleFileSelect}
accept="image/*,.pdf,.txt,.md"
className="hidden"
/>
<input
type="text"
value={input}
onChange={handleInputChange}
placeholder="メッセージを入力..."
className="flex-1 px-4 py-2 bg-gray-800 border border-gray-600 rounded"
/>
<button
type="submit"
disabled={isLoading}
className="px-6 py-2 bg-blue-600 rounded hover:bg-blue-700 disabled:opacity-50"
>
送信
</button>
</div>
</form>
</div>
);
}マルチモーダル API ハンドラー
// app/api/chat-multimodal/route.ts
import { StreamingTextResponse } from 'ai';
import Antigravity from '@antigravitylab/sdk';
const client = new Antigravity({
apiKey: process.env.ANTIGRAVITY_API_KEY,
});
export async function POST(req: Request) {
const formData = await req.formData();
const message = formData.get('message') as string;
const filesJson = formData.get('files') as string;
const files = JSON.parse(filesJson || '[]');
const stream = await client.streamCompletion({
messages: [
{
role: 'user',
content: message,
attachments: files.map(f => ({
type: 'document',
mimeType: f.type,
data: f.data,
})),
},
],
model: 'claude-3-opus',
});
return new StreamingTextResponse(stream);
}高度な機能
会話履歴の永続化
// lib/db.ts
import { Message } from 'ai';
export class ChatHistoryDB {
static async saveMessage(conversationId: string, message: Message) {
// データベースに保存
const response = await fetch('/api/history', {
method: 'POST',
body: JSON.stringify({ conversationId, message }),
});
return response.json();
}
static async loadHistory(conversationId: string): Promise<Message[]> {
const response = await fetch(`/api/history?conversationId=${conversationId}`);
return response.json();
}
static async deleteHistory(conversationId: string) {
await fetch(`/api/history/${conversationId}`, { method: 'DELETE' });
}
}カスタムホック
// hooks/useCustomChat.ts
import { useChat } from 'ai/react';
import { useEffect, useState } from 'react';
import { ChatHistoryDB } from '@/lib/db';
export function useCustomChat(conversationId: string) {
const [isLoading, setIsLoading] = useState(true);
const chat = useChat({
api: '/api/chat',
});
useEffect(() => {
// 会話履歴を読み込み
ChatHistoryDB.loadHistory(conversationId)
.then(messages => {
// メッセージを復元
setIsLoading(false);
});
}, [conversationId]);
// メッセージ送信時に履歴を保存
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
await chat.handleSubmit(e);
// 最新メッセージを保存
if (chat.messages.length > 0) {
const lastMessage = chat.messages[chat.messages.length - 1];
await ChatHistoryDB.saveMessage(conversationId, lastMessage);
}
};
return {
...chat,
isLoading: isLoading || chat.isLoading,
handleSubmit,
};
}デプロイとベストプラクティス
Vercel へのデプロイ
# リポジトリを初期化
git init
git add .
git commit -m "Initial commit"
# Vercel にデプロイ
vercel deploy
# 環境変数を設定
vercel env add ANTIGRAVITY_API_KEYセキュリティとパフォーマンス
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
// API キー検証
const apiKey = request.headers.get('Authorization');
if (!apiKey) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
// レート制限
const ip = request.ip || 'unknown';
const rateLimit = checkRateLimit(ip);
if (!rateLimit) {
return NextResponse.json(
{ error: 'Rate limit exceeded' },
{ status: 429 }
);
}
return NextResponse.next();
}
function checkRateLimit(ip: string): boolean {
// 実装: レート制限チェック
return true;
}
export const config = {
matcher: '/api/:path*',
};実践例:カスタムAIアシスタント
// app/components/AntgravityAssistant.tsx
'use client';
import { useChat } from 'ai/react';
export function AntgravityAssistant() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
api: '/api/assistant',
system: `
You are Antigravity AI Assistant.
Help users with:
- Antigravity IDE usage
- Code analysis and suggestions
- Project setup guidance
- Performance optimization tips
`,
});
return (
<div className="max-w-2xl mx-auto p-6">
<h1 className="text-2xl font-bold mb-6">Antigravity AI アシスタント</h1>
<div className="space-y-4 mb-6 h-96 overflow-y-auto bg-gray-800 p-4 rounded-lg">
{messages.map(msg => (
<div key={msg.id} className={msg.role === 'user' ? 'text-right' : 'text-left'}>
<div className={`inline-block px-4 py-2 rounded-lg max-w-xs ${
msg.role === 'user' ? 'bg-blue-600' : 'bg-gray-700'
}`}>
{msg.content}
</div>
</div>
))}
</div>
<form onSubmit={handleSubmit} className="flex gap-2">
<input
type="text"
value={input}
onChange={handleInputChange}
placeholder="質問を入力..."
className="flex-1 px-4 py-2 bg-gray-800 border border-gray-600 rounded"
disabled={isLoading}
/>
<button
type="submit"
disabled={isLoading}
className="px-6 py-2 bg-blue-600 rounded hover:bg-blue-700 disabled:opacity-50"
>
{isLoading ? '送信中...' : '送信'}
</button>
</form>
</div>
);
}全体を振り返って
Antigravity と Vercel AI SDK の組み合わせにより、以下を実現できます:
- リアルタイムストリーミング会話UI
- 複数のLLMモデルへの対応
- 高度なツール呼び出し機能
- マルチモーダル入力対応
- スケーラブルなデプロイメント
次のステップで、さらに高度なAIアプリケーションを構築してみましょう。
個人開発12年の現場で実感したこと
線引きするときの3つの判断軸
- 失敗時の影響が金銭やユーザー体験にどれだけ波及するか
- 復旧オペレーションが明文化されているか
- 観測ログから人間が再現できる粒度に整っているか