mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-24 06:18:04 -05:00
updated agent handler
This commit is contained in:
@@ -144,25 +144,22 @@ describe('AgentBlockHandler', () => {
|
||||
}
|
||||
mockGetProviderFromModel.mockReturnValue('mock-provider')
|
||||
|
||||
mockFetch.mockImplementation(() => {
|
||||
mockExecuteProviderRequest.mockResolvedValue({
|
||||
content: 'Mocked response content',
|
||||
model: 'mock-model',
|
||||
tokens: { input: 10, output: 20, total: 30 },
|
||||
toolCalls: [],
|
||||
cost: 0.001,
|
||||
timing: { total: 100 },
|
||||
})
|
||||
|
||||
mockFetch.mockImplementation((url: string) => {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
headers: {
|
||||
get: (name: string) => {
|
||||
if (name === 'Content-Type') return 'application/json'
|
||||
if (name === 'X-Execution-Data') return null
|
||||
return null
|
||||
},
|
||||
get: () => null,
|
||||
},
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
content: 'Mocked response content',
|
||||
model: 'mock-model',
|
||||
tokens: { input: 10, output: 20, total: 30 },
|
||||
toolCalls: [],
|
||||
cost: 0.001,
|
||||
timing: { total: 100 },
|
||||
}),
|
||||
json: () => Promise.resolve({}),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -244,7 +241,7 @@ describe('AgentBlockHandler', () => {
|
||||
const result = await handler.execute(mockContext, mockBlock, inputs)
|
||||
|
||||
expect(mockGetProviderFromModel).toHaveBeenCalledWith('gpt-4o')
|
||||
expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.any(Object))
|
||||
expect(mockExecuteProviderRequest).toHaveBeenCalled()
|
||||
expect(result).toEqual(expectedOutput)
|
||||
})
|
||||
|
||||
@@ -263,34 +260,21 @@ describe('AgentBlockHandler', () => {
|
||||
return result
|
||||
})
|
||||
|
||||
mockFetch.mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
headers: {
|
||||
get: (name: string) => {
|
||||
if (name === 'Content-Type') return 'application/json'
|
||||
if (name === 'X-Execution-Data') return null
|
||||
return null
|
||||
},
|
||||
mockExecuteProviderRequest.mockResolvedValueOnce({
|
||||
content: 'Using tools to respond',
|
||||
model: 'mock-model',
|
||||
tokens: { input: 10, output: 20, total: 30 },
|
||||
toolCalls: [
|
||||
{
|
||||
name: 'auto_tool',
|
||||
arguments: { input: 'test input for auto tool' },
|
||||
},
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
content: 'Using tools to respond',
|
||||
model: 'mock-model',
|
||||
tokens: { input: 10, output: 20, total: 30 },
|
||||
toolCalls: [
|
||||
{
|
||||
name: 'auto_tool',
|
||||
arguments: { input: 'test input for auto tool' },
|
||||
},
|
||||
{
|
||||
name: 'force_tool',
|
||||
arguments: { input: 'test input for force tool' },
|
||||
},
|
||||
],
|
||||
timing: { total: 100 },
|
||||
}),
|
||||
})
|
||||
{
|
||||
name: 'force_tool',
|
||||
arguments: { input: 'test input for force tool' },
|
||||
},
|
||||
],
|
||||
timing: { total: 100 },
|
||||
})
|
||||
|
||||
const inputs = {
|
||||
@@ -403,8 +387,8 @@ describe('AgentBlockHandler', () => {
|
||||
expect.any(Object) // execution context
|
||||
)
|
||||
|
||||
const fetchCall = mockFetch.mock.calls[0]
|
||||
const requestBody = JSON.parse(fetchCall[1].body)
|
||||
const providerCall = mockExecuteProviderRequest.mock.calls[0]
|
||||
const requestBody = providerCall[1]
|
||||
|
||||
expect(requestBody.tools.length).toBe(2)
|
||||
})
|
||||
@@ -443,8 +427,8 @@ describe('AgentBlockHandler', () => {
|
||||
|
||||
await handler.execute(mockContext, mockBlock, inputs)
|
||||
|
||||
const fetchCall = mockFetch.mock.calls[0]
|
||||
const requestBody = JSON.parse(fetchCall[1].body)
|
||||
const providerCall = mockExecuteProviderRequest.mock.calls[0]
|
||||
const requestBody = providerCall[1]
|
||||
|
||||
expect(requestBody.tools.length).toBe(2)
|
||||
|
||||
@@ -488,8 +472,8 @@ describe('AgentBlockHandler', () => {
|
||||
|
||||
await handler.execute(mockContext, mockBlock, inputs)
|
||||
|
||||
const fetchCall = mockFetch.mock.calls[0]
|
||||
const requestBody = JSON.parse(fetchCall[1].body)
|
||||
const providerCall = mockExecuteProviderRequest.mock.calls[0]
|
||||
const requestBody = providerCall[1]
|
||||
|
||||
expect(requestBody.tools[0].usageControl).toBe('auto')
|
||||
expect(requestBody.tools[1].usageControl).toBe('force')
|
||||
@@ -553,8 +537,8 @@ describe('AgentBlockHandler', () => {
|
||||
|
||||
await handler.execute(mockContext, mockBlock, inputs)
|
||||
|
||||
const fetchCall = mockFetch.mock.calls[0]
|
||||
const requestBody = JSON.parse(fetchCall[1].body)
|
||||
const providerCall = mockExecuteProviderRequest.mock.calls[0]
|
||||
const requestBody = providerCall[1]
|
||||
|
||||
expect(requestBody.tools.length).toBe(2)
|
||||
|
||||
@@ -583,7 +567,7 @@ describe('AgentBlockHandler', () => {
|
||||
|
||||
await handler.execute(mockContext, mockBlock, inputs)
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.any(Object))
|
||||
expect(mockExecuteProviderRequest).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should execute with standard block tools', async () => {
|
||||
@@ -625,7 +609,7 @@ describe('AgentBlockHandler', () => {
|
||||
inputs.tools[0],
|
||||
expect.objectContaining({ selectedOperation: 'analyze' })
|
||||
)
|
||||
expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.any(Object))
|
||||
expect(mockExecuteProviderRequest).toHaveBeenCalled()
|
||||
expect(result).toEqual(expectedOutput)
|
||||
})
|
||||
|
||||
@@ -676,30 +660,17 @@ describe('AgentBlockHandler', () => {
|
||||
|
||||
await handler.execute(mockContext, mockBlock, inputs)
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.any(Object))
|
||||
expect(mockExecuteProviderRequest).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle responseFormat with valid JSON', async () => {
|
||||
mockFetch.mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
headers: {
|
||||
get: (name: string) => {
|
||||
if (name === 'Content-Type') return 'application/json'
|
||||
if (name === 'X-Execution-Data') return null
|
||||
return null
|
||||
},
|
||||
},
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
content: '{"result": "Success", "score": 0.95}',
|
||||
model: 'mock-model',
|
||||
tokens: { input: 10, output: 20, total: 30 },
|
||||
timing: { total: 100 },
|
||||
toolCalls: [],
|
||||
cost: undefined,
|
||||
}),
|
||||
})
|
||||
mockExecuteProviderRequest.mockResolvedValueOnce({
|
||||
content: '{"result": "Success", "score": 0.95}',
|
||||
model: 'mock-model',
|
||||
tokens: { input: 10, output: 20, total: 30 },
|
||||
timing: { total: 100 },
|
||||
toolCalls: [],
|
||||
cost: undefined,
|
||||
})
|
||||
|
||||
const inputs = {
|
||||
@@ -723,24 +694,11 @@ describe('AgentBlockHandler', () => {
|
||||
})
|
||||
|
||||
it('should handle responseFormat when it is an empty string', async () => {
|
||||
mockFetch.mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
headers: {
|
||||
get: (name: string) => {
|
||||
if (name === 'Content-Type') return 'application/json'
|
||||
if (name === 'X-Execution-Data') return null
|
||||
return null
|
||||
},
|
||||
},
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
content: 'Regular text response',
|
||||
model: 'mock-model',
|
||||
tokens: { input: 10, output: 20, total: 30 },
|
||||
timing: { total: 100 },
|
||||
}),
|
||||
})
|
||||
mockExecuteProviderRequest.mockResolvedValueOnce({
|
||||
content: 'Regular text response',
|
||||
model: 'mock-model',
|
||||
tokens: { input: 10, output: 20, total: 30 },
|
||||
timing: { total: 100 },
|
||||
})
|
||||
|
||||
const inputs = {
|
||||
@@ -763,26 +721,13 @@ describe('AgentBlockHandler', () => {
|
||||
})
|
||||
|
||||
it('should handle invalid JSON in responseFormat gracefully', async () => {
|
||||
mockFetch.mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
headers: {
|
||||
get: (name: string) => {
|
||||
if (name === 'Content-Type') return 'application/json'
|
||||
if (name === 'X-Execution-Data') return null
|
||||
return null
|
||||
},
|
||||
},
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
content: 'Regular text response',
|
||||
model: 'mock-model',
|
||||
tokens: { input: 10, output: 20, total: 30 },
|
||||
timing: { total: 100 },
|
||||
toolCalls: [],
|
||||
cost: undefined,
|
||||
}),
|
||||
})
|
||||
mockExecuteProviderRequest.mockResolvedValueOnce({
|
||||
content: 'Regular text response',
|
||||
model: 'mock-model',
|
||||
tokens: { input: 10, output: 20, total: 30 },
|
||||
timing: { total: 100 },
|
||||
toolCalls: [],
|
||||
cost: undefined,
|
||||
})
|
||||
|
||||
const inputs = {
|
||||
@@ -806,26 +751,13 @@ describe('AgentBlockHandler', () => {
|
||||
})
|
||||
|
||||
it('should handle variable references in responseFormat gracefully', async () => {
|
||||
mockFetch.mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
headers: {
|
||||
get: (name: string) => {
|
||||
if (name === 'Content-Type') return 'application/json'
|
||||
if (name === 'X-Execution-Data') return null
|
||||
return null
|
||||
},
|
||||
},
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
content: 'Regular text response',
|
||||
model: 'mock-model',
|
||||
tokens: { input: 10, output: 20, total: 30 },
|
||||
timing: { total: 100 },
|
||||
toolCalls: [],
|
||||
cost: undefined,
|
||||
}),
|
||||
})
|
||||
mockExecuteProviderRequest.mockResolvedValueOnce({
|
||||
content: 'Regular text response',
|
||||
model: 'mock-model',
|
||||
tokens: { input: 10, output: 20, total: 30 },
|
||||
timing: { total: 100 },
|
||||
toolCalls: [],
|
||||
cost: undefined,
|
||||
})
|
||||
|
||||
const inputs = {
|
||||
@@ -856,7 +788,7 @@ describe('AgentBlockHandler', () => {
|
||||
}
|
||||
|
||||
mockGetProviderFromModel.mockReturnValue('openai')
|
||||
mockFetch.mockRejectedValue(new Error('Provider API Error'))
|
||||
mockExecuteProviderRequest.mockRejectedValueOnce(new Error('Provider API Error'))
|
||||
|
||||
await expect(handler.execute(mockContext, mockBlock, inputs)).rejects.toThrow(
|
||||
'Provider API Error'
|
||||
@@ -870,30 +802,17 @@ describe('AgentBlockHandler', () => {
|
||||
},
|
||||
})
|
||||
|
||||
mockFetch.mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
headers: {
|
||||
get: (name: string) => {
|
||||
if (name === 'Content-Type') return 'application/json'
|
||||
if (name === 'X-Execution-Data') return null
|
||||
return null
|
||||
},
|
||||
mockExecuteProviderRequest.mockResolvedValueOnce({
|
||||
stream: mockStreamBody,
|
||||
execution: {
|
||||
success: true,
|
||||
output: {},
|
||||
logs: [],
|
||||
metadata: {
|
||||
duration: 0,
|
||||
startTime: new Date().toISOString(),
|
||||
},
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
stream: mockStreamBody,
|
||||
execution: {
|
||||
success: true,
|
||||
output: {},
|
||||
logs: [],
|
||||
metadata: {
|
||||
duration: 0,
|
||||
startTime: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const inputs = {
|
||||
@@ -947,22 +866,9 @@ describe('AgentBlockHandler', () => {
|
||||
},
|
||||
}
|
||||
|
||||
mockFetch.mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
headers: {
|
||||
get: (name: string) => {
|
||||
if (name === 'Content-Type') return 'application/json'
|
||||
if (name === 'X-Execution-Data') return JSON.stringify(mockExecutionData)
|
||||
return null
|
||||
},
|
||||
},
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
stream: mockStreamBody,
|
||||
execution: mockExecutionData,
|
||||
}),
|
||||
})
|
||||
mockExecuteProviderRequest.mockResolvedValueOnce({
|
||||
stream: mockStreamBody,
|
||||
execution: mockExecutionData,
|
||||
})
|
||||
|
||||
const inputs = {
|
||||
@@ -996,30 +902,21 @@ describe('AgentBlockHandler', () => {
|
||||
},
|
||||
})
|
||||
|
||||
mockFetch.mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
headers: {
|
||||
get: (name: string) => (name === 'Content-Type' ? 'application/json' : null),
|
||||
mockExecuteProviderRequest.mockResolvedValueOnce({
|
||||
stream: {}, // Serialized stream placeholder
|
||||
execution: {
|
||||
success: true,
|
||||
output: {
|
||||
content: 'Test streaming content',
|
||||
model: 'gpt-4o',
|
||||
tokens: { input: 10, output: 5, total: 15 },
|
||||
},
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
stream: {}, // Serialized stream placeholder
|
||||
execution: {
|
||||
success: true,
|
||||
output: {
|
||||
content: 'Test streaming content',
|
||||
model: 'gpt-4o',
|
||||
tokens: { input: 10, output: 5, total: 15 },
|
||||
},
|
||||
logs: [],
|
||||
metadata: {
|
||||
startTime: new Date().toISOString(),
|
||||
duration: 150,
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
logs: [],
|
||||
metadata: {
|
||||
startTime: new Date().toISOString(),
|
||||
duration: 150,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const inputs = {
|
||||
@@ -1060,8 +957,8 @@ describe('AgentBlockHandler', () => {
|
||||
|
||||
await handler.execute(mockContext, mockBlock, inputs)
|
||||
|
||||
const fetchCall = mockFetch.mock.calls[0]
|
||||
const requestBody = JSON.parse(fetchCall[1].body)
|
||||
const providerCall = mockExecuteProviderRequest.mock.calls[0]
|
||||
const requestBody = providerCall[1]
|
||||
|
||||
// Verify messages were built correctly
|
||||
expect(requestBody.messages).toBeDefined()
|
||||
@@ -1110,8 +1007,8 @@ describe('AgentBlockHandler', () => {
|
||||
|
||||
await handler.execute(mockContext, mockBlock, inputs)
|
||||
|
||||
const fetchCall = mockFetch.mock.calls[0]
|
||||
const requestBody = JSON.parse(fetchCall[1].body)
|
||||
const providerCall = mockExecuteProviderRequest.mock.calls[0]
|
||||
const requestBody = providerCall[1]
|
||||
|
||||
// Verify messages were built correctly
|
||||
expect(requestBody.messages).toBeDefined()
|
||||
@@ -1149,8 +1046,8 @@ describe('AgentBlockHandler', () => {
|
||||
|
||||
await handler.execute(mockContext, mockBlock, inputs)
|
||||
|
||||
const fetchCall = mockFetch.mock.calls[0]
|
||||
const requestBody = JSON.parse(fetchCall[1].body)
|
||||
const providerCall = mockExecuteProviderRequest.mock.calls[0]
|
||||
const requestBody = providerCall[1]
|
||||
|
||||
// Verify messages were built correctly
|
||||
expect(requestBody.messages).toBeDefined()
|
||||
@@ -1181,8 +1078,8 @@ describe('AgentBlockHandler', () => {
|
||||
|
||||
await handler.execute(mockContext, mockBlock, inputs)
|
||||
|
||||
const fetchCall = mockFetch.mock.calls[0]
|
||||
const requestBody = JSON.parse(fetchCall[1].body)
|
||||
const providerCall = mockExecuteProviderRequest.mock.calls[0]
|
||||
const requestBody = providerCall[1]
|
||||
|
||||
// Verify messages were built correctly
|
||||
// Agent system (1) + legacy memories (3) + user from messages (1) = 5
|
||||
@@ -1225,8 +1122,8 @@ describe('AgentBlockHandler', () => {
|
||||
|
||||
await handler.execute(mockContext, mockBlock, inputs)
|
||||
|
||||
const fetchCall = mockFetch.mock.calls[0]
|
||||
const requestBody = JSON.parse(fetchCall[1].body)
|
||||
const providerCall = mockExecuteProviderRequest.mock.calls[0]
|
||||
const requestBody = providerCall[1]
|
||||
|
||||
// Verify messages were built correctly
|
||||
expect(requestBody.messages).toBeDefined()
|
||||
@@ -1268,8 +1165,8 @@ describe('AgentBlockHandler', () => {
|
||||
|
||||
await handler.execute(mockContext, mockBlock, inputs)
|
||||
|
||||
const fetchCall = mockFetch.mock.calls[0]
|
||||
const requestBody = JSON.parse(fetchCall[1].body)
|
||||
const providerCall = mockExecuteProviderRequest.mock.calls[0]
|
||||
const requestBody = providerCall[1]
|
||||
|
||||
// Verify messages were built correctly
|
||||
expect(requestBody.messages).toBeDefined()
|
||||
@@ -1310,8 +1207,8 @@ describe('AgentBlockHandler', () => {
|
||||
|
||||
await handler.execute(mockContext, mockBlock, inputs)
|
||||
|
||||
const fetchCall = mockFetch.mock.calls[0]
|
||||
const requestBody = JSON.parse(fetchCall[1].body)
|
||||
const providerCall = mockExecuteProviderRequest.mock.calls[0]
|
||||
const requestBody = providerCall[1]
|
||||
|
||||
// Verify user prompt content was extracted correctly
|
||||
expect(requestBody.messages).toBeDefined()
|
||||
@@ -1337,15 +1234,14 @@ describe('AgentBlockHandler', () => {
|
||||
|
||||
await handler.execute(mockContext, mockBlock, inputs)
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.any(Object))
|
||||
expect(mockExecuteProviderRequest).toHaveBeenCalled()
|
||||
|
||||
const fetchCall = mockFetch.mock.calls[0]
|
||||
const requestBody = JSON.parse(fetchCall[1].body)
|
||||
const providerCall = mockExecuteProviderRequest.mock.calls[0]
|
||||
const requestBody = providerCall[1]
|
||||
|
||||
// Check that Azure parameters are included in the request
|
||||
expect(requestBody.azureEndpoint).toBe('https://my-azure-resource.openai.azure.com')
|
||||
expect(requestBody.azureApiVersion).toBe('2024-07-01-preview')
|
||||
expect(requestBody.provider).toBe('azure-openai')
|
||||
expect(providerCall[0]).toBe('azure-openai')
|
||||
expect(requestBody.model).toBe('azure/gpt-4o')
|
||||
expect(requestBody.apiKey).toBe('test-azure-api-key')
|
||||
})
|
||||
@@ -1365,15 +1261,14 @@ describe('AgentBlockHandler', () => {
|
||||
|
||||
await handler.execute(mockContext, mockBlock, inputs)
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.any(Object))
|
||||
expect(mockExecuteProviderRequest).toHaveBeenCalled()
|
||||
|
||||
const fetchCall = mockFetch.mock.calls[0]
|
||||
const requestBody = JSON.parse(fetchCall[1].body)
|
||||
const providerCall = mockExecuteProviderRequest.mock.calls[0]
|
||||
const requestBody = providerCall[1]
|
||||
|
||||
// Check that GPT-5 parameters are included in the request
|
||||
expect(requestBody.reasoningEffort).toBe('minimal')
|
||||
expect(requestBody.verbosity).toBe('high')
|
||||
expect(requestBody.provider).toBe('openai')
|
||||
expect(providerCall[0]).toBe('openai')
|
||||
expect(requestBody.model).toBe('gpt-5')
|
||||
expect(requestBody.apiKey).toBe('test-api-key')
|
||||
})
|
||||
@@ -1385,22 +1280,20 @@ describe('AgentBlockHandler', () => {
|
||||
userPrompt: 'Hello!',
|
||||
apiKey: 'test-api-key',
|
||||
temperature: 0.7,
|
||||
// No reasoningEffort or verbosity provided
|
||||
}
|
||||
|
||||
mockGetProviderFromModel.mockReturnValue('openai')
|
||||
|
||||
await handler.execute(mockContext, mockBlock, inputs)
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.any(Object))
|
||||
expect(mockExecuteProviderRequest).toHaveBeenCalled()
|
||||
|
||||
const fetchCall = mockFetch.mock.calls[0]
|
||||
const requestBody = JSON.parse(fetchCall[1].body)
|
||||
const providerCall = mockExecuteProviderRequest.mock.calls[0]
|
||||
const requestBody = providerCall[1]
|
||||
|
||||
// Check that GPT-5 parameters are undefined when not provided
|
||||
expect(requestBody.reasoningEffort).toBeUndefined()
|
||||
expect(requestBody.verbosity).toBeUndefined()
|
||||
expect(requestBody.provider).toBe('openai')
|
||||
expect(providerCall[0]).toBe('openai')
|
||||
expect(requestBody.model).toBe('gpt-5')
|
||||
})
|
||||
|
||||
@@ -1422,42 +1315,29 @@ describe('AgentBlockHandler', () => {
|
||||
return Promise.resolve({ success: false, error: 'Unknown tool' })
|
||||
})
|
||||
|
||||
mockFetch.mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
headers: {
|
||||
get: (name: string) => {
|
||||
if (name === 'Content-Type') return 'application/json'
|
||||
if (name === 'X-Execution-Data') return null
|
||||
return null
|
||||
mockExecuteProviderRequest.mockResolvedValueOnce({
|
||||
content: 'I will use MCP tools to help you.',
|
||||
model: 'gpt-4o',
|
||||
tokens: { input: 15, output: 25, total: 40 },
|
||||
toolCalls: [
|
||||
{
|
||||
name: 'mcp-server1-list_files',
|
||||
arguments: { path: '/tmp' },
|
||||
result: {
|
||||
success: true,
|
||||
output: { content: [{ type: 'text', text: 'Files listed' }] },
|
||||
},
|
||||
},
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
content: 'I will use MCP tools to help you.',
|
||||
model: 'gpt-4o',
|
||||
tokens: { input: 15, output: 25, total: 40 },
|
||||
toolCalls: [
|
||||
{
|
||||
name: 'mcp-server1-list_files',
|
||||
arguments: { path: '/tmp' },
|
||||
result: {
|
||||
success: true,
|
||||
output: { content: [{ type: 'text', text: 'Files listed' }] },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'mcp-server2-search',
|
||||
arguments: { query: 'test', limit: 5 },
|
||||
result: {
|
||||
success: true,
|
||||
output: { content: [{ type: 'text', text: 'Search results' }] },
|
||||
},
|
||||
},
|
||||
],
|
||||
timing: { total: 150 },
|
||||
}),
|
||||
})
|
||||
{
|
||||
name: 'mcp-server2-search',
|
||||
arguments: { query: 'test', limit: 5 },
|
||||
result: {
|
||||
success: true,
|
||||
output: { content: [{ type: 'text', text: 'Search results' }] },
|
||||
},
|
||||
},
|
||||
],
|
||||
timing: { total: 150 },
|
||||
})
|
||||
|
||||
const inputs = {
|
||||
@@ -1533,34 +1413,21 @@ describe('AgentBlockHandler', () => {
|
||||
return Promise.resolve({ success: false, error: 'Unknown tool' })
|
||||
})
|
||||
|
||||
mockFetch.mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
headers: {
|
||||
get: (name: string) => {
|
||||
if (name === 'Content-Type') return 'application/json'
|
||||
if (name === 'X-Execution-Data') return null
|
||||
return null
|
||||
mockExecuteProviderRequest.mockResolvedValueOnce({
|
||||
content: 'Let me try to use this tool.',
|
||||
model: 'gpt-4o',
|
||||
tokens: { input: 10, output: 15, total: 25 },
|
||||
toolCalls: [
|
||||
{
|
||||
name: 'mcp-server1-failing_tool',
|
||||
arguments: { param: 'value' },
|
||||
result: {
|
||||
success: false,
|
||||
error: 'MCP server connection failed',
|
||||
},
|
||||
},
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
content: 'Let me try to use this tool.',
|
||||
model: 'gpt-4o',
|
||||
tokens: { input: 10, output: 15, total: 25 },
|
||||
toolCalls: [
|
||||
{
|
||||
name: 'mcp-server1-failing_tool',
|
||||
arguments: { param: 'value' },
|
||||
result: {
|
||||
success: false,
|
||||
error: 'MCP server connection failed',
|
||||
},
|
||||
},
|
||||
],
|
||||
timing: { total: 100 },
|
||||
}),
|
||||
})
|
||||
],
|
||||
timing: { total: 100 },
|
||||
})
|
||||
|
||||
const inputs = {
|
||||
@@ -1638,25 +1505,12 @@ describe('AgentBlockHandler', () => {
|
||||
|
||||
mockGetProviderFromModel.mockReturnValue('openai')
|
||||
|
||||
mockFetch.mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
headers: {
|
||||
get: (name: string) => {
|
||||
if (name === 'Content-Type') return 'application/json'
|
||||
if (name === 'X-Execution-Data') return null
|
||||
return null
|
||||
},
|
||||
},
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
content: 'Used MCP tools successfully',
|
||||
model: 'gpt-4o',
|
||||
tokens: { input: 20, output: 30, total: 50 },
|
||||
toolCalls: [],
|
||||
timing: { total: 200 },
|
||||
}),
|
||||
})
|
||||
mockExecuteProviderRequest.mockResolvedValueOnce({
|
||||
content: 'Used MCP tools successfully',
|
||||
model: 'gpt-4o',
|
||||
tokens: { input: 20, output: 30, total: 50 },
|
||||
toolCalls: [],
|
||||
timing: { total: 200 },
|
||||
})
|
||||
|
||||
mockTransformBlockTool.mockImplementation((tool: any) => ({
|
||||
@@ -1669,11 +1523,9 @@ describe('AgentBlockHandler', () => {
|
||||
|
||||
const result = await handler.execute(mockContext, mockBlock, inputs)
|
||||
|
||||
// Verify that the agent executed successfully with MCP tools
|
||||
expect(result).toBeDefined()
|
||||
expect(mockFetch).toHaveBeenCalled()
|
||||
expect(mockExecuteProviderRequest).toHaveBeenCalled()
|
||||
|
||||
// Verify the agent returns the expected response format
|
||||
expect((result as any).content).toBe('Used MCP tools successfully')
|
||||
expect((result as any).model).toBe('gpt-4o')
|
||||
})
|
||||
@@ -1691,21 +1543,12 @@ describe('AgentBlockHandler', () => {
|
||||
return Promise.resolve({ success: false, error: 'Unknown tool' })
|
||||
})
|
||||
|
||||
mockFetch.mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
headers: {
|
||||
get: (name: string) => (name === 'Content-Type' ? 'application/json' : null),
|
||||
},
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
content: 'Using MCP tool',
|
||||
model: 'gpt-4o',
|
||||
tokens: { input: 10, output: 10, total: 20 },
|
||||
toolCalls: [{ name: 'mcp-test-tool', arguments: {} }],
|
||||
timing: { total: 50 },
|
||||
}),
|
||||
})
|
||||
mockExecuteProviderRequest.mockResolvedValueOnce({
|
||||
content: 'Using MCP tool',
|
||||
model: 'gpt-4o',
|
||||
tokens: { input: 10, output: 10, total: 20 },
|
||||
toolCalls: [{ name: 'mcp-test-tool', arguments: {} }],
|
||||
timing: { total: 50 },
|
||||
})
|
||||
|
||||
const inputs = {
|
||||
@@ -1815,37 +1658,26 @@ describe('AgentBlockHandler', () => {
|
||||
const discoveryCalls = fetchCalls.filter((c) => c.url.includes('/api/mcp/tools/discover'))
|
||||
expect(discoveryCalls.length).toBe(0)
|
||||
|
||||
const providerCalls = fetchCalls.filter((c) => c.url.includes('/api/providers'))
|
||||
expect(providerCalls.length).toBe(1)
|
||||
expect(mockExecuteProviderRequest).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should pass toolSchema to execution endpoint when using cached schema', async () => {
|
||||
let executionCall: any = null
|
||||
|
||||
mockFetch.mockImplementation((url: string, options: any) => {
|
||||
if (url.includes('/api/providers')) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
headers: {
|
||||
get: (name: string) => (name === 'Content-Type' ? 'application/json' : null),
|
||||
},
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
content: 'Tool executed',
|
||||
model: 'gpt-4o',
|
||||
tokens: { input: 10, output: 10, total: 20 },
|
||||
toolCalls: [
|
||||
{
|
||||
name: 'search_files',
|
||||
arguments: { query: 'test' },
|
||||
result: { success: true, output: {} },
|
||||
},
|
||||
],
|
||||
timing: { total: 50 },
|
||||
}),
|
||||
})
|
||||
}
|
||||
mockExecuteProviderRequest.mockResolvedValueOnce({
|
||||
content: 'Tool executed',
|
||||
model: 'gpt-4o',
|
||||
tokens: { input: 10, output: 10, total: 20 },
|
||||
toolCalls: [
|
||||
{
|
||||
name: 'search_files',
|
||||
arguments: JSON.stringify({ query: 'test' }),
|
||||
},
|
||||
],
|
||||
timing: { total: 50 },
|
||||
})
|
||||
|
||||
mockFetch.mockImplementation((url: string, options: any) => {
|
||||
if (url.includes('/api/mcp/tools/execute')) {
|
||||
executionCall = { url, body: JSON.parse(options.body) }
|
||||
return Promise.resolve({
|
||||
@@ -1898,13 +1730,11 @@ describe('AgentBlockHandler', () => {
|
||||
|
||||
await handler.execute(contextWithWorkspace, mockBlock, inputs)
|
||||
|
||||
const providerCalls = mockFetch.mock.calls.filter((c: any) => c[0].includes('/api/providers'))
|
||||
expect(providerCalls.length).toBe(1)
|
||||
|
||||
const providerRequestBody = JSON.parse(providerCalls[0][1].body)
|
||||
expect(providerRequestBody.tools).toBeDefined()
|
||||
expect(providerRequestBody.tools.length).toBe(1)
|
||||
expect(providerRequestBody.tools[0].name).toBe('search_files')
|
||||
expect(mockExecuteProviderRequest).toHaveBeenCalled()
|
||||
const providerCallArgs = mockExecuteProviderRequest.mock.calls[0]
|
||||
expect(providerCallArgs[1].tools).toBeDefined()
|
||||
expect(providerCallArgs[1].tools.length).toBe(1)
|
||||
expect(providerCallArgs[1].tools[0].name).toBe('search_files')
|
||||
})
|
||||
|
||||
it('should handle multiple MCP tools from the same server efficiently', async () => {
|
||||
@@ -1987,14 +1817,12 @@ describe('AgentBlockHandler', () => {
|
||||
const discoveryCalls = fetchCalls.filter((c) => c.url.includes('/api/mcp/tools/discover'))
|
||||
expect(discoveryCalls.length).toBe(0)
|
||||
|
||||
const providerCalls = fetchCalls.filter((c) => c.url.includes('/api/providers'))
|
||||
expect(providerCalls.length).toBe(1)
|
||||
|
||||
const providerRequestBody = JSON.parse(providerCalls[0].options.body)
|
||||
expect(providerRequestBody.tools.length).toBe(3)
|
||||
expect(mockExecuteProviderRequest).toHaveBeenCalled()
|
||||
const providerCallArgs = mockExecuteProviderRequest.mock.calls[0]
|
||||
expect(providerCallArgs[1].tools.length).toBe(3)
|
||||
})
|
||||
|
||||
it('should should fallback to discovery for MCP tools without cached schema', async () => {
|
||||
it('should fallback to discovery for MCP tools without cached schema', async () => {
|
||||
const fetchCalls: any[] = []
|
||||
|
||||
mockFetch.mockImplementation((url: string, options: any) => {
|
||||
|
||||
@@ -6,14 +6,7 @@ import { createMcpToolId } from '@/lib/mcp/utils'
|
||||
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import { getAllBlocks } from '@/blocks'
|
||||
import type { BlockOutput } from '@/blocks/types'
|
||||
import {
|
||||
AGENT,
|
||||
BlockType,
|
||||
DEFAULTS,
|
||||
HTTP,
|
||||
REFERENCE,
|
||||
stripCustomToolPrefix,
|
||||
} from '@/executor/constants'
|
||||
import { AGENT, BlockType, DEFAULTS, REFERENCE, stripCustomToolPrefix } from '@/executor/constants'
|
||||
import { memoryService } from '@/executor/handlers/agent/memory'
|
||||
import type {
|
||||
AgentInputs,
|
||||
@@ -23,7 +16,7 @@ import type {
|
||||
} from '@/executor/handlers/agent/types'
|
||||
import type { BlockHandler, ExecutionContext, StreamingExecution } from '@/executor/types'
|
||||
import { collectBlockData } from '@/executor/utils/block-data'
|
||||
import { buildAPIUrl, buildAuthHeaders, extractAPIErrorMessage } from '@/executor/utils/http'
|
||||
import { buildAPIUrl, buildAuthHeaders } from '@/executor/utils/http'
|
||||
import { stringifyJSON } from '@/executor/utils/json'
|
||||
import {
|
||||
validateBlockType,
|
||||
@@ -52,11 +45,9 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
block: SerializedBlock,
|
||||
inputs: AgentInputs
|
||||
): Promise<BlockOutput | StreamingExecution> {
|
||||
// Filter out unavailable MCP tools early so they don't appear in logs/inputs
|
||||
const filteredTools = await this.filterUnavailableMcpTools(ctx, inputs.tools || [])
|
||||
const filteredInputs = { ...inputs, tools: filteredTools }
|
||||
|
||||
// Validate tool permissions before processing
|
||||
await this.validateToolPermissions(ctx, filteredInputs.tools || [])
|
||||
|
||||
const responseFormat = this.parseResponseFormat(filteredInputs.responseFormat)
|
||||
@@ -80,13 +71,7 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
streaming: streamingConfig.shouldUseStreaming ?? false,
|
||||
})
|
||||
|
||||
const result = await this.executeProviderRequest(
|
||||
ctx,
|
||||
providerRequest,
|
||||
block,
|
||||
responseFormat,
|
||||
filteredInputs
|
||||
)
|
||||
const result = await this.executeProviderRequest(ctx, providerRequest, block, responseFormat)
|
||||
|
||||
if (this.isStreamingExecution(result)) {
|
||||
if (filteredInputs.memoryType && filteredInputs.memoryType !== 'none') {
|
||||
@@ -996,157 +981,60 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
ctx: ExecutionContext,
|
||||
providerRequest: any,
|
||||
block: SerializedBlock,
|
||||
responseFormat: any,
|
||||
inputs: AgentInputs
|
||||
responseFormat: any
|
||||
): Promise<BlockOutput | StreamingExecution> {
|
||||
const providerId = providerRequest.provider
|
||||
const model = providerRequest.model
|
||||
const providerStartTime = Date.now()
|
||||
|
||||
try {
|
||||
const isBrowser = typeof window !== 'undefined'
|
||||
let finalApiKey: string | undefined = providerRequest.apiKey
|
||||
|
||||
if (!isBrowser) {
|
||||
return this.executeServerSide(
|
||||
ctx,
|
||||
providerRequest,
|
||||
providerId,
|
||||
model,
|
||||
block,
|
||||
responseFormat,
|
||||
providerStartTime
|
||||
if (providerId === 'vertex' && providerRequest.vertexCredential) {
|
||||
finalApiKey = await this.resolveVertexCredential(
|
||||
providerRequest.vertexCredential,
|
||||
ctx.workflowId
|
||||
)
|
||||
}
|
||||
return this.executeBrowserSide(
|
||||
ctx,
|
||||
providerRequest,
|
||||
block,
|
||||
responseFormat,
|
||||
providerStartTime,
|
||||
inputs
|
||||
)
|
||||
|
||||
const { blockData, blockNameMapping } = collectBlockData(ctx)
|
||||
|
||||
const response = await executeProviderRequest(providerId, {
|
||||
model,
|
||||
systemPrompt: 'systemPrompt' in providerRequest ? providerRequest.systemPrompt : undefined,
|
||||
context: 'context' in providerRequest ? providerRequest.context : undefined,
|
||||
tools: providerRequest.tools,
|
||||
temperature: providerRequest.temperature,
|
||||
maxTokens: providerRequest.maxTokens,
|
||||
apiKey: finalApiKey,
|
||||
azureEndpoint: providerRequest.azureEndpoint,
|
||||
azureApiVersion: providerRequest.azureApiVersion,
|
||||
vertexProject: providerRequest.vertexProject,
|
||||
vertexLocation: providerRequest.vertexLocation,
|
||||
bedrockAccessKeyId: providerRequest.bedrockAccessKeyId,
|
||||
bedrockSecretKey: providerRequest.bedrockSecretKey,
|
||||
bedrockRegion: providerRequest.bedrockRegion,
|
||||
responseFormat: providerRequest.responseFormat,
|
||||
workflowId: providerRequest.workflowId,
|
||||
workspaceId: ctx.workspaceId,
|
||||
stream: providerRequest.stream,
|
||||
messages: 'messages' in providerRequest ? providerRequest.messages : undefined,
|
||||
environmentVariables: ctx.environmentVariables || {},
|
||||
workflowVariables: ctx.workflowVariables || {},
|
||||
blockData,
|
||||
blockNameMapping,
|
||||
isDeployedContext: ctx.isDeployedContext,
|
||||
reasoningEffort: providerRequest.reasoningEffort,
|
||||
verbosity: providerRequest.verbosity,
|
||||
})
|
||||
|
||||
return this.processProviderResponse(response, block, responseFormat)
|
||||
} catch (error) {
|
||||
this.handleExecutionError(error, providerStartTime, providerId, model, ctx, block)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private async executeServerSide(
|
||||
ctx: ExecutionContext,
|
||||
providerRequest: any,
|
||||
providerId: string,
|
||||
model: string,
|
||||
block: SerializedBlock,
|
||||
responseFormat: any,
|
||||
providerStartTime: number
|
||||
) {
|
||||
let finalApiKey: string | undefined = providerRequest.apiKey
|
||||
|
||||
if (providerId === 'vertex' && providerRequest.vertexCredential) {
|
||||
finalApiKey = await this.resolveVertexCredential(
|
||||
providerRequest.vertexCredential,
|
||||
ctx.workflowId
|
||||
)
|
||||
}
|
||||
|
||||
const { blockData, blockNameMapping } = collectBlockData(ctx)
|
||||
|
||||
const response = await executeProviderRequest(providerId, {
|
||||
model,
|
||||
systemPrompt: 'systemPrompt' in providerRequest ? providerRequest.systemPrompt : undefined,
|
||||
context: 'context' in providerRequest ? providerRequest.context : undefined,
|
||||
tools: providerRequest.tools,
|
||||
temperature: providerRequest.temperature,
|
||||
maxTokens: providerRequest.maxTokens,
|
||||
apiKey: finalApiKey,
|
||||
azureEndpoint: providerRequest.azureEndpoint,
|
||||
azureApiVersion: providerRequest.azureApiVersion,
|
||||
vertexProject: providerRequest.vertexProject,
|
||||
vertexLocation: providerRequest.vertexLocation,
|
||||
bedrockAccessKeyId: providerRequest.bedrockAccessKeyId,
|
||||
bedrockSecretKey: providerRequest.bedrockSecretKey,
|
||||
bedrockRegion: providerRequest.bedrockRegion,
|
||||
responseFormat: providerRequest.responseFormat,
|
||||
workflowId: providerRequest.workflowId,
|
||||
workspaceId: ctx.workspaceId,
|
||||
stream: providerRequest.stream,
|
||||
messages: 'messages' in providerRequest ? providerRequest.messages : undefined,
|
||||
environmentVariables: ctx.environmentVariables || {},
|
||||
workflowVariables: ctx.workflowVariables || {},
|
||||
blockData,
|
||||
blockNameMapping,
|
||||
isDeployedContext: ctx.isDeployedContext,
|
||||
})
|
||||
|
||||
return this.processProviderResponse(response, block, responseFormat)
|
||||
}
|
||||
|
||||
private async executeBrowserSide(
|
||||
ctx: ExecutionContext,
|
||||
providerRequest: any,
|
||||
block: SerializedBlock,
|
||||
responseFormat: any,
|
||||
providerStartTime: number,
|
||||
inputs: AgentInputs
|
||||
) {
|
||||
const url = buildAPIUrl('/api/providers')
|
||||
const response = await fetch(url.toString(), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': HTTP.CONTENT_TYPE.JSON },
|
||||
body: stringifyJSON(providerRequest),
|
||||
signal: AbortSignal.timeout(AGENT.REQUEST_TIMEOUT),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = await extractAPIErrorMessage(response)
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('Content-Type')
|
||||
if (contentType?.includes(HTTP.CONTENT_TYPE.EVENT_STREAM)) {
|
||||
return this.handleStreamingResponse(response, block, ctx, inputs)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
return this.processProviderResponse(result, block, responseFormat)
|
||||
}
|
||||
|
||||
private async handleStreamingResponse(
|
||||
response: Response,
|
||||
block: SerializedBlock,
|
||||
_ctx?: ExecutionContext,
|
||||
_inputs?: AgentInputs
|
||||
): Promise<StreamingExecution> {
|
||||
const executionDataHeader = response.headers.get('X-Execution-Data')
|
||||
|
||||
if (executionDataHeader) {
|
||||
try {
|
||||
const executionData = JSON.parse(executionDataHeader)
|
||||
return {
|
||||
stream: response.body!,
|
||||
execution: {
|
||||
success: executionData.success,
|
||||
output: executionData.output || {},
|
||||
error: executionData.error,
|
||||
logs: [],
|
||||
metadata: executionData.metadata || {
|
||||
duration: DEFAULTS.EXECUTION_TIME,
|
||||
startTime: new Date().toISOString(),
|
||||
},
|
||||
isStreaming: true,
|
||||
blockId: block.id,
|
||||
blockName: block.metadata?.name,
|
||||
blockType: block.metadata?.id,
|
||||
} as any,
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to parse execution data from header:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return this.createMinimalStreamingExecution(response.body!)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a Vertex AI OAuth credential to an access token
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user