ANTIGRAVITY LABEN
記事一覧/連携・プラグイン
連携・プラグイン/2026-03-14中級

Antigravity × Vercel AI SDK 統合ガイド — AIチャット・ストリーミングUIの高速構築

Antigravity で Vercel AI SDK を使ったAIチャットアプリケーションを構築する完全ガイド。useChat/useCompletion フック、ストリーミングレスポンス、ツール呼び出し、マルチモーダル入力まで。

Antigravity338VercelAI SDKstreaming5Next.js6

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
EOF

package.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つの判断軸

  • 失敗時の影響が金銭やユーザー体験にどれだけ波及するか
  • 復旧オペレーションが明文化されているか
  • 観測ログから人間が再現できる粒度に整っているか
シェア

お読みいただきありがとうございます

Antigravity Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

連携・プラグイン2026-07-11
許可を尋ねられても、答える人がいない — 統一パーミッションを無人実行の契約として書く
v2.2.1 の統一パーミッションは対話を前提にしています。深夜に走るスケジュール実行では、その前提が崩れます。allow / deny / queue の三分法でポリシーを事前宣言し、無人でも止まらない契約として書き下す設計をまとめました。
連携・プラグイン2026-07-09
Antigravity からローカル LLM を呼ぶ — Ollama・LM Studio 統合と実用ワークフロー
Ollama や LM Studio を介した Antigravity のローカル LLM 運用を、実測ベンチ・損益分岐の計算モデル・JSON 出力を強制するラッパー実装まで含めて整理しました。機密データを扱う案件と API コストの両面から、乗り換えの判断材料を揃えています。
連携・プラグイン2026-07-08
Antigravity の OAuth トークンが OS キーリングに保存されるようになった—ヘッドレス運用で認証を崩さない設計
v2.2.1 で更新後の OAuth トークンが OS キーリングへ自動保存されるようになりました。デスクトップでは快適な一方、ヘッドレスのスケジュール実行では保管庫そのものが存在せず認証が崩れます。バックエンドの明示指定・安全なファイルフォールバック・保存場所ごとの生死確認までを設計します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →