From ffd310c9038dfc37cfddff11f10722feb6b711d4 Mon Sep 17 00:00:00 2001 From: kappa Date: Thu, 5 Feb 2026 11:34:15 +0900 Subject: [PATCH] feat: add OpenAI types and AI caller utility - Consolidate OpenAI types to types.ts - Create reusable callOpenAI() function - Add tool call parsing and result handling Co-Authored-By: Claude Opus 4.5 --- src/types.ts | 44 ++++++++++++-- src/utils/ai-caller.ts | 127 +++++++++++++++++++++++++++++++++++++++++ src/utils/index.ts | 15 +++++ 3 files changed, 181 insertions(+), 5 deletions(-) create mode 100644 src/utils/ai-caller.ts create mode 100644 src/utils/index.ts diff --git a/src/types.ts b/src/types.ts index 5f0a538..7a8e043 100644 --- a/src/types.ts +++ b/src/types.ts @@ -466,8 +466,11 @@ export interface BraveSearchResponse { }; } -// OpenAI API 응답 타입 -export interface ToolCall { +// ============================================ +// OpenAI API Types (for Function Calling) +// ============================================ + +export interface OpenAIToolCall { id: string; type: 'function'; function: { @@ -479,19 +482,50 @@ export interface ToolCall { export interface OpenAIMessage { role: 'system' | 'user' | 'assistant' | 'tool'; content: string | null; - tool_calls?: ToolCall[]; + tool_calls?: OpenAIToolCall[]; tool_call_id?: string; name?: string; // For tool responses } export interface OpenAIChoice { message: OpenAIMessage; + finish_reason: string; } -export interface OpenAIResponse { - choices?: OpenAIChoice[]; +export interface OpenAIAPIResponse { + choices: OpenAIChoice[]; } +export interface ToolDefinition { + type: 'function'; + function: { + name: string; + description: string; + parameters: { + type: 'object'; + properties: Record; + required?: string[]; + }; + }; +} + +// Parsed tool call result +export interface ParsedToolCall { + name: string; + arguments: Record; +} + +// AI caller result +export interface AICallerResult { + response: string; + toolCalls?: ParsedToolCall[]; + finishReason?: string; +} + +// Legacy type alias for backward compatibility +export type ToolCall = OpenAIToolCall; +export type OpenAIResponse = OpenAIAPIResponse; + // Context7 API 응답 타입 export interface Context7Library { id: string; diff --git a/src/utils/ai-caller.ts b/src/utils/ai-caller.ts new file mode 100644 index 0000000..1071f1a --- /dev/null +++ b/src/utils/ai-caller.ts @@ -0,0 +1,127 @@ +import { createLogger } from './logger'; +import { AI_CONFIG } from '../constants/agent-config'; +import type { + Env, + OpenAIAPIResponse, + OpenAIToolCall, + ToolDefinition, + AICallerResult, + ParsedToolCall +} from '../types'; + +const logger = createLogger('ai-caller'); + +export interface AICallerConfig { + model?: string; + maxTokens: number; + temperature: number; + maxToolCalls?: number; + responseFormat?: { type: 'json_object' } | { type: 'text' }; +} + +export interface ChatMessage { + role: 'system' | 'user' | 'assistant' | 'tool'; + content: string; + tool_call_id?: string; +} + +/** + * Call OpenAI API with optional function calling support + */ +export async function callOpenAI( + messages: ChatMessage[], + tools: ToolDefinition[] | undefined, + config: AICallerConfig, + env: Env +): Promise { + const model = config.model || AI_CONFIG.model; + const maxToolCalls = config.maxToolCalls ?? AI_CONFIG.maxToolCalls; + + try { + const requestBody: Record = { + model, + messages, + max_tokens: config.maxTokens, + temperature: config.temperature, + }; + + if (tools && tools.length > 0) { + requestBody.tools = tools; + requestBody.tool_choice = 'auto'; + } + + if (config.responseFormat) { + requestBody.response_format = config.responseFormat; + } + + const response = await fetch(`${env.OPENAI_API_BASE}/chat/completions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${env.OPENAI_API_KEY}`, + }, + body: JSON.stringify(requestBody), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`OpenAI API error: ${response.status} - ${errorText}`); + } + + const data = await response.json() as OpenAIAPIResponse; + const choice = data.choices[0]; + + if (!choice) { + throw new Error('No response from OpenAI'); + } + + const message = choice.message; + const result: AICallerResult = { + response: message.content || '', + finishReason: choice.finish_reason, + }; + + // Parse tool calls if present + if (message.tool_calls && message.tool_calls.length > 0) { + result.toolCalls = message.tool_calls + .slice(0, maxToolCalls) + .map(parseToolCall) + .filter((tc): tc is ParsedToolCall => tc !== null); + } + + return result; + } catch (error) { + logger.error('OpenAI API 호출 실패', error as Error); + throw error; + } +} + +/** + * Parse a single tool call from OpenAI response + */ +function parseToolCall(toolCall: OpenAIToolCall): ParsedToolCall | null { + try { + return { + name: toolCall.function.name, + arguments: JSON.parse(toolCall.function.arguments), + }; + } catch (error) { + logger.error('도구 호출 파싱 실패', error as Error, { toolCall }); + return null; + } +} + +/** + * Execute tool calls and get results + * Returns array of tool results for follow-up API call + */ +export function createToolResultMessages( + toolCalls: OpenAIToolCall[], + results: string[] +): ChatMessage[] { + return toolCalls.map((tc, index) => ({ + role: 'tool' as const, + tool_call_id: tc.id, + content: results[index] || 'Error executing tool', + })); +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..834b400 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,15 @@ +export * from './logger'; +export * from './session-manager'; +export * from './ai-caller'; +export * from './formatters'; +export * from './patterns'; +export * from './retry'; +export * from './circuit-breaker'; +export * from './metrics'; +export * from './optimistic-lock'; +export * from './api-helper'; +export * from './api-urls'; +export * from './env-validation'; +export * from './error'; +export * from './email-decoder'; +export * from './reconciliation';