株主対話デモアプリケーションにおける Azure OpenAI との連携方法について詳細に説明します。本ガイドでは、API の設定から実装、トラブルシューティングまでを包括的にカバーします。
# Azure CLI での作成例
az cognitiveservices account create \
--name "your-openai-resource" \
--resource-group "your-rg" \
--location "East US" \
--kind "OpenAI" \
--sku "S0"
gpt-4o-mini
(推奨)必要な情報:
エンドポイント: https://your-resource.openai.azure.com/
APIキー: リソースの「キーとエンドポイント」から取得
デプロイ名: 上記で設定したデプロイ名
APIバージョン: 2024-02-15-preview
POST https://{resource-name}.openai.azure.com/openai/deployments/{deployment-name}/chat/completions?api-version={api-version}
const headers = {
'Content-Type': 'application/json',
'api-key': 'YOUR_API_KEY'
};
const requestBody = {
messages: [
{
role: 'system',
content: 'あなたは経験豊富な個人株主です。提供された決算資料を基に質問を生成してください。'
},
{
role: 'user',
content: 'PDFから抽出されたテキスト内容'
}
],
max_tokens: 500,
temperature: 0.7,
top_p: 0.9,
frequency_penalty: 0,
presence_penalty: 0
};
{
"id": "chatcmpl-ABCDEF",
"object": "chat.completion",
"created": 1677652288,
"model": "gpt-4o-mini",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "生成された質問や回答テキスト"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 150,
"completion_tokens": 100,
"total_tokens": 250
}
}
class AzureOpenAIClient {
constructor(config) {
this.endpoint = config.endpoint;
this.apiKey = config.apiKey;
this.deploymentName = config.deploymentName;
this.apiVersion = config.apiVersion;
}
async callChatCompletion(messages, options = {}) {
const url = `${this.endpoint}/openai/deployments/${this.deploymentName}/chat/completions?api-version=${this.apiVersion}`;
const requestBody = {
messages: messages,
max_tokens: options.maxTokens || 500,
temperature: options.temperature || 0.7,
top_p: options.topP || 0.9,
frequency_penalty: options.frequencyPenalty || 0,
presence_penalty: options.presencePenalty || 0
};
console.log('🚀 Azure OpenAI API呼び出し開始:', url);
console.log('📝 リクエストボディ:', JSON.stringify(requestBody, null, 2));
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'api-key': this.apiKey
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
const errorData = await response.text();
throw new Error(`API Error ${response.status}: ${errorData}`);
}
const data = await response.json();
console.log('✅ API呼び出し成功:', data);
return data;
} catch (error) {
console.error('❌ API呼び出しエラー:', error);
throw error;
}
}
async testConnection() {
try {
const testMessages = [
{
role: 'system',
content: 'テスト用のメッセージです。'
},
{
role: 'user',
content: '接続テストを実行してください。'
}
];
const response = await this.callChatCompletion(testMessages, {
maxTokens: 50
});
return {
success: true,
message: '接続テスト成功',
usage: response.usage
};
} catch (error) {
return {
success: false,
message: `接続テスト失敗: ${error.message}`,
error: error
};
}
}
}
class DialogueGenerator {
constructor(azureClient, languageConfig) {
this.client = azureClient;
this.languageConfig = languageConfig;
}
async generateShareholderQuestion(pdfContent, language = 'ja', turnNumber = 1) {
const systemPrompt = this.languageConfig[language].shareholderPrompt;
const contextPrompt = this.buildContextPrompt(pdfContent, turnNumber, 'shareholder');
const messages = [
{
role: 'system',
content: systemPrompt
},
{
role: 'user',
content: contextPrompt
}
];
console.log(`👤 株主質問生成開始 (ターン${turnNumber}, 言語: ${language})`);
try {
const response = await this.client.callChatCompletion(messages, {
maxTokens: 300,
temperature: 0.7
});
const question = response.choices[0].message.content;
console.log('✅ 株主質問生成完了:', question);
return {
content: question,
tokens: response.usage.total_tokens,
turnNumber: turnNumber
};
} catch (error) {
console.error('❌ 株主質問生成エラー:', error);
throw new Error(`株主質問の生成に失敗しました: ${error.message}`);
}
}
async generateDirectorResponse(question, pdfContent, language = 'ja', turnNumber = 1) {
const systemPrompt = this.languageConfig[language].directorPrompt;
const contextPrompt = this.buildResponseContextPrompt(question, pdfContent, turnNumber);
const messages = [
{
role: 'system',
content: systemPrompt
},
{
role: 'user',
content: contextPrompt
}
];
console.log(`👔 取締役回答生成開始 (ターン${turnNumber}, 言語: ${language})`);
try {
const response = await this.client.callChatCompletion(messages, {
maxTokens: 400,
temperature: 0.6
});
const answer = response.choices[0].message.content;
console.log('✅ 取締役回答生成完了:', answer);
return {
content: answer,
tokens: response.usage.total_tokens,
turnNumber: turnNumber
};
} catch (error) {
console.error('❌ 取締役回答生成エラー:', error);
throw new Error(`取締役回答の生成に失敗しました: ${error.message}`);
}
}
async generateSummary(dialogueHistory, language = 'ja') {
const systemPrompt = this.languageConfig[language].summaryPrompt;
const dialogueText = this.formatDialogueForSummary(dialogueHistory);
const messages = [
{
role: 'system',
content: systemPrompt
},
{
role: 'user',
content: dialogueText
}
];
console.log(`📋 対話要約生成開始 (言語: ${language})`);
try {
const response = await this.client.callChatCompletion(messages, {
maxTokens: 600,
temperature: 0.5
});
const summary = response.choices[0].message.content;
console.log('✅ 対話要約生成完了:', summary);
return {
content: summary,
tokens: response.usage.total_tokens
};
} catch (error) {
console.error('❌ 対話要約生成エラー:', error);
throw new Error(`対話要約の生成に失敗しました: ${error.message}`);
}
}
buildContextPrompt(pdfContent, turnNumber, role) {
const truncatedContent = this.truncateContent(pdfContent, 3000);
return `
決算資料・株主総会資料:
${truncatedContent}
対話ターン: ${turnNumber}/5
役割: ${role}
上記の資料を参考に、${role === 'shareholder' ? '株主として適切な質問' : '取締役として適切な回答'}を生成してください。
`.trim();
}
buildResponseContextPrompt(question, pdfContent, turnNumber) {
const truncatedContent = this.truncateContent(pdfContent, 2500);
return `
株主からの質問:
${question}
決算資料・株主総会資料:
${truncatedContent}
対話ターン: ${turnNumber}/5
上記の質問に対して、提供された資料を基に取締役として適切な回答をしてください。
`.trim();
}
formatDialogueForSummary(dialogueHistory) {
return dialogueHistory.map((entry, index) => {
const turnNumber = Math.floor(index / 2) + 1;
const role = entry.role === 'shareholder' ? '株主' : '取締役';
return `ターン${turnNumber} - ${role}: ${entry.content}`;
}).join('\n\n');
}
truncateContent(content, maxLength) {
if (content.length <= maxLength) {
return content;
}
return content.substring(0, maxLength) + '...(以下省略)';
}
}
class ConfigManager {
static saveConfig(config) {
try {
// 設定をBase64エンコードして保存(簡易暗号化)
const encodedConfig = btoa(JSON.stringify(config));
localStorage.setItem('azureOpenAIConfig', encodedConfig);
console.log('💾 Azure OpenAI設定を保存しました');
return true;
} catch (error) {
console.error('❌ 設定保存エラー:', error);
return false;
}
}
static loadConfig() {
try {
const encodedConfig = localStorage.getItem('azureOpenAIConfig');
if (!encodedConfig) {
return null;
}
const config = JSON.parse(atob(encodedConfig));
console.log('📂 Azure OpenAI設定を読み込みました');
// APIキーをマスクしてログ出力
const maskedConfig = {
...config,
apiKey: config.apiKey ? '***masked***' : null
};
console.log('🔍 読み込んだ設定:', maskedConfig);
return config;
} catch (error) {
console.error('❌ 設定読み込みエラー:', error);
return null;
}
}
static validateConfig(config) {
const required = ['endpoint', 'apiKey', 'deploymentName', 'apiVersion'];
const missing = required.filter(field => !config[field]);
if (missing.length > 0) {
throw new Error(`必須項目が不足しています: ${missing.join(', ')}`);
}
// エンドポイントのフォーマット検証
const endpointPattern = /^https:\/\/[\w-]+\.openai\.azure\.com\/?$/;
if (!endpointPattern.test(config.endpoint)) {
throw new Error('エンドポイントの形式が正しくありません');
}
// APIバージョンの検証
const supportedVersions = ['2023-12-01-preview', '2024-02-15-preview', '2024-04-01-preview'];
if (!supportedVersions.includes(config.apiVersion)) {
console.warn('⚠️ サポートされていないAPIバージョンの可能性があります:', config.apiVersion);
}
return true;
}
static clearConfig() {
localStorage.removeItem('azureOpenAIConfig');
console.log('🗑️ Azure OpenAI設定をクリアしました');
}
}
// エラー例
{
"error": {
"code": "Unauthorized",
"message": "Access denied due to invalid subscription key."
}
}
// 対処法
function handleAuthError(error) {
console.error('🔐 認証エラー:', error);
// ユーザーに分かりやすいメッセージを表示
showError('APIキーが無効です。Azure ポータルで正しいキーを確認してください。');
// 設定画面への誘導
expandConnectionSettings();
}
// エラー例
{
"error": {
"code": "TooManyRequests",
"message": "Rate limit exceeded. Please wait before making more requests."
}
}
// 対処法
function handleRateLimitError(error, retryAfter = 60) {
console.warn(`⏳ レート制限エラー: ${retryAfter}秒後にリトライします`);
showWarning(`リクエスト頻度が高すぎます。${retryAfter}秒後に自動的にリトライします。`);
// 指数バックオフでリトライ
setTimeout(() => {
retryLastRequest();
}, retryAfter * 1000);
}
// エラー例
{
"error": {
"code": "content_filter",
"message": "The response was filtered due to the prompt triggering Azure OpenAI's content management policy."
}
}
// 対処法
function handleContentFilterError(error) {
console.warn('🛡️ コンテンツフィルターエラー:', error);
showWarning('生成されたコンテンツがポリシーに適合しませんでした。プロンプトを調整して再試行します。');
// より保守的なプロンプトで再試行
retryWithConservativePrompt();
}
// エラー例
{
"error": {
"code": "context_length_exceeded",
"message": "The request exceeds the maximum number of tokens."
}
}
// 対処法
function handleTokenLimitError(error) {
console.warn('📝 トークン制限エラー:', error);
showWarning('入力テキストが長すぎます。コンテンツを要約して再試行します。');
// コンテンツを短縮して再試行
retryWithTruncatedContent();
}
class RetryHandler {
static async withRetry(apiCall, maxRetries = 3, baseDelay = 1000) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
console.log(`🔄 API呼び出し試行 ${attempt}/${maxRetries}`);
return await apiCall();
} catch (error) {
lastError = error;
if (attempt === maxRetries) {
break;
}
// 指数バックオフ
const delay = baseDelay * Math.pow(2, attempt - 1);
console.log(`⏳ ${delay}ms後にリトライします`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
}
// 使用例
const result = await RetryHandler.withRetry(async () => {
return await azureClient.callChatCompletion(messages);
}, 3, 1000);
class PromptOptimizer {
static optimizePrompt(basePrompt, content, maxTokens = 4000) {
// コンテンツの長さを推定(おおよそ1トークン = 4文字)
const estimatedTokens = (basePrompt.length + content.length) / 4;
if (estimatedTokens <= maxTokens) {
return basePrompt + '\n\n' + content;
}
// コンテンツを適切な長さに切り詰める
const availableTokensForContent = maxTokens - (basePrompt.length / 4);
const maxContentLength = availableTokensForContent * 4 * 0.9; // 安全マージン
const truncatedContent = content.length > maxContentLength
? content.substring(0, maxContentLength) + '\n\n[以下省略...]'
: content;
console.log(`📝 プロンプト最適化: ${content.length}文字 → ${truncatedContent.length}文字`);
return basePrompt + '\n\n' + truncatedContent;
}
static generateEfficientPrompt(role, context, language) {
const prompts = {
shareholder: {
ja: '株主として決算資料を基に質問を1つ生成してください。',
en: 'Generate one question as a shareholder based on financial documents.'
},
director: {
ja: '取締役として以下の質問に回答してください。',
en: 'Respond to the following question as a director.'
}
};
return prompts[role][language] || prompts[role]['ja'];
}
}
class ResponseCache {
static cache = new Map();
static maxCacheSize = 50;
static generateKey(prompt, options) {
return btoa(JSON.stringify({ prompt, options }));
}
static get(prompt, options) {
const key = this.generateKey(prompt, options);
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < 3600000) { // 1時間有効
console.log('💾 キャッシュからレスポンスを取得');
return cached.response;
}
return null;
}
static set(prompt, options, response) {
const key = this.generateKey(prompt, options);
// キャッシュサイズ制限
if (this.cache.size >= this.maxCacheSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, {
response,
timestamp: Date.now()
});
console.log('💾 レスポンスをキャッシュに保存');
}
static clear() {
this.cache.clear();
console.log('🗑️ レスポンスキャッシュをクリア');
}
}
class UsageMonitor {
static usage = {
totalTokens: 0,
totalCost: 0,
apiCalls: 0,
startTime: Date.now()
};
static recordUsage(tokens, modelType = 'gpt-4o-mini') {
this.usage.totalTokens += tokens;
this.usage.apiCalls++;
// 料金計算(概算)
const rates = {
'gpt-4o-mini': 0.0001 // $0.0001 per 1K tokens
};
const cost = (tokens / 1000) * (rates[modelType] || 0.0001);
this.usage.totalCost += cost;
console.log(`📊 使用量更新: ${tokens}トークン, 累計コスト: $${this.usage.totalCost.toFixed(4)}`);
}
static getUsageReport() {
const duration = Date.now() - this.usage.startTime;
const hours = duration / (1000 * 60 * 60);
return {
...this.usage,
sessionDuration: `${hours.toFixed(2)}時間`,
averageTokensPerCall: Math.round(this.usage.totalTokens / this.usage.apiCalls) || 0
};
}
static checkUsageLimits() {
const report = this.getUsageReport();
if (report.totalCost > 10) { // $10制限
console.warn('⚠️ 使用量制限に近づいています:', report);
return false;
}
return true;
}
}
class APITester {
static async runFullTest(azureClient) {
console.log('🧪 Azure OpenAI API 総合テスト開始');
const tests = [
this.testConnection.bind(this),
this.testBasicCompletion.bind(this),
this.testJapaneseCompletion.bind(this),
this.testLongPrompt.bind(this),
this.testErrorHandling.bind(this)
];
const results = [];
for (const test of tests) {
try {
const result = await test(azureClient);
results.push(result);
console.log(`✅ テスト合格: ${result.name}`);
} catch (error) {
results.push({
name: test.name,
success: false,
error: error.message
});
console.error(`❌ テスト失敗: ${test.name} - ${error.message}`);
}
}
return results;
}
static async testConnection(client) {
const result = await client.testConnection();
if (!result.success) {
throw new Error(result.message);
}
return { name: '接続テスト', success: true, usage: result.usage };
}
static async testBasicCompletion(client) {
const messages = [
{ role: 'system', content: 'あなたは親切なアシスタントです。' },
{ role: 'user', content: '「こんにちは」と日本語で返答してください。' }
];
const response = await client.callChatCompletion(messages, { maxTokens: 50 });
if (!response.choices || response.choices.length === 0) {
throw new Error('レスポンスが空です');
}
return { name: '基本補完テスト', success: true, response: response.choices[0].message.content };
}
static async testJapaneseCompletion(client) {
const messages = [
{ role: 'system', content: '株主総会での質問を生成してください。' },
{ role: 'user', content: '売上高が前年比10%増加した理由について質問してください。' }
];
const response = await client.callChatCompletion(messages, { maxTokens: 100 });
const content = response.choices[0].message.content;
// 日本語が含まれているかチェック
if (!/[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]/.test(content)) {
throw new Error('日本語レスポンスが生成されませんでした');
}
return { name: '日本語補完テスト', success: true, response: content };
}
}
class SecureConfigManager {
static encrypt(text, password) {
// 実際の環境では crypto-js などの適切な暗号化ライブラリを使用
return btoa(text); // 簡易実装
}
static decrypt(encryptedText, password) {
try {
return atob(encryptedText);
} catch {
throw new Error('復号化に失敗しました');
}
}
static sanitizeApiKey(apiKey) {
// APIキーの形式検証
if (!apiKey || typeof apiKey !== 'string') {
throw new Error('APIキーが無効です');
}
if (apiKey.length < 10) {
throw new Error('APIキーが短すぎます');
}
return apiKey.trim();
}
static maskApiKey(apiKey) {
if (!apiKey || apiKey.length < 8) {
return '***';
}
return apiKey.substring(0, 4) + '***' + apiKey.substring(apiKey.length - 4);
}
}
class DataProtection {
static sanitizeInput(input) {
// XSS攻撃の防止
return input
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
static validatePDFContent(content) {
// 不適切なコンテンツの検出
const suspiciousPatterns = [
/<script/i,
/javascript:/i,
/data:text\/html/i
];
for (const pattern of suspiciousPatterns) {
if (pattern.test(content)) {
throw new Error('安全でないコンテンツが検出されました');
}
}
return true;
}
static limitContentSize(content, maxSize = 1000000) { // 1MB制限
if (content.length > maxSize) {
throw new Error(`コンテンツサイズが制限を超えています: ${content.length} > ${maxSize}`);
}
return true;
}
}
問題 | 症状 | 解決策 |
---|---|---|
接続エラー | 「接続テスト」ボタンでエラー | エンドポイントとAPIキーを確認 |
日本語の文字化け | レスポンスが英語のみ | プロンプトに明示的に日本語指定を追加 |
生成速度が遅い | レスポンスに30秒以上かかる | max_tokensを減らす、プロンプトを短縮 |
コンテンツフィルター | 回答が生成されない | より中立的なプロンプトに変更 |
レート制限 | 429エラーが頻発 | リクエスト間隔を広げる、リトライ機能を実装 |
class DebugUtils {
static logAPICall(url, headers, body, response) {
console.group('🔍 API呼び出し詳細');
console.log('URL:', url);
console.log('Headers:', { ...headers, 'api-key': '***masked***' });
console.log('Request Body:', JSON.stringify(body, null, 2));
console.log('Response:', response);
console.groupEnd();
}
static analyzeTokenUsage(prompt, response) {
const usage = response.usage || {};
console.group('📊 トークン使用量分析');
console.log('Prompt Tokens:', usage.prompt_tokens);
console.log('Completion Tokens:', usage.completion_tokens);
console.log('Total Tokens:', usage.total_tokens);
console.log('推定コスト:', `$${(usage.total_tokens * 0.0001 / 1000).toFixed(6)}`);
console.groupEnd();
}
static exportDebugInfo() {
const debugInfo = {
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href,
localStorage: Object.keys(localStorage),
usage: UsageMonitor.getUsageReport(),
lastErrors: this.getLastErrors()
};
const blob = new Blob([JSON.stringify(debugInfo, null, 2)], {
type: 'application/json'
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `debug-info-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
}
}