Skip to content

Commit 4f2eb3c

Browse files
Kamiel Wanrooijkmile
authored andcommitted
feat: loop until the model does no longer return tool usages
1 parent 75ce0b2 commit 4f2eb3c

File tree

3 files changed

+666
-122
lines changed

3 files changed

+666
-122
lines changed

server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,13 @@ describe('AgenticChatController', () => {
154154
rm: sinon.stub().resolves(),
155155
}
156156

157+
// Add agent with runTool method to testFeatures
158+
testFeatures.agent = {
159+
runTool: sinon.stub().resolves({}),
160+
getTools: sinon.stub().returns([]),
161+
addTool: sinon.stub().resolves(),
162+
}
163+
157164
// @ts-ignore
158165
const cachedInitializeParams: InitializeParams = {
159166
initializationOptions: {
@@ -305,6 +312,326 @@ describe('AgenticChatController', () => {
305312
assert.deepStrictEqual(chatResult, expectedCompleteChatResult)
306313
})
307314

315+
it('handles tool use responses and makes multiple requests', async () => {
316+
// First response includes a tool use request
317+
const mockToolUseId = 'mock-tool-use-id'
318+
const mockToolName = 'mock-tool-name'
319+
const mockToolInput = JSON.stringify({ param1: 'value1' })
320+
const mockToolResult = { result: 'tool execution result' }
321+
322+
const mockToolUseResponseList: ChatResponseStream[] = [
323+
{
324+
messageMetadataEvent: {
325+
conversationId: mockConversationId,
326+
},
327+
},
328+
{
329+
assistantResponseEvent: {
330+
content: 'I need to use a tool. ',
331+
},
332+
},
333+
{
334+
toolUseEvent: {
335+
toolUseId: mockToolUseId,
336+
name: mockToolName,
337+
input: mockToolInput,
338+
stop: true,
339+
},
340+
},
341+
]
342+
343+
// Second response after tool execution
344+
const mockFinalResponseList: ChatResponseStream[] = [
345+
{
346+
messageMetadataEvent: {
347+
conversationId: mockConversationId,
348+
},
349+
},
350+
{
351+
assistantResponseEvent: {
352+
content: 'Hello ',
353+
},
354+
},
355+
{
356+
assistantResponseEvent: {
357+
content: 'World',
358+
},
359+
},
360+
{
361+
assistantResponseEvent: {
362+
content: '!',
363+
},
364+
},
365+
]
366+
367+
// Reset the stub and set up to return different responses on consecutive calls
368+
generateAssistantResponseStub.restore()
369+
generateAssistantResponseStub = sinon.stub(CodeWhispererStreaming.prototype, 'generateAssistantResponse')
370+
371+
generateAssistantResponseStub.onFirstCall().returns(
372+
Promise.resolve({
373+
$metadata: {
374+
requestId: mockMessageId,
375+
},
376+
generateAssistantResponseResponse: createIterableResponse(mockToolUseResponseList),
377+
})
378+
)
379+
380+
generateAssistantResponseStub.onSecondCall().returns(
381+
Promise.resolve({
382+
$metadata: {
383+
requestId: mockMessageId,
384+
},
385+
generateAssistantResponseResponse: createIterableResponse(mockFinalResponseList),
386+
})
387+
)
388+
389+
// Reset the runTool stub
390+
const runToolStub = testFeatures.agent.runTool as sinon.SinonStub
391+
runToolStub.reset()
392+
runToolStub.resolves(mockToolResult)
393+
394+
// Make the request
395+
const chatResultPromise = chatController.onChatPrompt(
396+
{ tabId: mockTabId, prompt: { prompt: 'Hello with tool' } },
397+
mockCancellationToken
398+
)
399+
400+
const chatResult = await chatResultPromise
401+
402+
// Verify that generateAssistantResponse was called twice
403+
sinon.assert.calledTwice(generateAssistantResponseStub)
404+
405+
// Verify that the tool was executed
406+
sinon.assert.calledOnce(runToolStub)
407+
sinon.assert.calledWith(runToolStub, mockToolName, JSON.parse(mockToolInput))
408+
409+
// Verify that the second request included the tool results in the userInputMessageContext
410+
const secondCallArgs = generateAssistantResponseStub.secondCall.args[0]
411+
assert.ok(
412+
secondCallArgs.conversationState?.currentMessage?.userInputMessage?.userInputMessageContext?.toolResults
413+
)
414+
assert.strictEqual(
415+
secondCallArgs.conversationState?.currentMessage?.userInputMessage?.userInputMessageContext?.toolResults
416+
.length,
417+
1
418+
)
419+
assert.strictEqual(
420+
secondCallArgs.conversationState?.currentMessage?.userInputMessage?.userInputMessageContext
421+
?.toolResults[0].toolUseId,
422+
mockToolUseId
423+
)
424+
assert.strictEqual(
425+
secondCallArgs.conversationState?.currentMessage?.userInputMessage?.userInputMessageContext
426+
?.toolResults[0].status,
427+
'success'
428+
)
429+
assert.deepStrictEqual(
430+
secondCallArgs.conversationState?.currentMessage?.userInputMessage?.userInputMessageContext
431+
?.toolResults[0].content[0].json,
432+
{ result: mockToolResult }
433+
)
434+
435+
// Verify that the history was updated correctly
436+
assert.ok(secondCallArgs.conversationState?.history)
437+
assert.strictEqual(secondCallArgs.conversationState?.history.length, 2)
438+
assert.ok(secondCallArgs.conversationState?.history[0].userInputMessage)
439+
assert.ok(secondCallArgs.conversationState?.history[1].assistantResponseMessage)
440+
441+
// Verify the final result
442+
assert.deepStrictEqual(chatResult, expectedCompleteChatResult)
443+
})
444+
445+
it('handles multiple iterations of tool uses with proper history updates', async () => {
446+
// First response includes a tool use request
447+
const mockToolUseId1 = 'mock-tool-use-id-1'
448+
const mockToolName1 = 'mock-tool-name-1'
449+
const mockToolInput1 = JSON.stringify({ param1: 'value1' })
450+
const mockToolResult1 = { result: 'tool execution result 1' }
451+
452+
// Second tool use in a subsequent response
453+
const mockToolUseId2 = 'mock-tool-use-id-2'
454+
const mockToolName2 = 'mock-tool-name-2'
455+
const mockToolInput2 = JSON.stringify({ param2: 'value2' })
456+
const mockToolResult2 = { result: 'tool execution result 2' }
457+
458+
// First response with first tool use
459+
const mockFirstToolUseResponseList: ChatResponseStream[] = [
460+
{
461+
messageMetadataEvent: {
462+
conversationId: mockConversationId,
463+
},
464+
},
465+
{
466+
assistantResponseEvent: {
467+
content: 'I need to use tool 1. ',
468+
},
469+
},
470+
{
471+
toolUseEvent: {
472+
toolUseId: mockToolUseId1,
473+
name: mockToolName1,
474+
input: mockToolInput1,
475+
stop: true,
476+
},
477+
},
478+
]
479+
480+
// Second response with second tool use
481+
const mockSecondToolUseResponseList: ChatResponseStream[] = [
482+
{
483+
messageMetadataEvent: {
484+
conversationId: mockConversationId,
485+
},
486+
},
487+
{
488+
assistantResponseEvent: {
489+
content: 'Now I need to use tool 2. ',
490+
},
491+
},
492+
{
493+
toolUseEvent: {
494+
toolUseId: mockToolUseId2,
495+
name: mockToolName2,
496+
input: mockToolInput2,
497+
stop: true,
498+
},
499+
},
500+
]
501+
502+
// Final response with complete answer
503+
const mockFinalResponseList: ChatResponseStream[] = [
504+
{
505+
messageMetadataEvent: {
506+
conversationId: mockConversationId,
507+
},
508+
},
509+
{
510+
assistantResponseEvent: {
511+
content: 'Hello ',
512+
},
513+
},
514+
{
515+
assistantResponseEvent: {
516+
content: 'World',
517+
},
518+
},
519+
{
520+
assistantResponseEvent: {
521+
content: '!',
522+
},
523+
},
524+
]
525+
526+
// Reset the stub and set up to return different responses on consecutive calls
527+
generateAssistantResponseStub.restore()
528+
generateAssistantResponseStub = sinon.stub(CodeWhispererStreaming.prototype, 'generateAssistantResponse')
529+
530+
generateAssistantResponseStub.onFirstCall().returns(
531+
Promise.resolve({
532+
$metadata: {
533+
requestId: mockMessageId,
534+
},
535+
generateAssistantResponseResponse: createIterableResponse(mockFirstToolUseResponseList),
536+
})
537+
)
538+
539+
generateAssistantResponseStub.onSecondCall().returns(
540+
Promise.resolve({
541+
$metadata: {
542+
requestId: mockMessageId,
543+
},
544+
generateAssistantResponseResponse: createIterableResponse(mockSecondToolUseResponseList),
545+
})
546+
)
547+
548+
generateAssistantResponseStub.onThirdCall().returns(
549+
Promise.resolve({
550+
$metadata: {
551+
requestId: mockMessageId,
552+
},
553+
generateAssistantResponseResponse: createIterableResponse(mockFinalResponseList),
554+
})
555+
)
556+
557+
// Reset the runTool stub
558+
const runToolStub = testFeatures.agent.runTool as sinon.SinonStub
559+
runToolStub.reset()
560+
runToolStub.withArgs(mockToolName1, JSON.parse(mockToolInput1)).resolves(mockToolResult1)
561+
runToolStub.withArgs(mockToolName2, JSON.parse(mockToolInput2)).resolves(mockToolResult2)
562+
563+
// Make the request
564+
const chatResultPromise = chatController.onChatPrompt(
565+
{ tabId: mockTabId, prompt: { prompt: 'Hello with multiple tools' } },
566+
mockCancellationToken
567+
)
568+
569+
const chatResult = await chatResultPromise
570+
571+
// Verify that generateAssistantResponse was called three times
572+
sinon.assert.calledThrice(generateAssistantResponseStub)
573+
574+
// Verify that the tools were executed
575+
sinon.assert.calledTwice(runToolStub)
576+
sinon.assert.calledWith(runToolStub, mockToolName1, JSON.parse(mockToolInput1))
577+
sinon.assert.calledWith(runToolStub, mockToolName2, JSON.parse(mockToolInput2))
578+
579+
// Verify that the second request included the first tool results
580+
const secondCallArgs = generateAssistantResponseStub.secondCall.args[0]
581+
assert.ok(
582+
secondCallArgs.conversationState?.currentMessage?.userInputMessage?.userInputMessageContext?.toolResults
583+
)
584+
assert.strictEqual(
585+
secondCallArgs.conversationState?.currentMessage?.userInputMessage?.userInputMessageContext?.toolResults
586+
.length,
587+
1
588+
)
589+
assert.strictEqual(
590+
secondCallArgs.conversationState?.currentMessage?.userInputMessage?.userInputMessageContext
591+
?.toolResults[0].toolUseId,
592+
mockToolUseId1
593+
)
594+
595+
// Verify that the history was updated correctly after first tool use
596+
assert.ok(secondCallArgs.conversationState?.history)
597+
assert.strictEqual(secondCallArgs.conversationState?.history.length, 2)
598+
assert.ok(secondCallArgs.conversationState?.history[0].userInputMessage)
599+
assert.ok(secondCallArgs.conversationState?.history[1].assistantResponseMessage)
600+
assert.strictEqual(
601+
secondCallArgs.conversationState?.history[1].assistantResponseMessage?.content,
602+
'I need to use tool 1. '
603+
)
604+
605+
// Verify that the third request included the second tool results
606+
const thirdCallArgs = generateAssistantResponseStub.thirdCall.args[0]
607+
assert.ok(
608+
thirdCallArgs.conversationState?.currentMessage?.userInputMessage?.userInputMessageContext?.toolResults
609+
)
610+
assert.strictEqual(
611+
thirdCallArgs.conversationState?.currentMessage?.userInputMessage?.userInputMessageContext?.toolResults
612+
.length,
613+
1
614+
)
615+
assert.strictEqual(
616+
thirdCallArgs.conversationState?.currentMessage?.userInputMessage?.userInputMessageContext
617+
?.toolResults[0].toolUseId,
618+
mockToolUseId2
619+
)
620+
621+
// Verify that the history was updated correctly after second tool use
622+
assert.ok(thirdCallArgs.conversationState?.history)
623+
assert.strictEqual(thirdCallArgs.conversationState?.history.length, 4)
624+
assert.ok(thirdCallArgs.conversationState?.history[2].userInputMessage)
625+
assert.ok(thirdCallArgs.conversationState?.history[3].assistantResponseMessage)
626+
assert.strictEqual(
627+
thirdCallArgs.conversationState?.history[3].assistantResponseMessage?.content,
628+
'Now I need to use tool 2. '
629+
)
630+
631+
// Verify the final result
632+
assert.deepStrictEqual(chatResult, expectedCompleteChatResult)
633+
})
634+
308635
it('returns help message if it is a help follow up action', async () => {
309636
const chatResultPromise = chatController.onChatPrompt(
310637
{ tabId: mockTabId, prompt: { prompt: DEFAULT_HELP_FOLLOW_UP_PROMPT } },

0 commit comments

Comments
 (0)