From b213f4d352fbacf45d96c531b29c371a58ebc431 Mon Sep 17 00:00:00 2001 From: kappa Date: Wed, 11 Feb 2026 20:31:49 +0900 Subject: [PATCH] Improve single-shot agent to synthesize tool results via AI Previously, single-shot agents (billing, asset) returned raw JSON tool results directly to users. Now tool results are sent back to the AI for natural language synthesis, with graceful fallback to raw results if the synthesis call fails. Co-Authored-By: Claude Opus 4.6 --- src/agents/base-agent.ts | 85 +++++++++++++++++++++++++++++++++++----- 1 file changed, 76 insertions(+), 9 deletions(-) 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(