Skip to content

🤖 OpenAI APIの統合:チャットとテキスト処理 #8

@Wanko-IT

Description

@Wanko-IT

📋 タスク概要

優先度:
ステータス: 保留中
依存関係: 認証システム、ダッシュボードUIフレームワーク

🎯 説明

OpenAI APIを統合してGPT-4、GPT-3.5-turbo、その他の言語モデルを活用したチャット機能とテキスト処理機能を実装します。

🏗️ 実装詳細

対応AIモデル

  1. OpenAI モデル

    • GPT-4 Turbo (gpt-4-0125-preview)
    • GPT-4 (gpt-4)
    • GPT-3.5 Turbo (gpt-3.5-turbo-0125)
    • GPT-3.5 Turbo 16k (gpt-3.5-turbo-16k)
  2. 将来の拡張

    • Claude (Anthropic)
    • Gemini (Google)
    • LLaMA (Meta)

API統合アーキテクチャ

// lib/openai.ts
import OpenAI from 'openai'

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
})

export interface ChatMessage {
  role: 'system' | 'user' | 'assistant'
  content: string
  timestamp?: Date
}

export interface ChatOptions {
  model: 'gpt-4' | 'gpt-3.5-turbo' | 'gpt-4-turbo'
  temperature?: number
  maxTokens?: number
  stream?: boolean
}

export async function createChatCompletion(
  messages: ChatMessage[],
  options: ChatOptions = { model: 'gpt-3.5-turbo' }
) {
  try {
    const response = await openai.chat.completions.create({
      model: options.model,
      messages: messages.map(({ role, content }) => ({ role, content })),
      temperature: options.temperature ?? 0.7,
      max_tokens: options.maxTokens ?? 2000,
      stream: options.stream ?? false,
    })

    return response
  } catch (error) {
    console.error('OpenAI API エラー:', error)
    throw new Error('AI応答の生成に失敗しました')
  }
}

// ストリーミング対応
export async function* createStreamingChatCompletion(
  messages: ChatMessage[],
  options: ChatOptions
) {
  const stream = await openai.chat.completions.create({
    ...options,
    messages: messages.map(({ role, content }) => ({ role, content })),
    stream: true,
  })

  for await (const chunk of stream) {
    const content = chunk.choices[0]?.delta?.content
    if (content) {
      yield content
    }
  }
}

バックエンドAPIエンドポイント

// pages/api/chat/completions.ts
import { NextApiRequest, NextApiResponse } from 'next'
import { createChatCompletion, createStreamingChatCompletion } from '@/lib/openai'
import { verifyAuth } from '@/lib/auth'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' })
  }

  try {
    // 認証確認
    const user = await verifyAuth(req)
    if (!user) {
      return res.status(401).json({ error: '認証が必要です' })
    }

    const { messages, model, temperature, maxTokens, stream } = req.body

    // 使用量制限チェック
    await checkUsageLimit(user.id)

    if (stream) {
      // ストリーミングレスポンス
      res.writeHead(200, {
        'Content-Type': 'text/plain; charset=utf-8',
        'Transfer-Encoding': 'chunked',
      })

      const streamGenerator = createStreamingChatCompletion(messages, {
        model,
        temperature,
        maxTokens,
        stream: true,
      })

      for await (const chunk of streamGenerator) {
        res.write(chunk)
      }
      
      res.end()
    } else {
      // 通常のレスポンス
      const response = await createChatCompletion(messages, {
        model,
        temperature,
        maxTokens,
      })

      // 使用量記録
      await recordUsage(user.id, {
        model,
        tokensUsed: response.usage?.total_tokens || 0,
        cost: calculateCost(model, response.usage?.total_tokens || 0),
      })

      res.status(200).json(response)
    }
  } catch (error) {
    console.error('Chat API エラー:', error)
    res.status(500).json({ error: 'サーバーエラーが発生しました' })
  }
}

フロントエンド実装

// hooks/useChat.ts
import { useState, useCallback } from 'react'
import { ChatMessage, ChatOptions } from '@/lib/openai'

export function useChat() {
  const [messages, setMessages] = useState<ChatMessage[]>([])
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)

  const sendMessage = useCallback(async (
    content: string,
    options?: ChatOptions
  ) => {
    const userMessage: ChatMessage = {
      role: 'user',
      content,
      timestamp: new Date(),
    }

    setMessages(prev => [...prev, userMessage])
    setLoading(true)
    setError(null)

    try {
      const response = await fetch('/api/chat/completions', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          messages: [...messages, userMessage],
          ...options,
        }),
      })

      if (!response.ok) {
        throw new Error('API リクエストが失敗しました')
      }

      const data = await response.json()
      const assistantMessage: ChatMessage = {
        role: 'assistant',
        content: data.choices[0].message.content,
        timestamp: new Date(),
      }

      setMessages(prev => [...prev, assistantMessage])
    } catch (err) {
      setError(err instanceof Error ? err.message : '不明なエラー')
    } finally {
      setLoading(false)
    }
  }, [messages])

  const clearChat = useCallback(() => {
    setMessages([])
    setError(null)
  }, [])

  return {
    messages,
    loading,
    error,
    sendMessage,
    clearChat,
  }
}

// components/chat/ChatInterface.tsx
import { useState } from 'react'
import { useChat } from '@/hooks/useChat'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card } from '@/components/ui/card'
import { ScrollArea } from '@/components/ui/scroll-area'

export function ChatInterface() {
  const [input, setInput] = useState('')
  const [selectedModel, setSelectedModel] = useState<'gpt-4' | 'gpt-3.5-turbo'>('gpt-3.5-turbo')
  const { messages, loading, error, sendMessage, clearChat } = useChat()

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    if (!input.trim() || loading) return

    await sendMessage(input, { model: selectedModel })
    setInput('')
  }

  return (
    <div className="flex flex-col h-full">
      {/* ヘッダー */}
      <div className="flex items-center justify-between p-4 border-b">
        <h2 className="text-xl font-semibold">AIチャット</h2>
        <div className="flex items-center space-x-2">
          <select
            value={selectedModel}
            onChange={(e) => setSelectedModel(e.target.value as any)}
            className="px-3 py-1 border rounded"
          >
            <option value="gpt-3.5-turbo">GPT-3.5 Turbo</option>
            <option value="gpt-4">GPT-4</option>
          </select>
          <Button variant="outline" onClick={clearChat}>
            クリア
          </Button>
        </div>
      </div>

      {/* メッセージ表示エリア */}
      <ScrollArea className="flex-1 p-4">
        <div className="space-y-4">
          {messages.map((message, index) => (
            <Card key={index} className={`p-4 ${
              message.role === 'user' 
                ? 'bg-blue-50 ml-12' 
                : 'bg-gray-50 mr-12'
            }`}>
              <div className="flex items-start space-x-2">
                <div className={`w-2 h-2 rounded-full mt-2 ${
                  message.role === 'user' ? 'bg-blue-500' : 'bg-green-500'
                }`} />
                <div className="flex-1">
                  <p className="text-sm font-medium mb-1">
                    {message.role === 'user' ? 'あなた' : 'AI'}
                  </p>
                  <p className="text-sm whitespace-pre-wrap">{message.content}</p>
                </div>
              </div>
            </Card>
          ))}
        </div>
      </ScrollArea>

      {/* エラー表示 */}
      {error && (
        <div className="p-4 bg-red-50 border-t">
          <p className="text-red-600 text-sm">{error}</p>
        </div>
      )}

      {/* 入力エリア */}
      <form onSubmit={handleSubmit} className="p-4 border-t">
        <div className="flex space-x-2">
          <Input
            value={input}
            onChange={(e) => setInput(e.target.value)}
            placeholder="メッセージを入力してください..."
            disabled={loading}
            className="flex-1"
          />
          <Button type="submit" disabled={loading || !input.trim()}>
            {loading ? '送信中...' : '送信'}
          </Button>
        </div>
      </form>
    </div>
  )
}

データベーススキーマ

-- チャット会話
CREATE TABLE conversations (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES users(id),
  title VARCHAR(255),
  model VARCHAR(50) NOT NULL,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

-- チャットメッセージ
CREATE TABLE chat_messages (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  conversation_id UUID REFERENCES conversations(id),
  role VARCHAR(20) NOT NULL, -- 'user', 'assistant', 'system'
  content TEXT NOT NULL,
  tokens_used INTEGER,
  created_at TIMESTAMP DEFAULT NOW()
);

-- API使用量記録
CREATE TABLE api_usage (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES users(id),
  model VARCHAR(50) NOT NULL,
  tokens_used INTEGER NOT NULL,
  cost_usd DECIMAL(10, 6) NOT NULL,
  endpoint VARCHAR(100) NOT NULL,
  created_at TIMESTAMP DEFAULT NOW()
);

🧪 テスト戦略

  1. API統合テスト

    • OpenAI API接続テスト
    • レスポンス形式検証
    • エラーハンドリングテスト
  2. ユニットテスト

    • チャット機能
    • メッセージ処理
    • 使用量計算
  3. E2Eテスト

    • 完全なチャットフロー
    • モデル切り替え
    • 会話履歴管理

📚 完了条件

  • OpenAI API統合完了
  • GPT-4、GPT-3.5-turboサポート
  • ストリーミングレスポンス実装
  • チャットインターフェース完成
  • 会話履歴機能
  • モデル切り替え機能
  • 使用量トラッキング
  • エラーハンドリング
  • 日本語最適化
  • セキュリティ実装
  • パフォーマンス最適化

🔗 関連タスク

有効化するタスク:

  • マルチモーダルAIチャットインターフェース
  • 使用量追跡と課金システム
  • AIワークフロー自動化システム

📝 セキュリティ考慮事項

  • API キーの安全な管理
  • レート制限実装
  • 入力サニタイゼーション
  • 使用量制限
  • コンテンツフィルタリング
  • CORS設定

💰 コスト管理

  • トークン使用量監視
  • ユーザー別使用量制限
  • コスト計算とアラート
  • 無料枠と有料プラン管理

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions