import type { Env } from './types'; import { tools, selectToolsForMessage, executeTool } from './tools'; import { retryWithBackoff, RetryError } from './utils/retry'; import { CircuitBreaker, CircuitBreakerError } from './utils/circuit-breaker'; // Cloudflare AI Gateway를 통해 OpenAI API 호출 (지역 제한 우회) const OPENAI_API_URL = 'https://gateway.ai.cloudflare.com/v1/d8e5997eb4040f8b489f09095c0f623c/telegram-bot/openai/chat/completions'; // Circuit Breaker 인스턴스 (전역 공유) const openaiCircuitBreaker = new CircuitBreaker({ failureThreshold: 3, // 3회 연속 실패 시 차단 resetTimeoutMs: 30000, // 30초 후 복구 시도 monitoringWindowMs: 60000 // 1분 윈도우 }); interface OpenAIMessage { role: 'system' | 'user' | 'assistant' | 'tool'; content: string | null; tool_calls?: ToolCall[]; tool_call_id?: string; } interface ToolCall { id: string; type: 'function'; function: { name: string; arguments: string; }; } interface OpenAIResponse { choices: { message: OpenAIMessage; finish_reason: string; }[]; } // OpenAI API 호출 (retry + circuit breaker 적용) async function callOpenAI( apiKey: string, messages: OpenAIMessage[], selectedTools?: typeof tools // undefined = 도구 없음, 배열 = 해당 도구만 사용 ): Promise { return await retryWithBackoff( async () => { const response = await fetch(OPENAI_API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, }, body: JSON.stringify({ model: 'gpt-4o-mini', messages, tools: selectedTools?.length ? selectedTools : undefined, tool_choice: selectedTools?.length ? 'auto' : undefined, max_tokens: 1000, }), }); if (!response.ok) { const error = await response.text(); throw new Error(`OpenAI API error: ${response.status} - ${error}`); } return response.json(); }, { maxRetries: 3, initialDelayMs: 1000, maxDelayMs: 10000, } ); } // 메인 응답 생성 함수 export async function generateOpenAIResponse( env: Env, userMessage: string, systemPrompt: string, recentContext: { role: 'user' | 'assistant'; content: string }[], telegramUserId?: string, db?: D1Database ): Promise { if (!env.OPENAI_API_KEY) { throw new Error('OPENAI_API_KEY not configured'); } const apiKey = env.OPENAI_API_KEY; // TypeScript 타입 안정성을 위해 변수 저장 try { // Circuit Breaker로 전체 실행 감싸기 return await openaiCircuitBreaker.execute(async () => { const messages: OpenAIMessage[] = [ { role: 'system', content: systemPrompt }, ...recentContext.map((m) => ({ role: m.role as 'user' | 'assistant', content: m.content, })), { role: 'user', content: userMessage }, ]; // 동적 도구 선택 const selectedTools = selectToolsForMessage(userMessage); // 첫 번째 호출 let response = await callOpenAI(apiKey, messages, selectedTools); let assistantMessage = response.choices[0].message; console.log('[OpenAI] tool_calls:', assistantMessage.tool_calls ? JSON.stringify(assistantMessage.tool_calls.map(t => ({ name: t.function.name, args: t.function.arguments }))) : 'none'); console.log('[OpenAI] content:', assistantMessage.content?.slice(0, 100)); // Function Calling 처리 (최대 3회 반복) let iterations = 0; while (assistantMessage.tool_calls && iterations < 3) { iterations++; // 도구 호출 결과 수집 const toolResults: OpenAIMessage[] = []; for (const toolCall of assistantMessage.tool_calls) { const args = JSON.parse(toolCall.function.arguments); const result = await executeTool(toolCall.function.name, args, env, telegramUserId, db); // __KEYBOARD__ 마커가 있으면 AI 재해석 없이 바로 반환 (버튼 보존) if (result.includes('__KEYBOARD__')) { return result; } toolResults.push({ role: 'tool', tool_call_id: toolCall.id, content: result, }); } // 대화에 추가 messages.push({ role: 'assistant', content: assistantMessage.content, tool_calls: assistantMessage.tool_calls, }); messages.push(...toolResults); // 다시 호출 (도구 없이 응답 생성) response = await callOpenAI(apiKey, messages, undefined); assistantMessage = response.choices[0].message; } const finalResponse = assistantMessage.content || '응답을 생성할 수 없습니다.'; return finalResponse; }); } catch (error) { // 에러 처리 if (error instanceof CircuitBreakerError) { console.error('[OpenAI] Circuit breaker open:', error.message); return '죄송합니다. 일시적으로 서비스를 이용할 수 없습니다. 잠시 후 다시 시도해주세요.'; } if (error instanceof RetryError) { console.error('[OpenAI] All retry attempts failed:', error.message); return '죄송합니다. AI 응답 생성에 실패했습니다. 잠시 후 다시 시도해주세요.'; } // 기타 에러 console.error('[OpenAI] Unexpected error:', error); return '죄송합니다. 예상치 못한 오류가 발생했습니다.'; } } // 프로필 생성용 (도구 없이) export async function generateProfileWithOpenAI( env: Env, prompt: string ): Promise { if (!env.OPENAI_API_KEY) { throw new Error('OPENAI_API_KEY not configured'); } const apiKey = env.OPENAI_API_KEY; // TypeScript 타입 안정성을 위해 변수 저장 try { // Circuit Breaker로 실행 감싸기 return await openaiCircuitBreaker.execute(async () => { const response = await callOpenAI( apiKey, [{ role: 'user', content: prompt }], undefined // 도구 없이 호출 ); return response.choices[0].message.content || '프로필 생성 실패'; }); } catch (error) { // 에러 처리 if (error instanceof CircuitBreakerError) { console.error('[OpenAI Profile] Circuit breaker open:', error.message); return '프로필 생성 실패: 일시적으로 서비스를 이용할 수 없습니다.'; } if (error instanceof RetryError) { console.error('[OpenAI Profile] All retry attempts failed:', error.message); return '프로필 생성 실패: 재시도 횟수 초과'; } // 기타 에러 console.error('[OpenAI Profile] Unexpected error:', error); return '프로필 생성 실패: 예상치 못한 오류'; } }