Feature/agentflow v2 (#4298)

* agent flow v2

* chat message background

* conditon agent flow

* add sticky note

* update human input dynamic prompt

* add HTTP node

* add default tool icon

* fix export duplicate agentflow v2

* add agentflow v2 marketplaces

* refractor memoization, add iteration nodes

* add agentflow v2 templates

* add agentflow generator

* add migration scripts for mysql, mariadb, posrgres and fix date filters for executions

* update agentflow chat history config

* fix get all flows error after deletion and rename

* add previous nodes from parent node

* update generator prompt

* update run time state when using iteration nodes

* prevent looping connection, prevent duplication of start node, add executeflow node, add nodes agentflow, chat history variable

* update embed

* convert form input to string

* bump openai version

* add react rewards

* add prompt generator to prediction queue

* add array schema to overrideconfig

* UI touchup

* update embedded chat version

* fix node info dialog

* update start node and loop default iteration

* update UI fixes for agentflow v2

* fix async drop down

* add export import to agentflowsv2, executions, fix UI bugs

* add default empty object to flowlisttable

* add ability to share trace link publicly, allow MCP tool use for Agent and Assistant

* add runtime message length to variable, display conditions on UI

* fix array validation

* add ability to add knowledge from vector store and embeddings for agent

* add agent tool require human input

* add ephemeral memory to start node

* update agent flow node to show vs and embeddings icons

* feat: add import chat data functionality for AgentFlowV2

* feat: set chatMessage.executionId to null if not found in import JSON file or database

* fix: MariaDB execution migration script to utf8mb4_unicode_520_ci

---------

Co-authored-by: Ong Chung Yau <33013947+chungyau97@users.noreply.github.com>
Co-authored-by: chungyau97 <chungyau97@gmail.com>
This commit is contained in:
Henry Heng
2025-05-10 10:21:26 +08:00
committed by GitHub
parent 82e6f43b5c
commit 7924fbce0d
216 changed files with 33304 additions and 5269 deletions
+177 -115
View File
@@ -29,7 +29,7 @@ import { ICommonObject, IDatabaseEntity, INodeData, IServerSideEventStreamer } f
import { LangWatch, LangWatchSpan, LangWatchTrace, autoconvertTypedValues } from 'langwatch'
import { DataSource } from 'typeorm'
import { ChatGenerationChunk } from '@langchain/core/outputs'
import { AIMessageChunk } from '@langchain/core/messages'
import { AIMessageChunk, BaseMessageLike } from '@langchain/core/messages'
import { Serialized } from '@langchain/core/load/serializable'
interface AgentRun extends Run {
@@ -635,137 +635,184 @@ export const additionalCallbacks = async (nodeData: INodeData, options: ICommonO
}
export class AnalyticHandler {
nodeData: INodeData
options: ICommonObject = {}
handlers: ICommonObject = {}
private static instances: Map<string, AnalyticHandler> = new Map()
private nodeData: INodeData
private options: ICommonObject
private handlers: ICommonObject = {}
private initialized: boolean = false
private analyticsConfig: string | undefined
private chatId: string
private createdAt: number
constructor(nodeData: INodeData, options: ICommonObject) {
this.options = options
private constructor(nodeData: INodeData, options: ICommonObject) {
this.nodeData = nodeData
this.init()
this.options = options
this.analyticsConfig = options.analytic
this.chatId = options.chatId
this.createdAt = Date.now()
}
static getInstance(nodeData: INodeData, options: ICommonObject): AnalyticHandler {
const chatId = options.chatId
if (!chatId) throw new Error('ChatId is required for analytics')
// Reset instance if analytics config changed for this chat
const instance = AnalyticHandler.instances.get(chatId)
if (instance?.analyticsConfig !== options.analytic) {
AnalyticHandler.resetInstance(chatId)
}
if (!AnalyticHandler.instances.get(chatId)) {
AnalyticHandler.instances.set(chatId, new AnalyticHandler(nodeData, options))
}
return AnalyticHandler.instances.get(chatId)!
}
static resetInstance(chatId: string): void {
AnalyticHandler.instances.delete(chatId)
}
// Keep this as backup for orphaned instances
static cleanup(maxAge: number = 3600000): void {
const now = Date.now()
for (const [chatId, instance] of AnalyticHandler.instances) {
if (now - instance.createdAt > maxAge) {
AnalyticHandler.resetInstance(chatId)
}
}
}
async init() {
if (this.initialized) return
try {
if (!this.options.analytic) return
const analytic = JSON.parse(this.options.analytic)
for (const provider in analytic) {
const providerStatus = analytic[provider].status as boolean
if (providerStatus) {
const credentialId = analytic[provider].credentialId as string
const credentialData = await getCredentialData(credentialId ?? '', this.options)
if (provider === 'langSmith') {
const langSmithProject = analytic[provider].projectName as string
const langSmithApiKey = getCredentialParam('langSmithApiKey', credentialData, this.nodeData)
const langSmithEndpoint = getCredentialParam('langSmithEndpoint', credentialData, this.nodeData)
const client = new LangsmithClient({
apiUrl: langSmithEndpoint ?? 'https://api.smith.langchain.com',
apiKey: langSmithApiKey
})
this.handlers['langSmith'] = { client, langSmithProject }
} else if (provider === 'langFuse') {
const release = analytic[provider].release as string
const langFuseSecretKey = getCredentialParam('langFuseSecretKey', credentialData, this.nodeData)
const langFusePublicKey = getCredentialParam('langFusePublicKey', credentialData, this.nodeData)
const langFuseEndpoint = getCredentialParam('langFuseEndpoint', credentialData, this.nodeData)
const langfuse = new Langfuse({
secretKey: langFuseSecretKey,
publicKey: langFusePublicKey,
baseUrl: langFuseEndpoint ?? 'https://cloud.langfuse.com',
sdkIntegration: 'Flowise',
release
})
this.handlers['langFuse'] = { client: langfuse }
} else if (provider === 'lunary') {
const lunaryPublicKey = getCredentialParam('lunaryAppId', credentialData, this.nodeData)
const lunaryEndpoint = getCredentialParam('lunaryEndpoint', credentialData, this.nodeData)
lunary.init({
publicKey: lunaryPublicKey,
apiUrl: lunaryEndpoint,
runtime: 'flowise'
})
this.handlers['lunary'] = { client: lunary }
} else if (provider === 'langWatch') {
const langWatchApiKey = getCredentialParam('langWatchApiKey', credentialData, this.nodeData)
const langWatchEndpoint = getCredentialParam('langWatchEndpoint', credentialData, this.nodeData)
const langwatch = new LangWatch({
apiKey: langWatchApiKey,
endpoint: langWatchEndpoint
})
this.handlers['langWatch'] = { client: langwatch }
} else if (provider === 'arize') {
const arizeApiKey = getCredentialParam('arizeApiKey', credentialData, this.nodeData)
const arizeSpaceId = getCredentialParam('arizeSpaceId', credentialData, this.nodeData)
const arizeEndpoint = getCredentialParam('arizeEndpoint', credentialData, this.nodeData)
const arizeProject = analytic[provider].projectName as string
let arizeOptions: ArizeTracerOptions = {
apiKey: arizeApiKey,
spaceId: arizeSpaceId,
baseUrl: arizeEndpoint ?? 'https://otlp.arize.com',
projectName: arizeProject ?? 'default',
sdkIntegration: 'Flowise',
enableCallback: false
}
const arize: Tracer | undefined = getArizeTracer(arizeOptions)
const rootSpan: Span | undefined = undefined
this.handlers['arize'] = { client: arize, arizeProject, rootSpan }
} else if (provider === 'phoenix') {
const phoenixApiKey = getCredentialParam('phoenixApiKey', credentialData, this.nodeData)
const phoenixEndpoint = getCredentialParam('phoenixEndpoint', credentialData, this.nodeData)
const phoenixProject = analytic[provider].projectName as string
let phoenixOptions: PhoenixTracerOptions = {
apiKey: phoenixApiKey,
baseUrl: phoenixEndpoint ?? 'https://app.phoenix.arize.com',
projectName: phoenixProject ?? 'default',
sdkIntegration: 'Flowise',
enableCallback: false
}
const phoenix: Tracer | undefined = getPhoenixTracer(phoenixOptions)
const rootSpan: Span | undefined = undefined
this.handlers['phoenix'] = { client: phoenix, phoenixProject, rootSpan }
} else if (provider === 'opik') {
const opikApiKey = getCredentialParam('opikApiKey', credentialData, this.nodeData)
const opikEndpoint = getCredentialParam('opikUrl', credentialData, this.nodeData)
const opikWorkspace = getCredentialParam('opikWorkspace', credentialData, this.nodeData)
const opikProject = analytic[provider].opikProjectName as string
let opikOptions: OpikTracerOptions = {
apiKey: opikApiKey,
baseUrl: opikEndpoint ?? 'https://www.comet.com/opik/api',
projectName: opikProject ?? 'default',
workspace: opikWorkspace ?? 'default',
sdkIntegration: 'Flowise',
enableCallback: false
}
const opik: Tracer | undefined = getOpikTracer(opikOptions)
const rootSpan: Span | undefined = undefined
this.handlers['opik'] = { client: opik, opikProject, rootSpan }
}
await this.initializeProvider(provider, analytic[provider], credentialData)
}
}
this.initialized = true
} catch (e) {
throw new Error(e)
}
}
// Add getter for handlers (useful for debugging)
getHandlers(): ICommonObject {
return this.handlers
}
async initializeProvider(provider: string, providerConfig: any, credentialData: any) {
if (provider === 'langSmith') {
const langSmithProject = providerConfig.projectName as string
const langSmithApiKey = getCredentialParam('langSmithApiKey', credentialData, this.nodeData)
const langSmithEndpoint = getCredentialParam('langSmithEndpoint', credentialData, this.nodeData)
const client = new LangsmithClient({
apiUrl: langSmithEndpoint ?? 'https://api.smith.langchain.com',
apiKey: langSmithApiKey
})
this.handlers['langSmith'] = { client, langSmithProject }
} else if (provider === 'langFuse') {
const release = providerConfig.release as string
const langFuseSecretKey = getCredentialParam('langFuseSecretKey', credentialData, this.nodeData)
const langFusePublicKey = getCredentialParam('langFusePublicKey', credentialData, this.nodeData)
const langFuseEndpoint = getCredentialParam('langFuseEndpoint', credentialData, this.nodeData)
const langfuse = new Langfuse({
secretKey: langFuseSecretKey,
publicKey: langFusePublicKey,
baseUrl: langFuseEndpoint ?? 'https://cloud.langfuse.com',
sdkIntegration: 'Flowise',
release
})
this.handlers['langFuse'] = { client: langfuse }
} else if (provider === 'lunary') {
const lunaryPublicKey = getCredentialParam('lunaryAppId', credentialData, this.nodeData)
const lunaryEndpoint = getCredentialParam('lunaryEndpoint', credentialData, this.nodeData)
lunary.init({
publicKey: lunaryPublicKey,
apiUrl: lunaryEndpoint,
runtime: 'flowise'
})
this.handlers['lunary'] = { client: lunary }
} else if (provider === 'langWatch') {
const langWatchApiKey = getCredentialParam('langWatchApiKey', credentialData, this.nodeData)
const langWatchEndpoint = getCredentialParam('langWatchEndpoint', credentialData, this.nodeData)
const langwatch = new LangWatch({
apiKey: langWatchApiKey,
endpoint: langWatchEndpoint
})
this.handlers['langWatch'] = { client: langwatch }
} else if (provider === 'arize') {
const arizeApiKey = getCredentialParam('arizeApiKey', credentialData, this.nodeData)
const arizeSpaceId = getCredentialParam('arizeSpaceId', credentialData, this.nodeData)
const arizeEndpoint = getCredentialParam('arizeEndpoint', credentialData, this.nodeData)
const arizeProject = providerConfig.projectName as string
let arizeOptions: ArizeTracerOptions = {
apiKey: arizeApiKey,
spaceId: arizeSpaceId,
baseUrl: arizeEndpoint ?? 'https://otlp.arize.com',
projectName: arizeProject ?? 'default',
sdkIntegration: 'Flowise',
enableCallback: false
}
const arize: Tracer | undefined = getArizeTracer(arizeOptions)
const rootSpan: Span | undefined = undefined
this.handlers['arize'] = { client: arize, arizeProject, rootSpan }
} else if (provider === 'phoenix') {
const phoenixApiKey = getCredentialParam('phoenixApiKey', credentialData, this.nodeData)
const phoenixEndpoint = getCredentialParam('phoenixEndpoint', credentialData, this.nodeData)
const phoenixProject = providerConfig.projectName as string
let phoenixOptions: PhoenixTracerOptions = {
apiKey: phoenixApiKey,
baseUrl: phoenixEndpoint ?? 'https://app.phoenix.arize.com',
projectName: phoenixProject ?? 'default',
sdkIntegration: 'Flowise',
enableCallback: false
}
const phoenix: Tracer | undefined = getPhoenixTracer(phoenixOptions)
const rootSpan: Span | undefined = undefined
this.handlers['phoenix'] = { client: phoenix, phoenixProject, rootSpan }
} else if (provider === 'opik') {
const opikApiKey = getCredentialParam('opikApiKey', credentialData, this.nodeData)
const opikEndpoint = getCredentialParam('opikUrl', credentialData, this.nodeData)
const opikWorkspace = getCredentialParam('opikWorkspace', credentialData, this.nodeData)
const opikProject = providerConfig.opikProjectName as string
let opikOptions: OpikTracerOptions = {
apiKey: opikApiKey,
baseUrl: opikEndpoint ?? 'https://www.comet.com/opik/api',
projectName: opikProject ?? 'default',
workspace: opikWorkspace ?? 'default',
sdkIntegration: 'Flowise',
enableCallback: false
}
const opik: Tracer | undefined = getOpikTracer(opikOptions)
const rootSpan: Span | undefined = undefined
this.handlers['opik'] = { client: opik, opikProject, rootSpan }
}
}
async onChainStart(name: string, input: string, parentIds?: ICommonObject) {
const returnIds: ICommonObject = {
langSmith: {},
@@ -1077,6 +1124,11 @@ export class AnalyticHandler {
chainSpan.end()
}
}
if (shutdown) {
// Cleanup this instance when chain ends
AnalyticHandler.resetInstance(this.chatId)
}
}
async onChainError(returnIds: ICommonObject, error: string | object, shutdown = false) {
@@ -1155,9 +1207,14 @@ export class AnalyticHandler {
chainSpan.end()
}
}
if (shutdown) {
// Cleanup this instance when chain ends
AnalyticHandler.resetInstance(this.chatId)
}
}
async onLLMStart(name: string, input: string, parentIds: ICommonObject) {
async onLLMStart(name: string, input: string | BaseMessageLike[], parentIds: ICommonObject) {
const returnIds: ICommonObject = {
langSmith: {},
langFuse: {},
@@ -1169,13 +1226,18 @@ export class AnalyticHandler {
if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) {
const parentRun: RunTree | undefined = this.handlers['langSmith'].chainRun[parentIds['langSmith'].chainRun]
if (parentRun) {
const inputs: any = {}
if (Array.isArray(input)) {
inputs.messages = input
} else {
inputs.prompts = [input]
}
const childLLMRun = await parentRun.createChild({
name,
run_type: 'llm',
inputs: {
prompts: [input]
}
inputs
})
await childLLMRun.postRun()
this.handlers['langSmith'].llmRun = { [childLLMRun.id]: childLLMRun }