mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 17:01:00 +03:00
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:
@@ -8,6 +8,7 @@ import { Moderation } from '../nodes/moderation/Moderation'
|
||||
|
||||
export type NodeParamsType =
|
||||
| 'asyncOptions'
|
||||
| 'asyncMultiOptions'
|
||||
| 'options'
|
||||
| 'multiOptions'
|
||||
| 'datagrid'
|
||||
@@ -57,12 +58,13 @@ export interface INodeOptionsValue {
|
||||
label: string
|
||||
name: string
|
||||
description?: string
|
||||
imageSrc?: string
|
||||
}
|
||||
|
||||
export interface INodeOutputsValue {
|
||||
label: string
|
||||
name: string
|
||||
baseClasses: string[]
|
||||
baseClasses?: string[]
|
||||
description?: string
|
||||
hidden?: boolean
|
||||
isAnchor?: boolean
|
||||
@@ -83,10 +85,12 @@ export interface INodeParams {
|
||||
rows?: number
|
||||
list?: boolean
|
||||
acceptVariable?: boolean
|
||||
acceptNodeOutputAsVariable?: boolean
|
||||
placeholder?: string
|
||||
fileType?: string
|
||||
additionalParams?: boolean
|
||||
loadMethod?: string
|
||||
loadConfig?: boolean
|
||||
hidden?: boolean
|
||||
hideCodeExecute?: boolean
|
||||
codeExample?: string
|
||||
@@ -96,6 +100,11 @@ export interface INodeParams {
|
||||
refresh?: boolean
|
||||
freeSolo?: boolean
|
||||
loadPreviousNodes?: boolean
|
||||
array?: Array<INodeParams>
|
||||
show?: INodeDisplay
|
||||
hide?: INodeDisplay
|
||||
generateDocStoreDescription?: boolean
|
||||
generateInstruction?: boolean
|
||||
}
|
||||
|
||||
export interface INodeExecutionData {
|
||||
@@ -103,7 +112,7 @@ export interface INodeExecutionData {
|
||||
}
|
||||
|
||||
export interface INodeDisplay {
|
||||
[key: string]: string[] | string
|
||||
[key: string]: string[] | string | boolean | number | ICommonObject
|
||||
}
|
||||
|
||||
export interface INodeProperties {
|
||||
@@ -120,11 +129,15 @@ export interface INodeProperties {
|
||||
badge?: string
|
||||
deprecateMessage?: string
|
||||
hideOutput?: boolean
|
||||
hideInput?: boolean
|
||||
author?: string
|
||||
documentation?: string
|
||||
color?: string
|
||||
hint?: string
|
||||
}
|
||||
|
||||
export interface INode extends INodeProperties {
|
||||
credential?: INodeParams
|
||||
inputs?: INodeParams[]
|
||||
output?: INodeOutputsValue[]
|
||||
loadMethods?: {
|
||||
@@ -412,14 +425,19 @@ export interface IServerSideEventStreamer {
|
||||
streamCustomEvent(chatId: string, eventType: string, data: any): void
|
||||
streamSourceDocumentsEvent(chatId: string, data: any): void
|
||||
streamUsedToolsEvent(chatId: string, data: any): void
|
||||
streamCalledToolsEvent(chatId: string, data: any): void
|
||||
streamFileAnnotationsEvent(chatId: string, data: any): void
|
||||
streamToolEvent(chatId: string, data: any): void
|
||||
streamAgentReasoningEvent(chatId: string, data: any): void
|
||||
streamAgentFlowExecutedDataEvent(chatId: string, data: any): void
|
||||
streamAgentFlowEvent(chatId: string, data: any): void
|
||||
streamNextAgentEvent(chatId: string, data: any): void
|
||||
streamNextAgentFlowEvent(chatId: string, data: any): void
|
||||
streamActionEvent(chatId: string, data: any): void
|
||||
streamArtifactsEvent(chatId: string, data: any): void
|
||||
streamAbortEvent(chatId: string): void
|
||||
streamEndEvent(chatId: string): void
|
||||
streamUsageMetadataEvent(chatId: string, data: any): void
|
||||
}
|
||||
|
||||
export enum FollowUpPromptProvider {
|
||||
@@ -446,3 +464,17 @@ export type FollowUpPromptConfig = {
|
||||
status: boolean
|
||||
selectedProvider: FollowUpPromptProvider
|
||||
} & FollowUpPromptProviderConfig
|
||||
|
||||
export interface ICondition {
|
||||
type: string
|
||||
value1: CommonType
|
||||
operation: string
|
||||
value2: CommonType
|
||||
isFulfilled?: boolean
|
||||
}
|
||||
|
||||
export interface IHumanInput {
|
||||
type: 'proceed' | 'reject'
|
||||
startNodeId: string
|
||||
feedback?: string
|
||||
}
|
||||
|
||||
@@ -0,0 +1,655 @@
|
||||
import { ICommonObject } from './Interface'
|
||||
import { z } from 'zod'
|
||||
import { StructuredOutputParser } from '@langchain/core/output_parsers'
|
||||
import { isEqual, get, cloneDeep } from 'lodash'
|
||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models'
|
||||
|
||||
const ToolType = z.array(z.string()).describe('List of tools')
|
||||
|
||||
// Define a more specific NodePosition schema
|
||||
const NodePositionType = z.object({
|
||||
x: z.number().describe('X coordinate of the node position'),
|
||||
y: z.number().describe('Y coordinate of the node position')
|
||||
})
|
||||
|
||||
// Define a more specific EdgeData schema
|
||||
const EdgeDataType = z.object({
|
||||
edgeLabel: z.string().optional().describe('Label for the edge')
|
||||
})
|
||||
|
||||
// Define a basic NodeData schema to avoid using .passthrough() which might cause issues
|
||||
const NodeDataType = z
|
||||
.object({
|
||||
label: z.string().optional().describe('Label for the node'),
|
||||
name: z.string().optional().describe('Name of the node')
|
||||
})
|
||||
.optional()
|
||||
|
||||
const NodeType = z.object({
|
||||
id: z.string().describe('Unique identifier for the node'),
|
||||
type: z.enum(['agentFlow']).describe('Type of the node'),
|
||||
position: NodePositionType.describe('Position of the node in the UI'),
|
||||
width: z.number().describe('Width of the node'),
|
||||
height: z.number().describe('Height of the node'),
|
||||
selected: z.boolean().optional().describe('Whether the node is selected'),
|
||||
positionAbsolute: NodePositionType.optional().describe('Absolute position of the node'),
|
||||
data: NodeDataType
|
||||
})
|
||||
|
||||
const EdgeType = z.object({
|
||||
id: z.string().describe('Unique identifier for the edge'),
|
||||
type: z.enum(['agentFlow']).describe('Type of the node'),
|
||||
source: z.string().describe('ID of the source node'),
|
||||
sourceHandle: z.string().describe('ID of the source handle'),
|
||||
target: z.string().describe('ID of the target node'),
|
||||
targetHandle: z.string().describe('ID of the target handle'),
|
||||
data: EdgeDataType.optional().describe('Data associated with the edge')
|
||||
})
|
||||
|
||||
const NodesEdgesType = z
|
||||
.object({
|
||||
description: z.string().optional().describe('Description of the workflow'),
|
||||
usecases: z.array(z.string()).optional().describe('Use cases for this workflow'),
|
||||
nodes: z.array(NodeType).describe('Array of nodes in the workflow'),
|
||||
edges: z.array(EdgeType).describe('Array of edges connecting the nodes')
|
||||
})
|
||||
.describe('Generate Agentflowv2 nodes and edges')
|
||||
|
||||
interface NodePosition {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
interface EdgeData {
|
||||
edgeLabel?: string
|
||||
sourceColor?: string
|
||||
targetColor?: string
|
||||
isHumanInput?: boolean
|
||||
}
|
||||
|
||||
interface AgentToolConfig {
|
||||
agentSelectedTool: string
|
||||
agentSelectedToolConfig: {
|
||||
agentSelectedTool: string
|
||||
}
|
||||
}
|
||||
|
||||
interface NodeInputs {
|
||||
agentTools?: AgentToolConfig[]
|
||||
selectedTool?: string
|
||||
toolInputArgs?: Record<string, any>[]
|
||||
selectedToolConfig?: {
|
||||
selectedTool: string
|
||||
}
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
interface NodeData {
|
||||
label?: string
|
||||
name?: string
|
||||
id?: string
|
||||
inputs?: NodeInputs
|
||||
inputAnchors?: InputAnchor[]
|
||||
inputParams?: InputParam[]
|
||||
outputs?: Record<string, any>
|
||||
outputAnchors?: OutputAnchor[]
|
||||
credential?: string
|
||||
color?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
interface Node {
|
||||
id: string
|
||||
type: 'agentFlow' | 'iteration'
|
||||
position: NodePosition
|
||||
width: number
|
||||
height: number
|
||||
selected?: boolean
|
||||
positionAbsolute?: NodePosition
|
||||
data: NodeData
|
||||
parentNode?: string
|
||||
extent?: string
|
||||
}
|
||||
|
||||
interface Edge {
|
||||
id: string
|
||||
type: 'agentFlow'
|
||||
source: string
|
||||
sourceHandle: string
|
||||
target: string
|
||||
targetHandle: string
|
||||
data?: EdgeData
|
||||
label?: string
|
||||
}
|
||||
|
||||
interface InputAnchor {
|
||||
id: string
|
||||
label: string
|
||||
name: string
|
||||
type?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
interface InputParam {
|
||||
id: string
|
||||
name: string
|
||||
label?: string
|
||||
type?: string
|
||||
display?: boolean
|
||||
show?: Record<string, any>
|
||||
hide?: Record<string, any>
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
interface OutputAnchor {
|
||||
id: string
|
||||
label: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export const generateAgentflowv2 = async (config: Record<string, any>, question: string, options: ICommonObject) => {
|
||||
try {
|
||||
const result = await generateNodesEdges(config, question, options)
|
||||
|
||||
const { nodes, edges } = generateNodesData(result, config)
|
||||
|
||||
const updatedNodes = await generateSelectedTools(nodes, config, question, options)
|
||||
|
||||
const updatedEdges = updateEdges(edges, nodes)
|
||||
|
||||
return { nodes: updatedNodes, edges: updatedEdges }
|
||||
} catch (error) {
|
||||
console.error('Error generating AgentflowV2:', error)
|
||||
return { error: error.message || 'Unknown error occurred' }
|
||||
}
|
||||
}
|
||||
|
||||
const updateEdges = (edges: Edge[], nodes: Node[]): Edge[] => {
|
||||
const isMultiOutput = (source: string) => {
|
||||
return source.includes('conditionAgentflow') || source.includes('conditionAgentAgentflow') || source.includes('humanInputAgentflow')
|
||||
}
|
||||
const findNodeColor = (nodeId: string) => {
|
||||
const node = nodes.find((node) => node.id === nodeId)
|
||||
return node?.data?.color
|
||||
}
|
||||
|
||||
// filter out edges that do not exist in nodes
|
||||
edges = edges.filter((edge) => {
|
||||
return nodes.some((node) => node.id === edge.source || node.id === edge.target)
|
||||
})
|
||||
|
||||
// filter out the edge that has hideInput/hideOutput on the source/target node
|
||||
const indexToDelete = []
|
||||
for (let i = 0; i < edges.length; i += 1) {
|
||||
const edge = edges[i]
|
||||
const sourceNode = nodes.find((node) => node.id === edge.source)
|
||||
if (sourceNode?.data?.hideOutput) {
|
||||
indexToDelete.push(i)
|
||||
}
|
||||
|
||||
const targetNode = nodes.find((node) => node.id === edge.target)
|
||||
if (targetNode?.data?.hideInput) {
|
||||
indexToDelete.push(i)
|
||||
}
|
||||
}
|
||||
|
||||
// delete the edges at the index in indexToDelete
|
||||
for (let i = indexToDelete.length - 1; i >= 0; i -= 1) {
|
||||
edges.splice(indexToDelete[i], 1)
|
||||
}
|
||||
|
||||
const updatedEdges = edges.map((edge) => {
|
||||
return {
|
||||
...edge,
|
||||
data: {
|
||||
...edge.data,
|
||||
sourceColor: findNodeColor(edge.source),
|
||||
targetColor: findNodeColor(edge.target),
|
||||
edgeLabel: isMultiOutput(edge.source) && edge.label && edge.label.trim() !== '' ? edge.label.trim() : undefined,
|
||||
isHumanInput: edge.source.includes('humanInputAgentflow') ? true : false
|
||||
},
|
||||
type: 'agentFlow',
|
||||
id: `${edge.source}-${edge.sourceHandle}-${edge.target}-${edge.targetHandle}`
|
||||
}
|
||||
}) as Edge[]
|
||||
|
||||
if (updatedEdges.length > 0) {
|
||||
updatedEdges.forEach((edge) => {
|
||||
if (isMultiOutput(edge.source)) {
|
||||
if (edge.sourceHandle.includes('true')) {
|
||||
edge.sourceHandle = edge.sourceHandle.replace('true', '0')
|
||||
} else if (edge.sourceHandle.includes('false')) {
|
||||
edge.sourceHandle = edge.sourceHandle.replace('false', '1')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return updatedEdges
|
||||
}
|
||||
|
||||
const generateSelectedTools = async (nodes: Node[], config: Record<string, any>, question: string, options: ICommonObject) => {
|
||||
const selectedTools: string[] = []
|
||||
|
||||
for (let i = 0; i < nodes.length; i += 1) {
|
||||
const node = nodes[i]
|
||||
if (!node.data.inputs) {
|
||||
node.data.inputs = {}
|
||||
}
|
||||
|
||||
if (node.data.name === 'agentAgentflow') {
|
||||
const sysPrompt = `You are a workflow orchestrator that is designed to make agent coordination and execution easy. Your goal is to select the tools that are needed to achieve the given task.
|
||||
|
||||
Here are the tools to choose from:
|
||||
${config.toolNodes}
|
||||
|
||||
Here's the selected tools:
|
||||
${JSON.stringify(selectedTools, null, 2)}
|
||||
|
||||
Output Format should be a list of tool names:
|
||||
For example:["googleCustomSearch", "slackMCP"]
|
||||
|
||||
Now, select the tools that are needed to achieve the given task. You must only select tools that are in the list of tools above. You must NOT select the tools that are already in the list of selected tools.
|
||||
`
|
||||
const tools = await _generateSelectedTools({ ...config, prompt: sysPrompt }, question, options)
|
||||
if (Array.isArray(tools) && tools.length > 0) {
|
||||
selectedTools.push(...tools)
|
||||
|
||||
const existingTools = node.data.inputs.agentTools || []
|
||||
node.data.inputs.agentTools = [
|
||||
...existingTools,
|
||||
...tools.map((tool) => ({
|
||||
agentSelectedTool: tool,
|
||||
agentSelectedToolConfig: {
|
||||
agentSelectedTool: tool
|
||||
}
|
||||
}))
|
||||
]
|
||||
}
|
||||
} else if (node.data.name === 'toolAgentflow') {
|
||||
const sysPrompt = `You are a workflow orchestrator that is designed to make agent coordination and execution easy. Your goal is to select ONE tool that is needed to achieve the given task.
|
||||
|
||||
Here are the tools to choose from:
|
||||
${config.toolNodes}
|
||||
|
||||
Here's the selected tools:
|
||||
${JSON.stringify(selectedTools, null, 2)}
|
||||
|
||||
Output Format should ONLY one tool name inside of a list:
|
||||
For example:["googleCustomSearch"]
|
||||
|
||||
Now, select the ONLY tool that is needed to achieve the given task. You must only select tool that is in the list of tools above. You must NOT select the tool that is already in the list of selected tools.
|
||||
`
|
||||
const tools = await _generateSelectedTools({ ...config, prompt: sysPrompt }, question, options)
|
||||
if (Array.isArray(tools) && tools.length > 0) {
|
||||
selectedTools.push(...tools)
|
||||
|
||||
node.data.inputs.selectedTool = tools[0]
|
||||
node.data.inputs.toolInputArgs = []
|
||||
node.data.inputs.selectedToolConfig = {
|
||||
selectedTool: tools[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
const _generateSelectedTools = async (config: Record<string, any>, question: string, options: ICommonObject) => {
|
||||
try {
|
||||
const chatModelComponent = config.componentNodes[config.selectedChatModel?.name]
|
||||
if (!chatModelComponent) {
|
||||
throw new Error('Chat model component not found')
|
||||
}
|
||||
const nodeInstanceFilePath = chatModelComponent.filePath as string
|
||||
const nodeModule = await import(nodeInstanceFilePath)
|
||||
const newToolNodeInstance = new nodeModule.nodeClass()
|
||||
const model = (await newToolNodeInstance.init(config.selectedChatModel, '', options)) as BaseChatModel
|
||||
|
||||
// Create a parser to validate the output
|
||||
const parser = StructuredOutputParser.fromZodSchema(ToolType)
|
||||
|
||||
// Generate JSON schema from our Zod schema
|
||||
const formatInstructions = parser.getFormatInstructions()
|
||||
|
||||
// Full conversation with system prompt and instructions
|
||||
const messages = [
|
||||
{
|
||||
role: 'system',
|
||||
content: `${config.prompt}\n\n${formatInstructions}\n\nMake sure to follow the exact JSON schema structure.`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: question
|
||||
}
|
||||
]
|
||||
|
||||
// Standard completion without structured output
|
||||
const response = await model.invoke(messages)
|
||||
|
||||
// Try to extract JSON from the response
|
||||
const responseContent = response.content.toString()
|
||||
const jsonMatch = responseContent.match(/```json\n([\s\S]*?)\n```/) || responseContent.match(/{[\s\S]*?}/)
|
||||
|
||||
if (jsonMatch) {
|
||||
const jsonStr = jsonMatch[1] || jsonMatch[0]
|
||||
try {
|
||||
const parsedJSON = JSON.parse(jsonStr)
|
||||
// Validate with our schema
|
||||
return ToolType.parse(parsedJSON)
|
||||
} catch (parseError) {
|
||||
console.error('Error parsing JSON from response:', parseError)
|
||||
return { error: 'Failed to parse JSON from response', content: responseContent }
|
||||
}
|
||||
} else {
|
||||
console.error('No JSON found in response:', responseContent)
|
||||
return { error: 'No JSON found in response', content: responseContent }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating AgentflowV2:', error)
|
||||
return { error: error.message || 'Unknown error occurred' }
|
||||
}
|
||||
}
|
||||
|
||||
const generateNodesEdges = async (config: Record<string, any>, question: string, options?: ICommonObject) => {
|
||||
try {
|
||||
const chatModelComponent = config.componentNodes[config.selectedChatModel?.name]
|
||||
if (!chatModelComponent) {
|
||||
throw new Error('Chat model component not found')
|
||||
}
|
||||
const nodeInstanceFilePath = chatModelComponent.filePath as string
|
||||
const nodeModule = await import(nodeInstanceFilePath)
|
||||
const newToolNodeInstance = new nodeModule.nodeClass()
|
||||
const model = (await newToolNodeInstance.init(config.selectedChatModel, '', options)) as BaseChatModel
|
||||
|
||||
// Create a parser to validate the output
|
||||
const parser = StructuredOutputParser.fromZodSchema(NodesEdgesType)
|
||||
|
||||
// Generate JSON schema from our Zod schema
|
||||
const formatInstructions = parser.getFormatInstructions()
|
||||
|
||||
// Full conversation with system prompt and instructions
|
||||
const messages = [
|
||||
{
|
||||
role: 'system',
|
||||
content: `${config.prompt}\n\n${formatInstructions}\n\nMake sure to follow the exact JSON schema structure.`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: question
|
||||
}
|
||||
]
|
||||
|
||||
// Standard completion without structured output
|
||||
const response = await model.invoke(messages)
|
||||
|
||||
// Try to extract JSON from the response
|
||||
const responseContent = response.content.toString()
|
||||
const jsonMatch = responseContent.match(/```json\n([\s\S]*?)\n```/) || responseContent.match(/{[\s\S]*?}/)
|
||||
|
||||
if (jsonMatch) {
|
||||
const jsonStr = jsonMatch[1] || jsonMatch[0]
|
||||
try {
|
||||
const parsedJSON = JSON.parse(jsonStr)
|
||||
// Validate with our schema
|
||||
return NodesEdgesType.parse(parsedJSON)
|
||||
} catch (parseError) {
|
||||
console.error('Error parsing JSON from response:', parseError)
|
||||
return { error: 'Failed to parse JSON from response', content: responseContent }
|
||||
}
|
||||
} else {
|
||||
console.error('No JSON found in response:', responseContent)
|
||||
return { error: 'No JSON found in response', content: responseContent }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating AgentflowV2:', error)
|
||||
return { error: error.message || 'Unknown error occurred' }
|
||||
}
|
||||
}
|
||||
|
||||
const generateNodesData = (result: Record<string, any>, config: Record<string, any>) => {
|
||||
try {
|
||||
if (result.error) {
|
||||
return result
|
||||
}
|
||||
|
||||
let nodes = result.nodes
|
||||
|
||||
for (let i = 0; i < nodes.length; i += 1) {
|
||||
const node = nodes[i]
|
||||
let nodeName = node.data.name
|
||||
|
||||
// If nodeName is not found in data.name, try extracting from node.id
|
||||
if (!nodeName || !config.componentNodes[nodeName]) {
|
||||
nodeName = node.id.split('_')[0]
|
||||
}
|
||||
|
||||
const componentNode = config.componentNodes[nodeName]
|
||||
if (!componentNode) {
|
||||
continue
|
||||
}
|
||||
|
||||
const initializedNodeData = initNode(cloneDeep(componentNode), node.id)
|
||||
nodes[i].data = {
|
||||
...initializedNodeData,
|
||||
label: node.data?.label
|
||||
}
|
||||
|
||||
if (nodes[i].data.name === 'iterationAgentflow') {
|
||||
nodes[i].type = 'iteration'
|
||||
}
|
||||
|
||||
if (nodes[i].parentNode) {
|
||||
nodes[i].extent = 'parent'
|
||||
}
|
||||
}
|
||||
|
||||
return { nodes, edges: result.edges }
|
||||
} catch (error) {
|
||||
console.error('Error generating AgentflowV2:', error)
|
||||
return { error: error.message || 'Unknown error occurred' }
|
||||
}
|
||||
}
|
||||
|
||||
const initNode = (nodeData: Record<string, any>, newNodeId: string): NodeData => {
|
||||
const inputParams = []
|
||||
const incoming = nodeData.inputs ? nodeData.inputs.length : 0
|
||||
|
||||
// Inputs
|
||||
for (let i = 0; i < incoming; i += 1) {
|
||||
const newInput = {
|
||||
...nodeData.inputs[i],
|
||||
id: `${newNodeId}-input-${nodeData.inputs[i].name}-${nodeData.inputs[i].type}`
|
||||
}
|
||||
inputParams.push(newInput)
|
||||
}
|
||||
|
||||
// Credential
|
||||
if (nodeData.credential) {
|
||||
const newInput = {
|
||||
...nodeData.credential,
|
||||
id: `${newNodeId}-input-${nodeData.credential.name}-${nodeData.credential.type}`
|
||||
}
|
||||
inputParams.unshift(newInput)
|
||||
}
|
||||
|
||||
// Outputs
|
||||
let outputAnchors = initializeOutputAnchors(nodeData, newNodeId)
|
||||
|
||||
/* Initial
|
||||
inputs = [
|
||||
{
|
||||
label: 'field_label_1',
|
||||
name: 'string'
|
||||
},
|
||||
{
|
||||
label: 'field_label_2',
|
||||
name: 'CustomType'
|
||||
}
|
||||
]
|
||||
|
||||
=> Convert to inputs, inputParams, inputAnchors
|
||||
|
||||
=> inputs = { 'field': 'defaultvalue' } // Turn into inputs object with default values
|
||||
|
||||
=> // For inputs that are part of whitelistTypes
|
||||
inputParams = [
|
||||
{
|
||||
label: 'field_label_1',
|
||||
name: 'string'
|
||||
}
|
||||
]
|
||||
|
||||
=> // For inputs that are not part of whitelistTypes
|
||||
inputAnchors = [
|
||||
{
|
||||
label: 'field_label_2',
|
||||
name: 'CustomType'
|
||||
}
|
||||
]
|
||||
*/
|
||||
|
||||
// Inputs
|
||||
if (nodeData.inputs) {
|
||||
const defaultInputs = initializeDefaultNodeData(nodeData.inputs)
|
||||
nodeData.inputAnchors = showHideInputAnchors({ ...nodeData, inputAnchors: [], inputs: defaultInputs })
|
||||
nodeData.inputParams = showHideInputParams({ ...nodeData, inputParams, inputs: defaultInputs })
|
||||
nodeData.inputs = defaultInputs
|
||||
} else {
|
||||
nodeData.inputAnchors = []
|
||||
nodeData.inputParams = []
|
||||
nodeData.inputs = {}
|
||||
}
|
||||
|
||||
// Outputs
|
||||
if (nodeData.outputs) {
|
||||
nodeData.outputs = initializeDefaultNodeData(outputAnchors)
|
||||
} else {
|
||||
nodeData.outputs = {}
|
||||
}
|
||||
nodeData.outputAnchors = outputAnchors
|
||||
|
||||
// Credential
|
||||
if (nodeData.credential) nodeData.credential = ''
|
||||
|
||||
nodeData.id = newNodeId
|
||||
|
||||
return nodeData
|
||||
}
|
||||
|
||||
const initializeDefaultNodeData = (nodeParams: Record<string, any>[]) => {
|
||||
const initialValues: Record<string, any> = {}
|
||||
|
||||
for (let i = 0; i < nodeParams.length; i += 1) {
|
||||
const input = nodeParams[i]
|
||||
initialValues[input.name] = input.default || ''
|
||||
}
|
||||
|
||||
return initialValues
|
||||
}
|
||||
|
||||
const createAgentFlowOutputs = (nodeData: Record<string, any>, newNodeId: string) => {
|
||||
if (nodeData.hideOutput) return []
|
||||
|
||||
if (nodeData.outputs?.length) {
|
||||
return nodeData.outputs.map((_: any, index: number) => ({
|
||||
id: `${newNodeId}-output-${index}`,
|
||||
label: nodeData.label,
|
||||
name: nodeData.name
|
||||
}))
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: `${newNodeId}-output-${nodeData.name}`,
|
||||
label: nodeData.label,
|
||||
name: nodeData.name
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const initializeOutputAnchors = (nodeData: Record<string, any>, newNodeId: string): OutputAnchor[] => {
|
||||
return createAgentFlowOutputs(nodeData, newNodeId)
|
||||
}
|
||||
|
||||
const _showHideOperation = (nodeData: Record<string, any>, inputParam: Record<string, any>, displayType: string, index?: number) => {
|
||||
const displayOptions = inputParam[displayType]
|
||||
/* For example:
|
||||
show: {
|
||||
enableMemory: true
|
||||
}
|
||||
*/
|
||||
Object.keys(displayOptions).forEach((path) => {
|
||||
const comparisonValue = displayOptions[path]
|
||||
if (path.includes('$index') && index) {
|
||||
path = path.replace('$index', index.toString())
|
||||
}
|
||||
const groundValue = get(nodeData.inputs, path, '')
|
||||
|
||||
if (Array.isArray(comparisonValue)) {
|
||||
if (displayType === 'show' && !comparisonValue.includes(groundValue)) {
|
||||
inputParam.display = false
|
||||
}
|
||||
if (displayType === 'hide' && comparisonValue.includes(groundValue)) {
|
||||
inputParam.display = false
|
||||
}
|
||||
} else if (typeof comparisonValue === 'string') {
|
||||
if (displayType === 'show' && !(comparisonValue === groundValue || new RegExp(comparisonValue).test(groundValue))) {
|
||||
inputParam.display = false
|
||||
}
|
||||
if (displayType === 'hide' && (comparisonValue === groundValue || new RegExp(comparisonValue).test(groundValue))) {
|
||||
inputParam.display = false
|
||||
}
|
||||
} else if (typeof comparisonValue === 'boolean') {
|
||||
if (displayType === 'show' && comparisonValue !== groundValue) {
|
||||
inputParam.display = false
|
||||
}
|
||||
if (displayType === 'hide' && comparisonValue === groundValue) {
|
||||
inputParam.display = false
|
||||
}
|
||||
} else if (typeof comparisonValue === 'object') {
|
||||
if (displayType === 'show' && !isEqual(comparisonValue, groundValue)) {
|
||||
inputParam.display = false
|
||||
}
|
||||
if (displayType === 'hide' && isEqual(comparisonValue, groundValue)) {
|
||||
inputParam.display = false
|
||||
}
|
||||
} else if (typeof comparisonValue === 'number') {
|
||||
if (displayType === 'show' && comparisonValue !== groundValue) {
|
||||
inputParam.display = false
|
||||
}
|
||||
if (displayType === 'hide' && comparisonValue === groundValue) {
|
||||
inputParam.display = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const showHideInputs = (nodeData: Record<string, any>, inputType: string, overrideParams?: Record<string, any>, arrayIndex?: number) => {
|
||||
const params = overrideParams ?? nodeData[inputType] ?? []
|
||||
|
||||
for (let i = 0; i < params.length; i += 1) {
|
||||
const inputParam = params[i]
|
||||
|
||||
// Reset display flag to false for each inputParam
|
||||
inputParam.display = true
|
||||
|
||||
if (inputParam.show) {
|
||||
_showHideOperation(nodeData, inputParam, 'show', arrayIndex)
|
||||
}
|
||||
if (inputParam.hide) {
|
||||
_showHideOperation(nodeData, inputParam, 'hide', arrayIndex)
|
||||
}
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
const showHideInputParams = (nodeData: Record<string, any>): InputParam[] => {
|
||||
return showHideInputs(nodeData, 'inputParams')
|
||||
}
|
||||
|
||||
const showHideInputAnchors = (nodeData: Record<string, any>): InputAnchor[] => {
|
||||
return showHideInputs(nodeData, 'inputAnchors')
|
||||
}
|
||||
+177
-115
@@ -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 }
|
||||
|
||||
@@ -11,3 +11,4 @@ export * from './storageUtils'
|
||||
export * from './handler'
|
||||
export * from './followUpPrompts'
|
||||
export * from './validator'
|
||||
export * from './agentflowv2Generator'
|
||||
|
||||
@@ -712,7 +712,7 @@ export const mapChatMessageToBaseMessage = async (chatmessages: any[] = []): Pro
|
||||
for (const message of chatmessages) {
|
||||
if (message.role === 'apiMessage' || message.type === 'apiMessage') {
|
||||
chatHistory.push(new AIMessage(message.content || ''))
|
||||
} else if (message.role === 'userMessage' || message.role === 'userMessage') {
|
||||
} else if (message.role === 'userMessage' || message.type === 'userMessage') {
|
||||
// check for image/files uploads
|
||||
if (message.fileUploads) {
|
||||
// example: [{"type":"stored-file","name":"0_DiXc4ZklSTo3M8J4.jpg","mime":"image/jpeg"}]
|
||||
@@ -788,17 +788,23 @@ export const mapChatMessageToBaseMessage = async (chatmessages: any[] = []): Pro
|
||||
* @param {IMessage[]} chatHistory
|
||||
* @returns {string}
|
||||
*/
|
||||
export const convertChatHistoryToText = (chatHistory: IMessage[] = []): string => {
|
||||
export const convertChatHistoryToText = (chatHistory: IMessage[] | { content: string; role: string }[] = []): string => {
|
||||
return chatHistory
|
||||
.map((chatMessage) => {
|
||||
if (chatMessage.type === 'apiMessage') {
|
||||
return `Assistant: ${chatMessage.message}`
|
||||
} else if (chatMessage.type === 'userMessage') {
|
||||
return `Human: ${chatMessage.message}`
|
||||
if (!chatMessage) return ''
|
||||
const messageContent = 'message' in chatMessage ? chatMessage.message : chatMessage.content
|
||||
if (!messageContent || messageContent.trim() === '') return ''
|
||||
|
||||
const messageType = 'type' in chatMessage ? chatMessage.type : chatMessage.role
|
||||
if (messageType === 'apiMessage' || messageType === 'assistant') {
|
||||
return `Assistant: ${messageContent}`
|
||||
} else if (messageType === 'userMessage' || messageType === 'user') {
|
||||
return `Human: ${messageContent}`
|
||||
} else {
|
||||
return `${chatMessage.message}`
|
||||
return `${messageContent}`
|
||||
}
|
||||
})
|
||||
.filter((message) => message !== '') // Remove empty messages
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user