mirror of
https://github.com/FlowiseAI/Flowise.git
synced 2026-10-12 02:02:17 +00:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user