Skip to content
Merged
19 changes: 19 additions & 0 deletions packages/agentflow/src/core/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import type { InternalAxiosRequestConfig } from 'axios'

import type { NodeData } from './node'

export type RequestInterceptor = (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig

export interface Chatflow {
Expand All @@ -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 changes: 9 additions & 0 deletions packages/agentflow/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -24,6 +29,9 @@ export type {
// API
ApiResponse,
Chatflow,
ChatModel,
Credential,
Tool,
// Context
ConfigContextValue,
// Flow data
Expand All @@ -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'
Expand Down
37 changes: 37 additions & 0 deletions packages/agentflow/src/infrastructure/api/credentials.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
})
28 changes: 28 additions & 0 deletions packages/agentflow/src/infrastructure/api/credentials.ts
Original file line number Diff line number Diff line change
@@ -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>
4 changes: 4 additions & 0 deletions packages/agentflow/src/infrastructure/api/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Original file line number Diff line number Diff line change
@@ -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()
})
})
48 changes: 48 additions & 0 deletions packages/agentflow/src/infrastructure/api/loadMethodRegistry.ts
Original file line number Diff line number Diff line change
@@ -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>> = {
Comment thread
j-sanaa marked this conversation as resolved.
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]
}
37 changes: 37 additions & 0 deletions packages/agentflow/src/infrastructure/api/models.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
})
28 changes: 28 additions & 0 deletions packages/agentflow/src/infrastructure/api/models.ts
Original file line number Diff line number Diff line change
@@ -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>
26 changes: 26 additions & 0 deletions packages/agentflow/src/infrastructure/api/tools.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
})
20 changes: 20 additions & 0 deletions packages/agentflow/src/infrastructure/api/tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { AxiosInstance } from 'axios'

import type { Tool } from '@/core/types'

/**
* Create tools API functions bound to a client instance
Comment thread
j-sanaa marked this conversation as resolved.
*/
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>
Loading