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 <noreply@anthropic.com>
This commit is contained in:
44
src/types.ts
44
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;
|
id: string;
|
||||||
type: 'function';
|
type: 'function';
|
||||||
function: {
|
function: {
|
||||||
@@ -479,19 +482,50 @@ export interface ToolCall {
|
|||||||
export interface OpenAIMessage {
|
export interface OpenAIMessage {
|
||||||
role: 'system' | 'user' | 'assistant' | 'tool';
|
role: 'system' | 'user' | 'assistant' | 'tool';
|
||||||
content: string | null;
|
content: string | null;
|
||||||
tool_calls?: ToolCall[];
|
tool_calls?: OpenAIToolCall[];
|
||||||
tool_call_id?: string;
|
tool_call_id?: string;
|
||||||
name?: string; // For tool responses
|
name?: string; // For tool responses
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OpenAIChoice {
|
export interface OpenAIChoice {
|
||||||
message: OpenAIMessage;
|
message: OpenAIMessage;
|
||||||
|
finish_reason: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OpenAIResponse {
|
export interface OpenAIAPIResponse {
|
||||||
choices?: OpenAIChoice[];
|
choices: OpenAIChoice[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ToolDefinition {
|
||||||
|
type: 'function';
|
||||||
|
function: {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
parameters: {
|
||||||
|
type: 'object';
|
||||||
|
properties: Record<string, unknown>;
|
||||||
|
required?: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parsed tool call result
|
||||||
|
export interface ParsedToolCall {
|
||||||
|
name: string;
|
||||||
|
arguments: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 응답 타입
|
// Context7 API 응답 타입
|
||||||
export interface Context7Library {
|
export interface Context7Library {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
127
src/utils/ai-caller.ts
Normal file
127
src/utils/ai-caller.ts
Normal file
@@ -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<AICallerResult> {
|
||||||
|
const model = config.model || AI_CONFIG.model;
|
||||||
|
const maxToolCalls = config.maxToolCalls ?? AI_CONFIG.maxToolCalls;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const requestBody: Record<string, unknown> = {
|
||||||
|
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',
|
||||||
|
}));
|
||||||
|
}
|
||||||
15
src/utils/index.ts
Normal file
15
src/utils/index.ts
Normal file
@@ -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';
|
||||||
Reference in New Issue
Block a user