diff --git a/src/agents/base-agent.ts b/src/agents/base-agent.ts index bb73df6..edbffcb 100644 --- a/src/agents/base-agent.ts +++ b/src/agents/base-agent.ts @@ -97,22 +97,20 @@ export abstract class BaseAgent { return '__PASSTHROUGH__'; } - // 5. Single-shot: execute tool calls returned by AI + // 5. Single-shot: execute tool calls, then synthesize via AI let finalResponse = aiResult.response; if (this.getExecutionStrategy() === 'single-shot' && aiResult.toolCalls && aiResult.toolCalls.length > 0) { - const toolResults: string[] = []; + const toolCallResults: Array<{ name: string; args: Record; result: string }> = []; for (const tc of aiResult.toolCalls) { const result = await this.executeToolCall(tc.name, tc.arguments, session, context); - toolResults.push(result); + toolCallResults.push({ name: tc.name, args: tc.arguments, result }); aiResult.calledTools.push(tc.name); } - if (toolResults.length > 0) { - const cleanAiText = (aiResult.response || '') - .replace('__SESSION_END__', '').trim(); - finalResponse = cleanAiText - ? cleanAiText + '\n\n' + toolResults.join('\n\n') - : toolResults.join('\n\n'); + if (toolCallResults.length > 0 && env.OPENAI_API_KEY) { + finalResponse = await this.synthesizeToolResults( + session, userMessage, aiResult.response, toolCallResults, env + ); } } @@ -149,6 +147,75 @@ export abstract class BaseAgent { } } + // --- Synthesize tool results into natural language --- + + private async synthesizeToolResults( + session: TSession, + userMessage: string, + aiText: string, + toolResults: Array<{ name: string; args: Record; result: string }>, + env: Env + ): Promise { + const log = createLogger(this.agentName); + + try { + const systemPrompt = this.getSystemPrompt(session) + this.buildPromptSuffix(session); + const toolSummary = toolResults + .map(tr => `[${tr.name}] 결과:\n${tr.result}`) + .join('\n\n'); + + const messages = [ + { role: 'system', content: systemPrompt }, + ...session.messages.map(m => ({ + role: m.role === 'user' ? 'user' as const : 'assistant' as const, + content: m.content, + })), + { role: 'user', content: userMessage }, + { + role: 'assistant', + content: aiText || '도구를 호출하여 정보를 조회했습니다.', + }, + { + role: 'user', + content: `위 도구 실행 결과를 바탕으로 고객에게 보기 좋게 안내해주세요. JSON을 그대로 보여주지 말고 자연어로 정리하세요.\n\n${toolSummary}`, + }, + ]; + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 15000); + + try { + const response = await fetch(getOpenAIUrl(env), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${env.OPENAI_API_KEY}`, + }, + signal: controller.signal, + body: JSON.stringify({ + model: AI_CONFIG.model, + messages, + max_tokens: this.getMaxTokens(), + temperature: this.getTemperature(), + }), + }); + + if (!response.ok) { + log.error('Synthesize API error', new Error(`HTTP ${response.status}`)); + return toolResults.map(tr => tr.result).join('\n\n'); + } + + const data = await response.json() as OpenAIAPIResponse; + return data.choices[0].message.content || toolResults.map(tr => tr.result).join('\n\n'); + } finally { + clearTimeout(timeoutId); + } + } catch (error) { + log.error('Tool result synthesis failed', error as Error); + return toolResults.map(tr => tr.result).join('\n\n'); + } + } + // --- AI Call Loop --- protected async callExpertAI(