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 <noreply@anthropic.com>
This commit is contained in:
@@ -97,22 +97,20 @@ export abstract class BaseAgent<TSession extends BaseSession> {
|
||||
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<string, unknown>; 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<TSession extends BaseSession> {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Synthesize tool results into natural language ---
|
||||
|
||||
private async synthesizeToolResults(
|
||||
session: TSession,
|
||||
userMessage: string,
|
||||
aiText: string,
|
||||
toolResults: Array<{ name: string; args: Record<string, unknown>; result: string }>,
|
||||
env: Env
|
||||
): Promise<string> {
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user