fix(conversation): implement proper tool loop for Claude API
- Fix streaming JSON parsing for tool inputs by accumulating partial JSON and parsing only on content_block_stop - Implement proper tool loop to continue conversation after tool execution - Send tool results back to Claude to get final response - Add safety limit of 10 iterations for tool loops This fixes the issue where AI responses were truncated after using tools like search_knowledge. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
141b45bec2
commit
72e67fa5d9
|
|
@ -69,7 +69,7 @@ export class ClaudeAgentService implements OnModuleInit {
|
|||
}
|
||||
|
||||
/**
|
||||
* Send a message and get streaming response
|
||||
* Send a message and get streaming response with tool loop support
|
||||
*/
|
||||
async *sendMessage(
|
||||
message: string,
|
||||
|
|
@ -97,6 +97,13 @@ export class ClaudeAgentService implements OnModuleInit {
|
|||
content: message,
|
||||
});
|
||||
|
||||
// Tool loop - continue until we get a final response (no tool use)
|
||||
const maxIterations = 10; // Safety limit
|
||||
let iterations = 0;
|
||||
|
||||
while (iterations < maxIterations) {
|
||||
iterations++;
|
||||
|
||||
try {
|
||||
// Create streaming message
|
||||
const stream = await this.client.messages.stream({
|
||||
|
|
@ -110,69 +117,130 @@ export class ClaudeAgentService implements OnModuleInit {
|
|||
let currentToolUse: {
|
||||
id: string;
|
||||
name: string;
|
||||
inputJson: string;
|
||||
input: Record<string, unknown>;
|
||||
} | null = null;
|
||||
|
||||
// Collect all tool uses and text blocks in this response
|
||||
const toolUses: Array<{ id: string; name: string; input: Record<string, unknown> }> = [];
|
||||
const assistantContent: Anthropic.ContentBlockParam[] = [];
|
||||
let hasText = false;
|
||||
|
||||
for await (const event of stream) {
|
||||
if (event.type === 'content_block_start') {
|
||||
if (event.content_block.type === 'tool_use') {
|
||||
currentToolUse = {
|
||||
id: event.content_block.id,
|
||||
name: event.content_block.name,
|
||||
inputJson: '',
|
||||
input: {},
|
||||
};
|
||||
}
|
||||
} else if (event.type === 'content_block_delta') {
|
||||
if (event.delta.type === 'text_delta') {
|
||||
hasText = true;
|
||||
yield {
|
||||
type: 'text',
|
||||
content: event.delta.text,
|
||||
};
|
||||
} else if (event.delta.type === 'input_json_delta' && currentToolUse) {
|
||||
// Accumulate tool input
|
||||
try {
|
||||
const partialInput = JSON.parse(event.delta.partial_json || '{}');
|
||||
currentToolUse.input = {
|
||||
...currentToolUse.input,
|
||||
...partialInput,
|
||||
};
|
||||
} catch {
|
||||
// Ignore parse errors for partial JSON
|
||||
}
|
||||
currentToolUse.inputJson += event.delta.partial_json || '';
|
||||
}
|
||||
} else if (event.type === 'content_block_stop') {
|
||||
if (currentToolUse) {
|
||||
// Parse the complete accumulated JSON
|
||||
try {
|
||||
currentToolUse.input = JSON.parse(currentToolUse.inputJson || '{}');
|
||||
} catch (e) {
|
||||
console.error('Failed to parse tool input JSON:', currentToolUse.inputJson, e);
|
||||
currentToolUse.input = {};
|
||||
}
|
||||
|
||||
toolUses.push({
|
||||
id: currentToolUse.id,
|
||||
name: currentToolUse.name,
|
||||
input: currentToolUse.input,
|
||||
});
|
||||
|
||||
yield {
|
||||
type: 'tool_use',
|
||||
toolName: currentToolUse.name,
|
||||
toolInput: currentToolUse.input,
|
||||
};
|
||||
|
||||
// Execute the tool
|
||||
const toolResult = await this.immigrationToolsService.executeTool(
|
||||
currentToolUse.name,
|
||||
currentToolUse.input,
|
||||
currentToolUse = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no tool uses, we're done
|
||||
if (toolUses.length === 0) {
|
||||
yield { type: 'end' };
|
||||
return;
|
||||
}
|
||||
|
||||
// Build assistant message content with tool uses
|
||||
// First get the final message to extract text content
|
||||
const finalMessage = await stream.finalMessage();
|
||||
for (const block of finalMessage.content) {
|
||||
if (block.type === 'text') {
|
||||
assistantContent.push({ type: 'text', text: block.text });
|
||||
} else if (block.type === 'tool_use') {
|
||||
assistantContent.push({
|
||||
type: 'tool_use',
|
||||
id: block.id,
|
||||
name: block.name,
|
||||
input: block.input as Record<string, unknown>,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add assistant message with tool uses
|
||||
messages.push({
|
||||
role: 'assistant',
|
||||
content: assistantContent,
|
||||
});
|
||||
|
||||
// Execute all tools and collect results
|
||||
const toolResults: Anthropic.ToolResultBlockParam[] = [];
|
||||
for (const toolUse of toolUses) {
|
||||
const result = await this.immigrationToolsService.executeTool(
|
||||
toolUse.name,
|
||||
toolUse.input,
|
||||
context,
|
||||
);
|
||||
|
||||
yield {
|
||||
type: 'tool_result',
|
||||
toolName: currentToolUse.name,
|
||||
toolResult,
|
||||
toolName: toolUse.name,
|
||||
toolResult: result,
|
||||
};
|
||||
|
||||
currentToolUse = null;
|
||||
}
|
||||
} else if (event.type === 'message_stop') {
|
||||
yield { type: 'end' };
|
||||
}
|
||||
toolResults.push({
|
||||
type: 'tool_result',
|
||||
tool_use_id: toolUse.id,
|
||||
content: JSON.stringify(result),
|
||||
});
|
||||
}
|
||||
|
||||
// Add user message with tool results
|
||||
messages.push({
|
||||
role: 'user',
|
||||
content: toolResults,
|
||||
});
|
||||
|
||||
// Continue the loop to get Claude's response after tool execution
|
||||
|
||||
} catch (error) {
|
||||
console.error('Claude API error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
console.error('Tool loop exceeded maximum iterations');
|
||||
yield { type: 'end' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-streaming message for simple queries
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in New Issue