feat(agentflow) Create Async Data Fetching Infrastructure in agentflow (#5937)

* initial flow

* Fixed  export types and added example application to test end to end flow

* Fix build errors

* fix gemini comments

* Test bug fix

* Fixed comments

* Removed API inspector example

* Refactor imports in index.ts for clarity

* Refactor exports and update type definitions

* Remove empty line in index.ts

Removed an empty line for cleaner code formatting.
This commit is contained in:
j-sanaa
2026-03-10 17:17:04 -07:00
committed by GitHub
parent c7f18f51bb
commit 20eac8f1ec
13 changed files with 371 additions and 4 deletions
+19
View File
@@ -4,6 +4,8 @@
import type { InternalAxiosRequestConfig } from 'axios'
import type { NodeData } from './node'
export type RequestInterceptor = (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig
export interface Chatflow {
@@ -23,3 +25,20 @@ export interface ApiResponse<T> {
data: T
status: number
}
export type ChatModel = Omit<NodeData, 'id'>
export interface Tool {
label: string
name: string
imageSrc?: string
}
export interface Credential {
id: string
name: string
credentialName: string
createdDate?: string
updatedDate?: string
workspaceID?: string
}
+9
View File
@@ -14,7 +14,12 @@ export { useAgentflow } from './useAgentflow'
// Context hooks (for advanced usage)
export { useAgentflowContext, useApiContext, useConfigContext } from './infrastructure/store'
// Load method registry (for dynamic API dispatch from node input loadMethod strings)
export type { ApiServices } from './infrastructure/api'
export { getLoadMethod } from './infrastructure/api'
// Types
/* eslint-disable simple-import-sort/exports */
export type {
// Instance
AgentFlowInstance,
@@ -24,6 +29,9 @@ export type {
// API
ApiResponse,
Chatflow,
ChatModel,
Credential,
Tool,
// Context
ConfigContextValue,
// Flow data
@@ -48,6 +56,7 @@ export type {
ValidationResult,
Viewport
} from './core/types'
/* eslint-enable simple-import-sort/exports */
// Utilities (for advanced usage)
export { filterNodesByComponents, isAgentflowNode } from './core/node-catalog'
@@ -0,0 +1,37 @@
import type { AxiosInstance } from 'axios'
import { bindCredentialsApi } from './credentials'
const mockClient = {
get: jest.fn()
} as unknown as jest.Mocked<AxiosInstance>
beforeEach(() => {
jest.clearAllMocks()
})
describe('bindCredentialsApi', () => {
const api = bindCredentialsApi(mockClient)
describe('getAllCredentials', () => {
it('should call GET /credentials', async () => {
const mockCredentials = [{ id: '1', name: 'My OpenAI Key', credentialName: 'openAIApi' }]
;(mockClient.get as jest.Mock).mockResolvedValue({ data: mockCredentials })
const result = await api.getAllCredentials()
expect(mockClient.get).toHaveBeenCalledWith('/credentials')
expect(result).toEqual(mockCredentials)
})
})
describe('getCredentialsByName', () => {
it('should call GET /credentials with credentialName param', async () => {
const mockCredentials = [{ id: '1', name: 'My OpenAI Key', credentialName: 'openAIApi' }]
;(mockClient.get as jest.Mock).mockResolvedValue({ data: mockCredentials })
const result = await api.getCredentialsByName('openAIApi')
expect(mockClient.get).toHaveBeenCalledWith('/credentials', { params: { credentialName: 'openAIApi' } })
expect(result).toEqual(mockCredentials)
})
})
})
@@ -0,0 +1,28 @@
import type { AxiosInstance } from 'axios'
import type { Credential } from '@/core/types'
/**
* Create credentials API functions bound to a client instance
*/
export function bindCredentialsApi(client: AxiosInstance) {
return {
/**
* Get all credentials
*/
getAllCredentials: async (): Promise<Credential[]> => {
const response = await client.get('/credentials')
return response.data
},
/**
* Get credentials filtered by component credential name
*/
getCredentialsByName: async (credentialName: string): Promise<Credential[]> => {
const response = await client.get('/credentials', { params: { credentialName } })
return response.data
}
}
}
export type CredentialsApi = ReturnType<typeof bindCredentialsApi>
@@ -1,4 +1,8 @@
// API infrastructure - External data layer
export { type ChatflowsApi, createChatflowsApi } from './chatflows'
export { createApiClient } from './client'
export { bindCredentialsApi, type CredentialsApi } from './credentials'
export { type ApiServices, getLoadMethod, loadMethodRegistry } from './loadMethodRegistry'
export { bindChatModelsApi, type ChatModelsApi } from './models'
export { createNodesApi, type NodesApi } from './nodes'
export { bindToolsApi, type ToolsApi } from './tools'
@@ -0,0 +1,80 @@
import type { ApiServices } from './loadMethodRegistry'
import { getLoadMethod, loadMethodRegistry } from './loadMethodRegistry'
const mockApis: ApiServices = {
chatModelsApi: {
getChatModels: jest.fn(),
getModelsByProvider: jest.fn()
},
toolsApi: {
getAllTools: jest.fn()
},
credentialsApi: {
getAllCredentials: jest.fn(),
getCredentialsByName: jest.fn()
}
}
beforeEach(() => {
jest.clearAllMocks()
})
describe('loadMethodRegistry', () => {
describe('listChatModels', () => {
it('should call chatModelsApi.getChatModels()', async () => {
const mockModels = [{ name: 'gpt-4', label: 'GPT-4' }]
;(mockApis.chatModelsApi.getChatModels as jest.Mock).mockResolvedValue(mockModels)
const result = await loadMethodRegistry['listChatModels'](mockApis)
expect(mockApis.chatModelsApi.getChatModels).toHaveBeenCalled()
expect(result).toEqual(mockModels)
})
})
describe('listTools', () => {
it('should call toolsApi.getAllTools()', async () => {
const mockTools = [{ id: '1', name: 'Calculator' }]
;(mockApis.toolsApi.getAllTools as jest.Mock).mockResolvedValue(mockTools)
const result = await loadMethodRegistry['listTools'](mockApis)
expect(mockApis.toolsApi.getAllTools).toHaveBeenCalled()
expect(result).toEqual(mockTools)
})
})
describe('listCredentials', () => {
it('should call credentialsApi.getCredentialsByName() with params.name', async () => {
const mockCredentials = [{ id: '1', name: 'My Key', credentialName: 'openAIApi' }]
;(mockApis.credentialsApi.getCredentialsByName as jest.Mock).mockResolvedValue(mockCredentials)
const result = await loadMethodRegistry['listCredentials'](mockApis, { name: 'openAIApi' })
expect(mockApis.credentialsApi.getCredentialsByName).toHaveBeenCalledWith('openAIApi')
expect(result).toEqual(mockCredentials)
})
})
})
describe('getLoadMethod', () => {
it('should return the registry function for a known key', () => {
const fn = getLoadMethod('listChatModels')
expect(fn).toBeDefined()
expect(typeof fn).toBe('function')
})
it('should return the registry function for listTools', () => {
const fn = getLoadMethod('listTools')
expect(fn).toBeDefined()
expect(typeof fn).toBe('function')
})
it('should return the registry function for listCredentials', () => {
const fn = getLoadMethod('listCredentials')
expect(fn).toBeDefined()
expect(typeof fn).toBe('function')
})
it('should return undefined for an unknown key', () => {
const fn = getLoadMethod('unknownMethod')
expect(fn).toBeUndefined()
})
})
@@ -0,0 +1,48 @@
import type { CredentialsApi } from './credentials'
import type { ChatModelsApi } from './models'
import type { ToolsApi } from './tools'
export interface ApiServices {
chatModelsApi: ChatModelsApi
toolsApi: ToolsApi
credentialsApi: CredentialsApi
}
/**
* Registry that maps `loadMethod` string keys — as declared on node `InputParam` definitions
* (e.g. `{ loadMethod: 'listTools' }`) — to functions that fetch the corresponding options
* from the Flowise API.
*
* Each entry receives the shared {@link ApiServices} instance and an optional `params` object,
* and must return a `Promise` of the option values to populate the node's dropdown.
*
* ### Built-in entries
* - `listChatModels` — fetches available chat models via `GET /assistants/components/chatmodels`
* - `listTools` — fetches available tool components via `POST /node-load-method/toolAgentflow`
* - `listCredentials` — fetches credentials filtered by `params.name` (credential component name)
* via `GET /credentials?credentialName=<name>`
*
*/
export const loadMethodRegistry: Record<string, (_apis: ApiServices, _params?: Record<string, unknown>) => Promise<unknown>> = {
listChatModels: (apis) => apis.chatModelsApi.getChatModels(),
listTools: (apis) => apis.toolsApi.getAllTools(),
listCredentials: (apis, params) => {
const name = params?.name
if (typeof name !== 'string') {
return Promise.reject(new Error('`listCredentials` requires a string `name` parameter.'))
}
return apis.credentialsApi.getCredentialsByName(name)
}
}
/**
* Looks up a load method handler by its string key.
*
* Returns `undefined` if no handler is registered for the given name,
* which callers should treat as a no-op or fallback.
*
* @param name - The `loadMethod` key declared on a node `InputParam`
*/
export function getLoadMethod(name: string): ((_apis: ApiServices, _params?: Record<string, unknown>) => Promise<unknown>) | undefined {
return loadMethodRegistry[name]
}
@@ -0,0 +1,37 @@
import type { AxiosInstance } from 'axios'
import { bindChatModelsApi } from './models'
const mockClient = {
get: jest.fn()
} as unknown as jest.Mocked<AxiosInstance>
beforeEach(() => {
jest.clearAllMocks()
})
describe('bindChatModelsApi', () => {
const api = bindChatModelsApi(mockClient)
describe('getChatModels', () => {
it('should call GET /assistants/components/chatmodels', async () => {
const mockModels = [{ name: 'gpt-4', label: 'GPT-4' }]
;(mockClient.get as jest.Mock).mockResolvedValue({ data: mockModels })
const result = await api.getChatModels()
expect(mockClient.get).toHaveBeenCalledWith('/assistants/components/chatmodels')
expect(result).toEqual(mockModels)
})
})
describe('getModelsByProvider', () => {
it('should call GET /assistants/components/chatmodels with provider param', async () => {
const mockModels = [{ name: 'gpt-4', label: 'GPT-4' }]
;(mockClient.get as jest.Mock).mockResolvedValue({ data: mockModels })
const result = await api.getModelsByProvider('openai')
expect(mockClient.get).toHaveBeenCalledWith('/assistants/components/chatmodels', { params: { provider: 'openai' } })
expect(result).toEqual(mockModels)
})
})
})
@@ -0,0 +1,28 @@
import type { AxiosInstance } from 'axios'
import type { ChatModel } from '@/core/types'
/**
* Create models API functions bound to a client instance
*/
export function bindChatModelsApi(client: AxiosInstance) {
return {
/**
* Get all available chat models
*/
getChatModels: async (): Promise<ChatModel[]> => {
const response = await client.get('/assistants/components/chatmodels')
return response.data
},
/**
* Get chat models filtered by provider
*/
getModelsByProvider: async (provider: string): Promise<ChatModel[]> => {
const response = await client.get('/assistants/components/chatmodels', { params: { provider } })
return response.data
}
}
}
export type ChatModelsApi = ReturnType<typeof bindChatModelsApi>
@@ -0,0 +1,26 @@
import type { AxiosInstance } from 'axios'
import { bindToolsApi } from './tools'
const mockClient = {
post: jest.fn()
} as unknown as jest.Mocked<AxiosInstance>
beforeEach(() => {
jest.clearAllMocks()
})
describe('bindToolsApi', () => {
const api = bindToolsApi(mockClient)
describe('getAllTools', () => {
it('should POST to /node-load-method/toolAgentflow with listTools loadMethod', async () => {
const mockTools = [{ label: 'Calculator', name: 'calculator' }]
;(mockClient.post as jest.Mock).mockResolvedValue({ data: mockTools })
const result = await api.getAllTools()
expect(mockClient.post).toHaveBeenCalledWith('/node-load-method/toolAgentflow', { loadMethod: 'listTools' })
expect(result).toEqual(mockTools)
})
})
})
@@ -0,0 +1,20 @@
import type { AxiosInstance } from 'axios'
import type { Tool } from '@/core/types'
/**
* Create tools API functions bound to a client instance
*/
export function bindToolsApi(client: AxiosInstance) {
return {
/**
* Get all available tools
*/
getAllTools: async (): Promise<Tool[]> => {
const response = await client.post('/node-load-method/toolAgentflow', { loadMethod: 'listTools' })
return response.data
}
}
}
export type ToolsApi = ReturnType<typeof bindToolsApi>
@@ -9,10 +9,14 @@ import { ApiProvider, useApiContext } from './ApiContext'
jest.mock('../api', () => ({
createApiClient: jest.fn(() => 'mock-client'),
createNodesApi: jest.fn(() => ({ getAllNodes: jest.fn() })),
createChatflowsApi: jest.fn(() => ({ getAll: jest.fn() }))
createChatflowsApi: jest.fn(() => ({ getAll: jest.fn() })),
bindChatModelsApi: jest.fn(() => ({ getChatModels: jest.fn() })),
bindToolsApi: jest.fn(() => ({ getAllTools: jest.fn() })),
bindCredentialsApi: jest.fn(() => ({ getAllCredentials: jest.fn() }))
}))
const { createApiClient, createNodesApi, createChatflowsApi } = jest.requireMock('../api')
const { createApiClient, createNodesApi, createChatflowsApi, bindChatModelsApi, bindToolsApi, bindCredentialsApi } =
jest.requireMock('../api')
describe('ApiContext', () => {
beforeEach(() => jest.clearAllMocks())
@@ -34,6 +38,9 @@ describe('ApiContext', () => {
expect(result.current.client).toBe('mock-client')
expect(result.current.nodesApi).toBeDefined()
expect(result.current.chatflowsApi).toBeDefined()
expect(result.current.chatModelsApi).toBeDefined()
expect(result.current.toolsApi).toBeDefined()
expect(result.current.credentialsApi).toBeDefined()
expect(createApiClient).toHaveBeenCalledWith('http://localhost:3000', undefined, expect.any(Function))
})
@@ -56,6 +63,9 @@ describe('ApiContext', () => {
expect(createNodesApi).toHaveBeenCalledWith('mock-client')
expect(createChatflowsApi).toHaveBeenCalledWith('mock-client')
expect(bindChatModelsApi).toHaveBeenCalledWith('mock-client')
expect(bindToolsApi).toHaveBeenCalledWith('mock-client')
expect(bindCredentialsApi).toHaveBeenCalledWith('mock-client')
})
it('should use updated requestInterceptor without recreating client', () => {
@@ -4,13 +4,28 @@ import type { AxiosInstance } from 'axios'
import type { RequestInterceptor } from '@/core/types'
import { type ChatflowsApi, createApiClient, createChatflowsApi, createNodesApi, type NodesApi } from '../api'
import {
bindChatModelsApi,
bindCredentialsApi,
bindToolsApi,
type ChatflowsApi,
type ChatModelsApi,
createApiClient,
createChatflowsApi,
createNodesApi,
type CredentialsApi,
type NodesApi,
type ToolsApi
} from '../api'
interface ApiContextValue {
client: AxiosInstance
apiBaseUrl: string
nodesApi: NodesApi
chatflowsApi: ChatflowsApi
chatModelsApi: ChatModelsApi
toolsApi: ToolsApi
credentialsApi: CredentialsApi
}
const ApiContext = createContext<ApiContextValue | null>(null)
@@ -33,12 +48,18 @@ export function ApiProvider({ apiBaseUrl, token, requestInterceptor, children }:
})
const nodesApi = createNodesApi(client)
const chatflowsApi = createChatflowsApi(client)
const chatModelsApi = bindChatModelsApi(client)
const toolsApi = bindToolsApi(client)
const credentialsApi = bindCredentialsApi(client)
return {
client,
apiBaseUrl,
nodesApi,
chatflowsApi
chatflowsApi,
chatModelsApi,
toolsApi,
credentialsApi
}
}, [apiBaseUrl, token])