mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 13:00:56 +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:
@@ -2,8 +2,10 @@ import {
|
||||
IAction,
|
||||
ICommonObject,
|
||||
IFileUpload,
|
||||
IHumanInput,
|
||||
INode,
|
||||
INodeData as INodeDataFromComponent,
|
||||
INodeExecutionData,
|
||||
INodeParams,
|
||||
IServerSideEventStreamer
|
||||
} from 'flowise-components'
|
||||
@@ -13,10 +15,12 @@ import { Telemetry } from './utils/telemetry'
|
||||
|
||||
export type MessageType = 'apiMessage' | 'userMessage'
|
||||
|
||||
export type ChatflowType = 'CHATFLOW' | 'MULTIAGENT' | 'ASSISTANT'
|
||||
export type ChatflowType = 'CHATFLOW' | 'MULTIAGENT' | 'ASSISTANT' | 'AGENTFLOW'
|
||||
|
||||
export type AssistantType = 'CUSTOM' | 'OPENAI' | 'AZURE'
|
||||
|
||||
export type ExecutionState = 'INPROGRESS' | 'FINISHED' | 'ERROR' | 'TERMINATED' | 'TIMEOUT' | 'STOPPED'
|
||||
|
||||
export enum MODE {
|
||||
QUEUE = 'queue',
|
||||
MAIN = 'main'
|
||||
@@ -57,6 +61,7 @@ export interface IChatMessage {
|
||||
role: MessageType
|
||||
content: string
|
||||
chatflowid: string
|
||||
executionId?: string
|
||||
sourceDocuments?: string
|
||||
usedTools?: string
|
||||
fileAnnotations?: string
|
||||
@@ -140,6 +145,19 @@ export interface IUpsertHistory {
|
||||
date: Date
|
||||
}
|
||||
|
||||
export interface IExecution {
|
||||
id: string
|
||||
executionData: string
|
||||
state: ExecutionState
|
||||
agentflowId: string
|
||||
sessionId: string
|
||||
isPublic?: boolean
|
||||
action?: string
|
||||
createdDate: Date
|
||||
updatedDate: Date
|
||||
stoppedDate: Date
|
||||
}
|
||||
|
||||
export interface IComponentNodes {
|
||||
[key: string]: INode
|
||||
}
|
||||
@@ -187,6 +205,8 @@ export interface IReactFlowNode {
|
||||
height: number
|
||||
selected: boolean
|
||||
dragging: boolean
|
||||
parentNode?: string
|
||||
extent?: string
|
||||
}
|
||||
|
||||
export interface IReactFlowEdge {
|
||||
@@ -227,6 +247,14 @@ export interface IDepthQueue {
|
||||
[key: string]: number
|
||||
}
|
||||
|
||||
export interface IAgentflowExecutedData {
|
||||
nodeLabel: string
|
||||
nodeId: string
|
||||
data: INodeExecutionData
|
||||
previousNodeIds: string[]
|
||||
status?: ExecutionState
|
||||
}
|
||||
|
||||
export interface IMessage {
|
||||
message: string
|
||||
type: MessageType
|
||||
@@ -238,6 +266,7 @@ export interface IncomingInput {
|
||||
question: string
|
||||
overrideConfig?: ICommonObject
|
||||
chatId?: string
|
||||
sessionId?: string
|
||||
stopNodeId?: string
|
||||
uploads?: IFileUpload[]
|
||||
leadEmail?: string
|
||||
@@ -246,6 +275,12 @@ export interface IncomingInput {
|
||||
streaming?: boolean
|
||||
}
|
||||
|
||||
export interface IncomingAgentflowInput extends Omit<IncomingInput, 'question'> {
|
||||
question?: string
|
||||
form?: Record<string, any>
|
||||
humanInput?: IHumanInput
|
||||
}
|
||||
|
||||
export interface IActiveChatflows {
|
||||
[key: string]: {
|
||||
startingNodes: IReactFlowNode[]
|
||||
@@ -266,6 +301,7 @@ export interface IOverrideConfig {
|
||||
label: string
|
||||
name: string
|
||||
type: string
|
||||
schema?: ICommonObject[]
|
||||
}
|
||||
|
||||
export type ICredentialDataDecrypted = ICommonObject
|
||||
@@ -315,6 +351,8 @@ export interface IFlowConfig {
|
||||
chatHistory: IMessage[]
|
||||
apiMessageId: string
|
||||
overrideConfig?: ICommonObject
|
||||
state?: ICommonObject
|
||||
runtimeChatHistoryLength?: number
|
||||
}
|
||||
|
||||
export interface IPredictionQueueAppServer {
|
||||
@@ -333,7 +371,13 @@ export interface IExecuteFlowParams extends IPredictionQueueAppServer {
|
||||
isInternal: boolean
|
||||
signal?: AbortController
|
||||
files?: Express.Multer.File[]
|
||||
fileUploads?: IFileUpload[]
|
||||
uploadedFilesContent?: string
|
||||
isUpsert?: boolean
|
||||
isRecursive?: boolean
|
||||
parentExecutionId?: string
|
||||
iterationContext?: ICommonObject
|
||||
isTool?: boolean
|
||||
}
|
||||
|
||||
export interface INodeOverrides {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Request, Response, NextFunction } from 'express'
|
||||
import agentflowv2Service from '../../services/agentflowv2-generator'
|
||||
|
||||
const generateAgentflowv2 = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
if (!req.body.question || !req.body.selectedChatModel) {
|
||||
throw new Error('Question and selectedChatModel are required')
|
||||
}
|
||||
const apiResponse = await agentflowv2Service.generateAgentflowv2(req.body.question, req.body.selectedChatModel)
|
||||
return res.json(apiResponse)
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
generateAgentflowv2
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { Request, Response, NextFunction } from 'express'
|
||||
import executionsService from '../../services/executions'
|
||||
import { ExecutionState } from '../../Interface'
|
||||
|
||||
const getExecutionById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const executionId = req.params.id
|
||||
const execution = await executionsService.getExecutionById(executionId)
|
||||
return res.json(execution)
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
const getPublicExecutionById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const executionId = req.params.id
|
||||
const execution = await executionsService.getPublicExecutionById(executionId)
|
||||
return res.json(execution)
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
const updateExecution = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const executionId = req.params.id
|
||||
const execution = await executionsService.updateExecution(executionId, req.body)
|
||||
return res.json(execution)
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
const getAllExecutions = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
// Extract all possible filters from query params
|
||||
const filters: any = {}
|
||||
|
||||
// ID filter
|
||||
if (req.query.id) filters.id = req.query.id as string
|
||||
|
||||
// Flow and session filters
|
||||
if (req.query.agentflowId) filters.agentflowId = req.query.agentflowId as string
|
||||
if (req.query.sessionId) filters.sessionId = req.query.sessionId as string
|
||||
|
||||
// State filter
|
||||
if (req.query.state) {
|
||||
const stateValue = req.query.state as string
|
||||
if (['INPROGRESS', 'FINISHED', 'ERROR', 'TERMINATED', 'TIMEOUT', 'STOPPED'].includes(stateValue)) {
|
||||
filters.state = stateValue as ExecutionState
|
||||
}
|
||||
}
|
||||
|
||||
// Date filters
|
||||
if (req.query.startDate) {
|
||||
filters.startDate = new Date(req.query.startDate as string)
|
||||
}
|
||||
|
||||
if (req.query.endDate) {
|
||||
filters.endDate = new Date(req.query.endDate as string)
|
||||
}
|
||||
|
||||
// Pagination
|
||||
if (req.query.page) {
|
||||
filters.page = parseInt(req.query.page as string, 10)
|
||||
}
|
||||
|
||||
if (req.query.limit) {
|
||||
filters.limit = parseInt(req.query.limit as string, 10)
|
||||
}
|
||||
|
||||
const apiResponse = await executionsService.getAllExecutions(filters)
|
||||
|
||||
return res.json(apiResponse)
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple executions by their IDs
|
||||
* If a single ID is provided in the URL params, it will delete that execution
|
||||
* If an array of IDs is provided in the request body, it will delete all those executions
|
||||
*/
|
||||
const deleteExecutions = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
let executionIds: string[] = []
|
||||
|
||||
// Check if we're deleting a single execution from URL param
|
||||
if (req.params.id) {
|
||||
executionIds = [req.params.id]
|
||||
}
|
||||
// Check if we're deleting multiple executions from request body
|
||||
else if (req.body.executionIds && Array.isArray(req.body.executionIds)) {
|
||||
executionIds = req.body.executionIds
|
||||
} else {
|
||||
return res.status(400).json({ success: false, message: 'No execution IDs provided' })
|
||||
}
|
||||
|
||||
const result = await executionsService.deleteExecutions(executionIds)
|
||||
return res.json(result)
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getAllExecutions,
|
||||
deleteExecutions,
|
||||
getExecutionById,
|
||||
getPublicExecutionById,
|
||||
updateExecution
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Request, Response, NextFunction } from 'express'
|
||||
import validationService from '../../services/validation'
|
||||
import { InternalFlowiseError } from '../../errors/internalFlowiseError'
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
|
||||
const checkFlowValidation = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const flowId = req.params?.id as string | undefined
|
||||
if (!flowId) {
|
||||
throw new InternalFlowiseError(
|
||||
StatusCodes.PRECONDITION_FAILED,
|
||||
`Error: validationController.checkFlowValidation - id not provided!`
|
||||
)
|
||||
}
|
||||
const apiResponse = await validationService.checkFlowValidation(flowId)
|
||||
return res.json(apiResponse)
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
checkFlowValidation
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable */
|
||||
import { Entity, Column, CreateDateColumn, PrimaryGeneratedColumn, Index } from 'typeorm'
|
||||
import { Entity, Column, CreateDateColumn, PrimaryGeneratedColumn, Index, JoinColumn, OneToOne } from 'typeorm'
|
||||
import { IChatMessage, MessageType } from '../../Interface'
|
||||
import { Execution } from './Execution'
|
||||
|
||||
@Entity()
|
||||
export class ChatMessage implements IChatMessage {
|
||||
@@ -14,6 +15,13 @@ export class ChatMessage implements IChatMessage {
|
||||
@Column({ type: 'uuid' })
|
||||
chatflowid: string
|
||||
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
executionId?: string
|
||||
|
||||
@OneToOne(() => Execution)
|
||||
@JoinColumn({ name: 'executionId' })
|
||||
execution: Execution
|
||||
|
||||
@Column({ type: 'text' })
|
||||
content: string
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Entity, Column, Index, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm'
|
||||
import { IExecution, ExecutionState } from '../../Interface'
|
||||
import { ChatFlow } from './ChatFlow'
|
||||
|
||||
@Entity()
|
||||
export class Execution implements IExecution {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string
|
||||
|
||||
@Column({ type: 'text' })
|
||||
executionData: string
|
||||
|
||||
@Column()
|
||||
state: ExecutionState
|
||||
|
||||
@Index()
|
||||
@Column({ type: 'uuid' })
|
||||
agentflowId: string
|
||||
|
||||
@Index()
|
||||
@Column({ type: 'uuid' })
|
||||
sessionId: string
|
||||
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
action?: string
|
||||
|
||||
@Column({ nullable: true })
|
||||
isPublic?: boolean
|
||||
|
||||
@Column({ type: 'timestamp' })
|
||||
@CreateDateColumn()
|
||||
createdDate: Date
|
||||
|
||||
@Column({ type: 'timestamp' })
|
||||
@UpdateDateColumn()
|
||||
updatedDate: Date
|
||||
|
||||
@Column()
|
||||
stoppedDate: Date
|
||||
|
||||
@ManyToOne(() => ChatFlow)
|
||||
@JoinColumn({ name: 'agentflowId' })
|
||||
agentflow: ChatFlow
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { Lead } from './Lead'
|
||||
import { UpsertHistory } from './UpsertHistory'
|
||||
import { ApiKey } from './ApiKey'
|
||||
import { CustomTemplate } from './CustomTemplate'
|
||||
import { Execution } from './Execution'
|
||||
|
||||
export const entities = {
|
||||
ChatFlow,
|
||||
@@ -25,5 +26,6 @@ export const entities = {
|
||||
Lead,
|
||||
UpsertHistory,
|
||||
ApiKey,
|
||||
CustomTemplate
|
||||
CustomTemplate,
|
||||
Execution
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class AddExecutionEntity1738090872625 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE IF NOT EXISTS \`execution\` (
|
||||
\`id\` varchar(36) NOT NULL,
|
||||
\`executionData\` text NOT NULL,
|
||||
\`action\` text,
|
||||
\`state\` varchar(255) NOT NULL,
|
||||
\`agentflowId\` varchar(255) NOT NULL,
|
||||
\`sessionId\` varchar(255) NOT NULL,
|
||||
\`isPublic\` boolean,
|
||||
\`createdDate\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
\`updatedDate\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||
\`stoppedDate\` datetime(6),
|
||||
PRIMARY KEY (\`id\`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;`
|
||||
)
|
||||
|
||||
const columnExists = await queryRunner.hasColumn('chat_message', 'executionId')
|
||||
if (!columnExists) {
|
||||
await queryRunner.query(`ALTER TABLE \`chat_message\` ADD COLUMN \`executionId\` TEXT;`)
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS \`execution\``)
|
||||
await queryRunner.query(`ALTER TABLE \`chat_message\` DROP COLUMN \`executionId\`;`)
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import { AddCustomTemplate1725629836652 } from './1725629836652-AddCustomTemplat
|
||||
import { AddArtifactsToChatMessage1726156258465 } from './1726156258465-AddArtifactsToChatMessage'
|
||||
import { AddFollowUpPrompts1726666318346 } from './1726666318346-AddFollowUpPrompts'
|
||||
import { AddTypeToAssistant1733011290987 } from './1733011290987-AddTypeToAssistant'
|
||||
import { AddExecutionEntity1738090872625 } from './1738090872625-AddExecutionEntity'
|
||||
|
||||
export const mariadbMigrations = [
|
||||
Init1693840429259,
|
||||
@@ -59,5 +60,6 @@ export const mariadbMigrations = [
|
||||
AddCustomTemplate1725629836652,
|
||||
AddArtifactsToChatMessage1726156258465,
|
||||
AddFollowUpPrompts1726666318346,
|
||||
AddTypeToAssistant1733011290987
|
||||
AddTypeToAssistant1733011290987,
|
||||
AddExecutionEntity1738090872625
|
||||
]
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class AddExecutionEntity1738090872625 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE IF NOT EXISTS \`execution\` (
|
||||
\`id\` varchar(36) NOT NULL,
|
||||
\`executionData\` text NOT NULL,
|
||||
\`action\` text,
|
||||
\`state\` varchar(255) NOT NULL,
|
||||
\`agentflowId\` varchar(255) NOT NULL,
|
||||
\`sessionId\` varchar(255) NOT NULL,
|
||||
\`isPublic\` boolean,
|
||||
\`createdDate\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
\`updatedDate\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||
\`stoppedDate\` datetime(6),
|
||||
PRIMARY KEY (\`id\`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;`
|
||||
)
|
||||
|
||||
const columnExists = await queryRunner.hasColumn('chat_message', 'executionId')
|
||||
if (!columnExists) {
|
||||
await queryRunner.query(`ALTER TABLE \`chat_message\` ADD COLUMN \`executionId\` TEXT;`)
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS \`execution\``)
|
||||
await queryRunner.query(`ALTER TABLE \`chat_message\` DROP COLUMN \`executionId\`;`)
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import { AddCustomTemplate1725629836652 } from './1725629836652-AddCustomTemplat
|
||||
import { AddArtifactsToChatMessage1726156258465 } from './1726156258465-AddArtifactsToChatMessage'
|
||||
import { AddFollowUpPrompts1726666302024 } from './1726666302024-AddFollowUpPrompts'
|
||||
import { AddTypeToAssistant1733011290987 } from './1733011290987-AddTypeToAssistant'
|
||||
import { AddExecutionEntity1738090872625 } from './1738090872625-AddExecutionEntity'
|
||||
|
||||
export const mysqlMigrations = [
|
||||
Init1693840429259,
|
||||
@@ -59,5 +60,6 @@ export const mysqlMigrations = [
|
||||
AddCustomTemplate1725629836652,
|
||||
AddArtifactsToChatMessage1726156258465,
|
||||
AddFollowUpPrompts1726666302024,
|
||||
AddTypeToAssistant1733011290987
|
||||
AddTypeToAssistant1733011290987,
|
||||
AddExecutionEntity1738090872625
|
||||
]
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class AddExecutionEntity1738090872625 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE IF NOT EXISTS execution (
|
||||
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"executionData" text NOT NULL,
|
||||
"action" text,
|
||||
"state" varchar NOT NULL,
|
||||
"agentflowId" uuid NOT NULL,
|
||||
"sessionId" uuid NOT NULL,
|
||||
"isPublic" boolean,
|
||||
"createdDate" timestamp NOT NULL DEFAULT now(),
|
||||
"updatedDate" timestamp NOT NULL DEFAULT now(),
|
||||
"stoppedDate" timestamp,
|
||||
CONSTRAINT "PK_936a419c3b8044598d72d95da61" PRIMARY KEY (id)
|
||||
);`
|
||||
)
|
||||
|
||||
const columnExists = await queryRunner.hasColumn('chat_message', 'executionId')
|
||||
if (!columnExists) {
|
||||
await queryRunner.query(`ALTER TABLE "chat_message" ADD COLUMN "executionId" uuid;`)
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP TABLE execution`)
|
||||
await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "executionId";`)
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import { AddCustomTemplate1725629836652 } from './1725629836652-AddCustomTemplat
|
||||
import { AddArtifactsToChatMessage1726156258465 } from './1726156258465-AddArtifactsToChatMessage'
|
||||
import { AddFollowUpPrompts1726666309552 } from './1726666309552-AddFollowUpPrompts'
|
||||
import { AddTypeToAssistant1733011290987 } from './1733011290987-AddTypeToAssistant'
|
||||
import { AddExecutionEntity1738090872625 } from './1738090872625-AddExecutionEntity'
|
||||
|
||||
export const postgresMigrations = [
|
||||
Init1693891895163,
|
||||
@@ -59,5 +60,6 @@ export const postgresMigrations = [
|
||||
AddCustomTemplate1725629836652,
|
||||
AddArtifactsToChatMessage1726156258465,
|
||||
AddFollowUpPrompts1726666309552,
|
||||
AddTypeToAssistant1733011290987
|
||||
AddTypeToAssistant1733011290987,
|
||||
AddExecutionEntity1738090872625
|
||||
]
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class AddExecutionEntity1738090872625 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE IF NOT EXISTS "execution" ("id" varchar PRIMARY KEY NOT NULL, "executionData" text NOT NULL, "action" text, "state" varchar NOT NULL, "agentflowId" varchar NOT NULL, "sessionId" varchar NOT NULL, "isPublic" boolean, "createdDate" datetime NOT NULL DEFAULT (datetime('now')), "updatedDate" datetime NOT NULL DEFAULT (datetime('now')), "stoppedDate" datetime);`
|
||||
)
|
||||
await queryRunner.query(`ALTER TABLE "chat_message" ADD COLUMN "executionId" varchar;`)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP TABLE execution`)
|
||||
await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "executionId";`)
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import { AddArtifactsToChatMessage1726156258465 } from './1726156258465-AddArtif
|
||||
import { AddCustomTemplate1725629836652 } from './1725629836652-AddCustomTemplate'
|
||||
import { AddFollowUpPrompts1726666294213 } from './1726666294213-AddFollowUpPrompts'
|
||||
import { AddTypeToAssistant1733011290987 } from './1733011290987-AddTypeToAssistant'
|
||||
import { AddExecutionEntity1738090872625 } from './1738090872625-AddExecutionEntity'
|
||||
|
||||
export const sqliteMigrations = [
|
||||
Init1693835579790,
|
||||
@@ -57,5 +58,6 @@ export const sqliteMigrations = [
|
||||
AddArtifactsToChatMessage1726156258465,
|
||||
AddCustomTemplate1725629836652,
|
||||
AddFollowUpPrompts1726666294213,
|
||||
AddTypeToAssistant1733011290987
|
||||
AddTypeToAssistant1733011290987,
|
||||
AddExecutionEntity1738090872625
|
||||
]
|
||||
|
||||
@@ -7,6 +7,9 @@ import { RedisEventPublisher } from './RedisEventPublisher'
|
||||
import { AbortControllerPool } from '../AbortControllerPool'
|
||||
import { BaseQueue } from './BaseQueue'
|
||||
import { RedisOptions } from 'bullmq'
|
||||
import logger from '../utils/logger'
|
||||
import { generateAgentflowv2 as generateAgentflowv2_json } from 'flowise-components'
|
||||
import { databaseEntities } from '../utils'
|
||||
|
||||
interface PredictionQueueOptions {
|
||||
appDataSource: DataSource
|
||||
@@ -16,6 +19,15 @@ interface PredictionQueueOptions {
|
||||
abortControllerPool: AbortControllerPool
|
||||
}
|
||||
|
||||
interface IGenerateAgentflowv2Params extends IExecuteFlowParams {
|
||||
prompt: string
|
||||
componentNodes: IComponentNodes
|
||||
toolNodes: IComponentNodes
|
||||
selectedChatModel: Record<string, any>
|
||||
question: string
|
||||
isAgentFlowGenerator: boolean
|
||||
}
|
||||
|
||||
export class PredictionQueue extends BaseQueue {
|
||||
private componentNodes: IComponentNodes
|
||||
private telemetry: Telemetry
|
||||
@@ -45,13 +57,24 @@ export class PredictionQueue extends BaseQueue {
|
||||
return this.queue
|
||||
}
|
||||
|
||||
async processJob(data: IExecuteFlowParams) {
|
||||
async processJob(data: IExecuteFlowParams | IGenerateAgentflowv2Params) {
|
||||
if (this.appDataSource) data.appDataSource = this.appDataSource
|
||||
if (this.telemetry) data.telemetry = this.telemetry
|
||||
if (this.cachePool) data.cachePool = this.cachePool
|
||||
if (this.componentNodes) data.componentNodes = this.componentNodes
|
||||
if (this.redisPublisher) data.sseStreamer = this.redisPublisher
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(data, 'isAgentFlowGenerator')) {
|
||||
logger.info('Generating Agentflow...')
|
||||
const { prompt, componentNodes, toolNodes, selectedChatModel, question } = data as IGenerateAgentflowv2Params
|
||||
const options: Record<string, any> = {
|
||||
appDataSource: this.appDataSource,
|
||||
databaseEntities: databaseEntities,
|
||||
logger: logger
|
||||
}
|
||||
return await generateAgentflowv2_json({ prompt, componentNodes, toolNodes, selectedChatModel }, question, options)
|
||||
}
|
||||
|
||||
if (this.abortControllerPool) {
|
||||
const abortControllerId = `${data.chatflow.id}_${data.chatId}`
|
||||
const signal = new AbortController()
|
||||
|
||||
@@ -119,6 +119,21 @@ export class RedisEventPublisher implements IServerSideEventStreamer {
|
||||
}
|
||||
}
|
||||
|
||||
streamCalledToolsEvent(chatId: string, data: any) {
|
||||
try {
|
||||
this.redisPublisher.publish(
|
||||
chatId,
|
||||
JSON.stringify({
|
||||
chatId,
|
||||
eventType: 'calledTools',
|
||||
data
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error streaming calledTools event:', error)
|
||||
}
|
||||
}
|
||||
|
||||
streamFileAnnotationsEvent(chatId: string, data: any) {
|
||||
try {
|
||||
this.redisPublisher.publish(
|
||||
@@ -164,6 +179,36 @@ export class RedisEventPublisher implements IServerSideEventStreamer {
|
||||
}
|
||||
}
|
||||
|
||||
streamAgentFlowEvent(chatId: string, data: any): void {
|
||||
try {
|
||||
this.redisPublisher.publish(
|
||||
chatId,
|
||||
JSON.stringify({
|
||||
chatId,
|
||||
eventType: 'agentFlowEvent',
|
||||
data
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error streaming agentFlow event:', error)
|
||||
}
|
||||
}
|
||||
|
||||
streamAgentFlowExecutedDataEvent(chatId: string, data: any): void {
|
||||
try {
|
||||
this.redisPublisher.publish(
|
||||
chatId,
|
||||
JSON.stringify({
|
||||
chatId,
|
||||
eventType: 'agentFlowExecutedData',
|
||||
data
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error streaming agentFlowExecutedData event:', error)
|
||||
}
|
||||
}
|
||||
|
||||
streamNextAgentEvent(chatId: string, data: any): void {
|
||||
try {
|
||||
this.redisPublisher.publish(
|
||||
@@ -179,6 +224,21 @@ export class RedisEventPublisher implements IServerSideEventStreamer {
|
||||
}
|
||||
}
|
||||
|
||||
streamNextAgentFlowEvent(chatId: string, data: any): void {
|
||||
try {
|
||||
this.redisPublisher.publish(
|
||||
chatId,
|
||||
JSON.stringify({
|
||||
chatId,
|
||||
eventType: 'nextAgentFlow',
|
||||
data
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error streaming nextAgentFlow event:', error)
|
||||
}
|
||||
}
|
||||
|
||||
streamActionEvent(chatId: string, data: any): void {
|
||||
try {
|
||||
this.redisPublisher.publish(
|
||||
@@ -254,6 +314,21 @@ export class RedisEventPublisher implements IServerSideEventStreamer {
|
||||
}
|
||||
}
|
||||
|
||||
streamUsageMetadataEvent(chatId: string, data: any): void {
|
||||
try {
|
||||
this.redisPublisher.publish(
|
||||
chatId,
|
||||
JSON.stringify({
|
||||
chatId,
|
||||
eventType: 'usageMetadata',
|
||||
data
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error streaming usage metadata event:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
if (this.redisPublisher) {
|
||||
await this.redisPublisher.quit()
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import express from 'express'
|
||||
import agentflowv2GeneratorController from '../../controllers/agentflowv2-generator'
|
||||
const router = express.Router()
|
||||
|
||||
router.post('/generate', agentflowv2GeneratorController.generateAgentflowv2)
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,16 @@
|
||||
import express from 'express'
|
||||
import executionController from '../../controllers/executions'
|
||||
const router = express.Router()
|
||||
|
||||
// READ
|
||||
router.get('/', executionController.getAllExecutions)
|
||||
router.get(['/', '/:id'], executionController.getExecutionById)
|
||||
|
||||
// PUT
|
||||
router.put(['/', '/:id'], executionController.updateExecution)
|
||||
|
||||
// DELETE - single execution or multiple executions
|
||||
router.delete('/:id', executionController.deleteExecutions)
|
||||
router.delete('/', executionController.deleteExecutions)
|
||||
|
||||
export default router
|
||||
@@ -35,6 +35,7 @@ import predictionRouter from './predictions'
|
||||
import promptListsRouter from './prompts-lists'
|
||||
import publicChatbotRouter from './public-chatbots'
|
||||
import publicChatflowsRouter from './public-chatflows'
|
||||
import publicExecutionsRouter from './public-executions'
|
||||
import statsRouter from './stats'
|
||||
import toolsRouter from './tools'
|
||||
import upsertHistoryRouter from './upsert-history'
|
||||
@@ -43,6 +44,9 @@ import vectorRouter from './vectors'
|
||||
import verifyRouter from './verify'
|
||||
import versionRouter from './versions'
|
||||
import nvidiaNimRouter from './nvidia-nim'
|
||||
import executionsRouter from './executions'
|
||||
import validationRouter from './validation'
|
||||
import agentflowv2GeneratorRouter from './agentflowv2-generator'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
@@ -82,6 +86,7 @@ router.use('/prediction', predictionRouter)
|
||||
router.use('/prompts-list', promptListsRouter)
|
||||
router.use('/public-chatbotConfig', publicChatbotRouter)
|
||||
router.use('/public-chatflows', publicChatflowsRouter)
|
||||
router.use('/public-executions', publicExecutionsRouter)
|
||||
router.use('/stats', statsRouter)
|
||||
router.use('/tools', toolsRouter)
|
||||
router.use('/variables', variablesRouter)
|
||||
@@ -90,5 +95,8 @@ router.use('/verify', verifyRouter)
|
||||
router.use('/version', versionRouter)
|
||||
router.use('/upsert-history', upsertHistoryRouter)
|
||||
router.use('/nvidia-nim', nvidiaNimRouter)
|
||||
router.use('/executions', executionsRouter)
|
||||
router.use('/validation', validationRouter)
|
||||
router.use('/agentflowv2-generator', agentflowv2GeneratorRouter)
|
||||
|
||||
export default router
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import express from 'express'
|
||||
import executionController from '../../controllers/executions'
|
||||
const router = express.Router()
|
||||
|
||||
// CREATE
|
||||
|
||||
// READ
|
||||
router.get(['/', '/:id'], executionController.getPublicExecutionById)
|
||||
|
||||
// UPDATE
|
||||
|
||||
// DELETE
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,8 @@
|
||||
import express from 'express'
|
||||
import validationController from '../../controllers/validation'
|
||||
const router = express.Router()
|
||||
|
||||
// READ
|
||||
router.get('/:id', validationController.checkFlowValidation)
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,248 @@
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import { InternalFlowiseError } from '../../errors/internalFlowiseError'
|
||||
import { getErrorMessage } from '../../errors/utils'
|
||||
import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
|
||||
import path from 'path'
|
||||
import * as fs from 'fs'
|
||||
import { generateAgentflowv2 as generateAgentflowv2_json } from 'flowise-components'
|
||||
import { z } from 'zod'
|
||||
import { sysPrompt } from './prompt'
|
||||
import { databaseEntities } from '../../utils'
|
||||
import logger from '../../utils/logger'
|
||||
import { MODE } from '../../Interface'
|
||||
|
||||
// Define the Zod schema for Agentflowv2 data structure
|
||||
const NodeType = z.object({
|
||||
id: z.string(),
|
||||
type: z.string(),
|
||||
position: z.object({
|
||||
x: z.number(),
|
||||
y: z.number()
|
||||
}),
|
||||
width: z.number(),
|
||||
height: z.number(),
|
||||
selected: z.boolean().optional(),
|
||||
positionAbsolute: z
|
||||
.object({
|
||||
x: z.number(),
|
||||
y: z.number()
|
||||
})
|
||||
.optional(),
|
||||
dragging: z.boolean().optional(),
|
||||
data: z.any().optional(),
|
||||
parentNode: z.string().optional()
|
||||
})
|
||||
|
||||
const EdgeType = z.object({
|
||||
source: z.string(),
|
||||
sourceHandle: z.string(),
|
||||
target: z.string(),
|
||||
targetHandle: z.string(),
|
||||
data: z
|
||||
.object({
|
||||
sourceColor: z.string().optional(),
|
||||
targetColor: z.string().optional(),
|
||||
edgeLabel: z.string().optional(),
|
||||
isHumanInput: z.boolean().optional()
|
||||
})
|
||||
.optional(),
|
||||
type: z.string().optional(),
|
||||
id: z.string()
|
||||
})
|
||||
|
||||
const AgentFlowV2Type = z
|
||||
.object({
|
||||
description: z.string().optional(),
|
||||
usecases: z.array(z.string()).optional(),
|
||||
nodes: z.array(NodeType),
|
||||
edges: z.array(EdgeType)
|
||||
})
|
||||
.describe('Generate Agentflowv2 nodes and edges')
|
||||
|
||||
// Type for the templates array
|
||||
type AgentFlowV2Template = z.infer<typeof AgentFlowV2Type>
|
||||
|
||||
const getAllAgentFlow2Nodes = async () => {
|
||||
const appServer = getRunningExpressApp()
|
||||
const nodes = appServer.nodesPool.componentNodes
|
||||
const agentFlow2Nodes = []
|
||||
for (const node in nodes) {
|
||||
if (nodes[node].category === 'Agent Flows') {
|
||||
agentFlow2Nodes.push({
|
||||
name: nodes[node].name,
|
||||
label: nodes[node].label,
|
||||
description: nodes[node].description
|
||||
})
|
||||
}
|
||||
}
|
||||
return JSON.stringify(agentFlow2Nodes, null, 2)
|
||||
}
|
||||
|
||||
const getAllToolNodes = async () => {
|
||||
const appServer = getRunningExpressApp()
|
||||
const nodes = appServer.nodesPool.componentNodes
|
||||
const toolNodes = []
|
||||
const disabled_nodes = process.env.DISABLED_NODES ? process.env.DISABLED_NODES.split(',') : []
|
||||
const removeTools = ['chainTool', 'retrieverTool', 'webBrowser', ...disabled_nodes]
|
||||
|
||||
for (const node in nodes) {
|
||||
if (nodes[node].category.includes('Tools')) {
|
||||
if (removeTools.includes(nodes[node].name)) {
|
||||
continue
|
||||
}
|
||||
toolNodes.push({
|
||||
name: nodes[node].name,
|
||||
description: nodes[node].description
|
||||
})
|
||||
}
|
||||
}
|
||||
return JSON.stringify(toolNodes, null, 2)
|
||||
}
|
||||
|
||||
const getAllAgentflowv2Marketplaces = async () => {
|
||||
const templates: AgentFlowV2Template[] = []
|
||||
let marketplaceDir = path.join(__dirname, '..', '..', '..', 'marketplaces', 'agentflowsv2')
|
||||
let jsonsInDir = fs.readdirSync(marketplaceDir).filter((file) => path.extname(file) === '.json')
|
||||
jsonsInDir.forEach((file) => {
|
||||
try {
|
||||
const filePath = path.join(__dirname, '..', '..', '..', 'marketplaces', 'agentflowsv2', file)
|
||||
const fileData = fs.readFileSync(filePath)
|
||||
const fileDataObj = JSON.parse(fileData.toString())
|
||||
// get rid of the node.data, remain all other properties
|
||||
const filteredNodes = fileDataObj.nodes.map((node: any) => {
|
||||
return {
|
||||
...node,
|
||||
data: undefined
|
||||
}
|
||||
})
|
||||
|
||||
const template = {
|
||||
title: file.split('.json')[0],
|
||||
description: fileDataObj.description || `Template from ${file}`,
|
||||
usecases: fileDataObj.usecases || [],
|
||||
nodes: filteredNodes,
|
||||
edges: fileDataObj.edges
|
||||
}
|
||||
|
||||
// Validate template against schema
|
||||
const validatedTemplate = AgentFlowV2Type.parse(template)
|
||||
templates.push(validatedTemplate)
|
||||
} catch (error) {
|
||||
console.error(`Error processing template file ${file}:`, error)
|
||||
// Continue with next file instead of failing completely
|
||||
}
|
||||
})
|
||||
|
||||
// Format templates into the requested string format
|
||||
let formattedTemplates = ''
|
||||
templates.forEach((template: AgentFlowV2Template, index: number) => {
|
||||
formattedTemplates += `Example ${index + 1}: <<${(template as any).title}>> - ${template.description}\n`
|
||||
formattedTemplates += `"nodes": [\n`
|
||||
|
||||
// Format nodes with proper indentation
|
||||
const nodesJson = JSON.stringify(template.nodes, null, 3)
|
||||
// Split by newlines and add 3 spaces to the beginning of each line except the first and last
|
||||
const nodesLines = nodesJson.split('\n')
|
||||
if (nodesLines.length > 2) {
|
||||
formattedTemplates += ` ${nodesLines[0]}\n`
|
||||
for (let i = 1; i < nodesLines.length - 1; i++) {
|
||||
formattedTemplates += ` ${nodesLines[i]}\n`
|
||||
}
|
||||
formattedTemplates += ` ${nodesLines[nodesLines.length - 1]}\n`
|
||||
} else {
|
||||
formattedTemplates += ` ${nodesJson}\n`
|
||||
}
|
||||
|
||||
formattedTemplates += `]\n`
|
||||
formattedTemplates += `"edges": [\n`
|
||||
|
||||
// Format edges with proper indentation
|
||||
const edgesJson = JSON.stringify(template.edges, null, 3)
|
||||
// Split by newlines and add tab to the beginning of each line except the first and last
|
||||
const edgesLines = edgesJson.split('\n')
|
||||
if (edgesLines.length > 2) {
|
||||
formattedTemplates += `\t${edgesLines[0]}\n`
|
||||
for (let i = 1; i < edgesLines.length - 1; i++) {
|
||||
formattedTemplates += `\t${edgesLines[i]}\n`
|
||||
}
|
||||
formattedTemplates += `\t${edgesLines[edgesLines.length - 1]}\n`
|
||||
} else {
|
||||
formattedTemplates += `\t${edgesJson}\n`
|
||||
}
|
||||
|
||||
formattedTemplates += `]\n\n`
|
||||
})
|
||||
|
||||
return formattedTemplates
|
||||
}
|
||||
|
||||
const generateAgentflowv2 = async (question: string, selectedChatModel: Record<string, any>) => {
|
||||
try {
|
||||
const agentFlow2Nodes = await getAllAgentFlow2Nodes()
|
||||
const toolNodes = await getAllToolNodes()
|
||||
const marketplaceTemplates = await getAllAgentflowv2Marketplaces()
|
||||
|
||||
const prompt = sysPrompt
|
||||
.replace('{agentFlow2Nodes}', agentFlow2Nodes)
|
||||
.replace('{marketplaceTemplates}', marketplaceTemplates)
|
||||
.replace('{userRequest}', question)
|
||||
const options: Record<string, any> = {
|
||||
appDataSource: getRunningExpressApp().AppDataSource,
|
||||
databaseEntities: databaseEntities,
|
||||
logger: logger
|
||||
}
|
||||
|
||||
let response
|
||||
|
||||
if (process.env.MODE === MODE.QUEUE) {
|
||||
const predictionQueue = getRunningExpressApp().queueManager.getQueue('prediction')
|
||||
const job = await predictionQueue.addJob({
|
||||
prompt,
|
||||
question,
|
||||
toolNodes,
|
||||
selectedChatModel,
|
||||
isAgentFlowGenerator: true
|
||||
})
|
||||
logger.debug(`[server]: Generated Agentflowv2 Job added to queue: ${job.id}`)
|
||||
const queueEvents = predictionQueue.getQueueEvents()
|
||||
response = await job.waitUntilFinished(queueEvents)
|
||||
} else {
|
||||
response = await generateAgentflowv2_json(
|
||||
{ prompt, componentNodes: getRunningExpressApp().nodesPool.componentNodes, toolNodes, selectedChatModel },
|
||||
question,
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to parse and validate the response if it's a string
|
||||
if (typeof response === 'string') {
|
||||
const parsedResponse = JSON.parse(response)
|
||||
const validatedResponse = AgentFlowV2Type.parse(parsedResponse)
|
||||
return validatedResponse
|
||||
}
|
||||
// If response is already an object
|
||||
else if (typeof response === 'object') {
|
||||
const validatedResponse = AgentFlowV2Type.parse(response)
|
||||
return validatedResponse
|
||||
}
|
||||
// Unexpected response type
|
||||
else {
|
||||
throw new Error(`Unexpected response type: ${typeof response}`)
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse or validate response:', parseError)
|
||||
// If parsing fails, return an error object
|
||||
return {
|
||||
error: 'Failed to validate response format',
|
||||
rawResponse: response
|
||||
} as any // Type assertion to avoid type errors
|
||||
}
|
||||
} catch (error) {
|
||||
throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: generateAgentflowv2 - ${getErrorMessage(error)}`)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
generateAgentflowv2
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
export const sysPromptBackup = `You are a workflow orchestrator that is designed to make agent coordination and execution easy. Workflow consists of nodes and edges. Your goal is to generate nodes and edges needed for the workflow to achieve the given task.
|
||||
|
||||
Here are the nodes to choose from:
|
||||
{agentFlow2Nodes}
|
||||
|
||||
Here's some examples of workflows, take a look at which nodes are most relevant to the task and how the nodes and edges are connected:
|
||||
{marketplaceTemplates}
|
||||
|
||||
Now, let's generate the nodes and edges for the user's request.
|
||||
The response should be in JSON format with "nodes" and "edges" arrays, following the structure shown in the examples.
|
||||
|
||||
Think carefully, break down the task into smaller steps and think about which nodes are needed for each step.
|
||||
1. First, take a look at the examples and use them as references to think about which nodes are needed to achieve the task. It must always start with startAgentflow node, and have at least 2 nodes in total. You MUST only use nodes that are in the list of nodes above. Each node must have a unique incrementing id.
|
||||
2. Then, think about the edges between the nodes.
|
||||
3. An agentAgentflow is an AI Agent that can use tools to accomplish goals, executing decisions, automating tasks, and interacting with the real world autonomously such as web search, interact with database and API, send messages, book appointments, etc. Always place higher priority to this and see if the tasks can be accomplished by this node. Use this node if you are asked to create an agent that can perform multiple tasks autonomously.
|
||||
4. A llmAgentflow is excel at processing, understanding, and generating human-like language. It can be used for generating text, summarizing, translating, returning JSON outputs, etc.
|
||||
5. If you need to execute the tool sequentially after another, you can use the toolAgentflow node.
|
||||
6. If you need to iterate over a set of data, you can use the iteration node. You must have at least 1 node inside the iteration node. The children nodes will be executed N times, where N is the number of items in the iterationInput array. The children nodes must have the property "parentNode" and the value must be the id of the iteration node.
|
||||
7. If you can't find a node that fits the task, you can use the httpAgentflow node to execute a http request. For example, to retrieve data from 3rd party APIs, or to send data to a webhook
|
||||
8. If you need to dynamically choose between user intention, for example classifying the user's intent, you can use the conditionAgentAgentflow node. For defined conditions, you can use the conditionAgentflow node.
|
||||
`
|
||||
|
||||
export const sysPrompt = `You are an advanced workflow orchestrator designed to generate nodes and edges for complex tasks. Your goal is to create a workflow that accomplishes the given user request efficiently and effectively.
|
||||
|
||||
Your task is to generate a workflow for the following user request:
|
||||
|
||||
<user_request>
|
||||
{userRequest}
|
||||
</user_request>
|
||||
|
||||
First, review the available nodes for this system:
|
||||
|
||||
<available_nodes>
|
||||
{agentFlow2Nodes}
|
||||
</available_nodes>
|
||||
|
||||
Now, examine these workflow examples to understand how nodes are typically connected and which are most relevant for different tasks:
|
||||
|
||||
<workflow_examples>
|
||||
{marketplaceTemplates}
|
||||
</workflow_examples>
|
||||
|
||||
To create this workflow, follow these steps and wrap your thought process in <workflow_planning> tags inside your thinking block:
|
||||
|
||||
1. List out all the key components of the user request.
|
||||
2. Analyze the user request and break it down into smaller steps.
|
||||
3. For each step, consider which nodes are most appropriate and match each component with potential nodes. Remember:
|
||||
- Always start with a startAgentflow node.
|
||||
- Include at least 2 nodes in total.
|
||||
- Only use nodes from the available nodes list.
|
||||
- Assign each node a unique, incrementing ID.
|
||||
4. Outline the overall structure of the workflow.
|
||||
5. Determine the logical connections (edges) between the nodes.
|
||||
6. Consider special cases:
|
||||
- Use agentAgentflow for multiple autonomous tasks.
|
||||
- Use llmAgentflow for language processing tasks.
|
||||
- Use toolAgentflow for sequential tool execution.
|
||||
- Use iteration node when you need to iterate over a set of data (must include at least one child node with a "parentNode" property).
|
||||
- Use httpAgentflow for API requests or webhooks.
|
||||
- Use conditionAgentAgentflow for dynamic choices or conditionAgentflow for defined conditions.
|
||||
- Use humanInputAgentflow for human input and review.
|
||||
- Use loopAgentflow for repetitive tasks, or when back and forth communication is needed such as hierarchical workflows.
|
||||
|
||||
After your analysis, provide the final workflow as a JSON object with "nodes" and "edges" arrays.
|
||||
|
||||
Begin your analysis and workflow creation process now. Your final output should consist only of the JSON object with the workflow and should not duplicate or rehash any of the work you did in the workflow planning section.`
|
||||
@@ -433,9 +433,10 @@ const getDocumentStores = async (): Promise<any> => {
|
||||
const getTools = async (): Promise<any> => {
|
||||
try {
|
||||
const tools = await nodesService.getAllNodesForCategory('Tools')
|
||||
const mcpTools = await nodesService.getAllNodesForCategory('Tools (MCP)')
|
||||
|
||||
// filter out those tools that input params type are not in the list
|
||||
const filteredTools = tools.filter((tool) => {
|
||||
const filteredTools = [...tools, ...mcpTools].filter((tool) => {
|
||||
const inputs = tool.inputs || []
|
||||
return inputs.every((input) => INPUT_PARAMS_TYPE.includes(input.type))
|
||||
})
|
||||
|
||||
@@ -118,6 +118,7 @@ const removeAllChatMessages = async (
|
||||
logger.error(`[server]: Error deleting file storage for chatflow ${chatflowid}, chatId ${chatId}: ${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
const dbResponse = await appServer.AppDataSource.getRepository(ChatMessage).delete(deleteOptions)
|
||||
return dbResponse
|
||||
} catch (error) {
|
||||
@@ -136,6 +137,10 @@ const removeChatMessagesByMessageIds = async (
|
||||
try {
|
||||
const appServer = getRunningExpressApp()
|
||||
|
||||
// Get messages before deletion to check for executionId
|
||||
const messages = await appServer.AppDataSource.getRepository(ChatMessage).findByIds(messageIds)
|
||||
const executionIds = messages.map((msg) => msg.executionId).filter(Boolean)
|
||||
|
||||
for (const [composite_key] of chatIdMap) {
|
||||
const [chatId] = composite_key.split('_')
|
||||
|
||||
@@ -147,6 +152,11 @@ const removeChatMessagesByMessageIds = async (
|
||||
await removeFilesFromStorage(chatflowid, chatId)
|
||||
}
|
||||
|
||||
// Delete executions if they exist
|
||||
if (executionIds.length > 0) {
|
||||
await appServer.AppDataSource.getRepository('Execution').delete(executionIds)
|
||||
}
|
||||
|
||||
const dbResponse = await appServer.AppDataSource.getRepository(ChatMessage).delete(messageIds)
|
||||
return dbResponse
|
||||
} catch (error) {
|
||||
|
||||
@@ -38,6 +38,10 @@ const checkIfChatflowIsValidForStreaming = async (chatflowId: string): Promise<a
|
||||
}
|
||||
}
|
||||
|
||||
if (chatflow.type === 'AGENTFLOW') {
|
||||
return { isStreaming: true }
|
||||
}
|
||||
|
||||
/*** Get Ending Node with Directed Graph ***/
|
||||
const flowData = chatflow.flowData
|
||||
const parsedFlowData: IReactFlowObject = JSON.parse(flowData)
|
||||
@@ -121,6 +125,8 @@ const getAllChatflows = async (type?: ChatflowType): Promise<ChatFlow[]> => {
|
||||
const dbResponse = await appServer.AppDataSource.getRepository(ChatFlow).find()
|
||||
if (type === 'MULTIAGENT') {
|
||||
return dbResponse.filter((chatflow) => chatflow.type === 'MULTIAGENT')
|
||||
} else if (type === 'AGENTFLOW') {
|
||||
return dbResponse.filter((chatflow) => chatflow.type === 'AGENTFLOW')
|
||||
} else if (type === 'ASSISTANT') {
|
||||
return dbResponse.filter((chatflow) => chatflow.type === 'ASSISTANT')
|
||||
} else if (type === 'CHATFLOW') {
|
||||
@@ -336,7 +342,7 @@ const getSinglePublicChatbotConfig = async (chatflowId: string): Promise<any> =>
|
||||
if (dbResponse.chatbotConfig || uploadsConfig) {
|
||||
try {
|
||||
const parsedConfig = dbResponse.chatbotConfig ? JSON.parse(dbResponse.chatbotConfig) : {}
|
||||
return { ...parsedConfig, uploads: uploadsConfig }
|
||||
return { ...parsedConfig, uploads: uploadsConfig, flowData: dbResponse.flowData }
|
||||
} catch (e) {
|
||||
throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error parsing Chatbot Config for Chatflow ${chatflowId}`)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import { InternalFlowiseError } from '../../errors/internalFlowiseError'
|
||||
import { getErrorMessage } from '../../errors/utils'
|
||||
import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
|
||||
import { Execution } from '../../database/entities/Execution'
|
||||
import { ExecutionState, IAgentflowExecutedData } from '../../Interface'
|
||||
import { In } from 'typeorm'
|
||||
import { ChatMessage } from '../../database/entities/ChatMessage'
|
||||
import { _removeCredentialId } from '../../utils/buildAgentflow'
|
||||
|
||||
interface ExecutionFilters {
|
||||
id?: string
|
||||
agentflowId?: string
|
||||
sessionId?: string
|
||||
state?: ExecutionState
|
||||
startDate?: Date
|
||||
endDate?: Date
|
||||
page?: number
|
||||
limit?: number
|
||||
}
|
||||
|
||||
const getExecutionById = async (executionId: string): Promise<Execution | null> => {
|
||||
try {
|
||||
const appServer = getRunningExpressApp()
|
||||
const executionRepository = appServer.AppDataSource.getRepository(Execution)
|
||||
const res = await executionRepository.findOne({ where: { id: executionId } })
|
||||
if (!res) {
|
||||
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `Execution ${executionId} not found`)
|
||||
}
|
||||
return res
|
||||
} catch (error) {
|
||||
throw new InternalFlowiseError(
|
||||
StatusCodes.INTERNAL_SERVER_ERROR,
|
||||
`Error: executionsService.getExecutionById - ${getErrorMessage(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const getPublicExecutionById = async (executionId: string): Promise<Execution | null> => {
|
||||
try {
|
||||
const appServer = getRunningExpressApp()
|
||||
const executionRepository = appServer.AppDataSource.getRepository(Execution)
|
||||
const res = await executionRepository.findOne({ where: { id: executionId, isPublic: true } })
|
||||
if (!res) {
|
||||
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `Execution ${executionId} not found`)
|
||||
}
|
||||
const executionData = typeof res?.executionData === 'string' ? JSON.parse(res?.executionData) : res?.executionData
|
||||
const executionDataWithoutCredentialId = executionData.map((data: IAgentflowExecutedData) => _removeCredentialId(data))
|
||||
const stringifiedExecutionData = JSON.stringify(executionDataWithoutCredentialId)
|
||||
return { ...res, executionData: stringifiedExecutionData }
|
||||
} catch (error) {
|
||||
throw new InternalFlowiseError(
|
||||
StatusCodes.INTERNAL_SERVER_ERROR,
|
||||
`Error: executionsService.getPublicExecutionById - ${getErrorMessage(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const getAllExecutions = async (filters: ExecutionFilters = {}): Promise<{ data: Execution[]; total: number }> => {
|
||||
try {
|
||||
const appServer = getRunningExpressApp()
|
||||
const { id, agentflowId, sessionId, state, startDate, endDate, page = 1, limit = 10 } = filters
|
||||
|
||||
// Handle UUID fields properly using raw parameters to avoid type conversion issues
|
||||
// This uses the query builder instead of direct objects for compatibility with UUID fields
|
||||
const queryBuilder = appServer.AppDataSource.getRepository(Execution)
|
||||
.createQueryBuilder('execution')
|
||||
.leftJoinAndSelect('execution.agentflow', 'agentflow')
|
||||
.orderBy('execution.createdDate', 'DESC')
|
||||
.skip((page - 1) * limit)
|
||||
.take(limit)
|
||||
|
||||
if (id) queryBuilder.andWhere('execution.id = :id', { id })
|
||||
if (agentflowId) queryBuilder.andWhere('execution.agentflowId = :agentflowId', { agentflowId })
|
||||
if (sessionId) queryBuilder.andWhere('execution.sessionId = :sessionId', { sessionId })
|
||||
if (state) queryBuilder.andWhere('execution.state = :state', { state })
|
||||
|
||||
// Date range conditions
|
||||
if (startDate && endDate) {
|
||||
queryBuilder.andWhere('execution.createdDate BETWEEN :startDate AND :endDate', { startDate, endDate })
|
||||
} else if (startDate) {
|
||||
queryBuilder.andWhere('execution.createdDate >= :startDate', { startDate })
|
||||
} else if (endDate) {
|
||||
queryBuilder.andWhere('execution.createdDate <= :endDate', { endDate })
|
||||
}
|
||||
|
||||
const [data, total] = await queryBuilder.getManyAndCount()
|
||||
|
||||
return { data, total }
|
||||
} catch (error) {
|
||||
throw new InternalFlowiseError(
|
||||
StatusCodes.INTERNAL_SERVER_ERROR,
|
||||
`Error: executionsService.getAllExecutions - ${getErrorMessage(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const updateExecution = async (executionId: string, data: Partial<Execution>): Promise<Execution | null> => {
|
||||
try {
|
||||
const appServer = getRunningExpressApp()
|
||||
const execution = await appServer.AppDataSource.getRepository(Execution).findOneBy({
|
||||
id: executionId
|
||||
})
|
||||
if (!execution) {
|
||||
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `Execution ${executionId} not found`)
|
||||
}
|
||||
const updateExecution = new Execution()
|
||||
Object.assign(updateExecution, data)
|
||||
await appServer.AppDataSource.getRepository(Execution).merge(execution, updateExecution)
|
||||
const dbResponse = await appServer.AppDataSource.getRepository(Execution).save(execution)
|
||||
return dbResponse
|
||||
} catch (error) {
|
||||
throw new InternalFlowiseError(
|
||||
StatusCodes.INTERNAL_SERVER_ERROR,
|
||||
`Error: executionsService.updateExecution - ${getErrorMessage(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple executions by their IDs
|
||||
* @param executionIds Array of execution IDs to delete
|
||||
* @returns Object with success status and count of deleted executions
|
||||
*/
|
||||
const deleteExecutions = async (executionIds: string[]): Promise<{ success: boolean; deletedCount: number }> => {
|
||||
try {
|
||||
const appServer = getRunningExpressApp()
|
||||
const executionRepository = appServer.AppDataSource.getRepository(Execution)
|
||||
|
||||
// Delete executions where id is in the provided array
|
||||
const result = await executionRepository.delete({
|
||||
id: In(executionIds)
|
||||
})
|
||||
|
||||
// Update chat message executionId column to NULL
|
||||
await appServer.AppDataSource.getRepository(ChatMessage).update({ executionId: In(executionIds) }, { executionId: null as any })
|
||||
|
||||
return {
|
||||
success: true,
|
||||
deletedCount: result.affected || 0
|
||||
}
|
||||
} catch (error) {
|
||||
throw new InternalFlowiseError(
|
||||
StatusCodes.INTERNAL_SERVER_ERROR,
|
||||
`Error: executionsService.deleteExecutions - ${getErrorMessage(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getExecutionById,
|
||||
getAllExecutions,
|
||||
deleteExecutions,
|
||||
getPublicExecutionById,
|
||||
updateExecution
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { ChatMessageFeedback } from '../../database/entities/ChatMessageFeedback
|
||||
import { CustomTemplate } from '../../database/entities/CustomTemplate'
|
||||
import { DocumentStore } from '../../database/entities/DocumentStore'
|
||||
import { DocumentStoreFileChunk } from '../../database/entities/DocumentStoreFileChunk'
|
||||
import { Execution } from '../../database/entities/Execution'
|
||||
import { Tool } from '../../database/entities/Tool'
|
||||
import { Variable } from '../../database/entities/Variable'
|
||||
import { InternalFlowiseError } from '../../errors/internalFlowiseError'
|
||||
@@ -17,12 +18,14 @@ import assistantService from '../assistants'
|
||||
import chatMessagesService from '../chat-messages'
|
||||
import chatflowService from '../chatflows'
|
||||
import documenStoreService from '../documentstore'
|
||||
import executionService from '../executions'
|
||||
import marketplacesService from '../marketplaces'
|
||||
import toolsService from '../tools'
|
||||
import variableService from '../variables'
|
||||
|
||||
type ExportInput = {
|
||||
agentflow: boolean
|
||||
agentflowv2: boolean
|
||||
assistantCustom: boolean
|
||||
assistantOpenAI: boolean
|
||||
assistantAzure: boolean
|
||||
@@ -31,12 +34,14 @@ type ExportInput = {
|
||||
chat_feedback: boolean
|
||||
custom_template: boolean
|
||||
document_store: boolean
|
||||
execution: boolean
|
||||
tool: boolean
|
||||
variable: boolean
|
||||
}
|
||||
|
||||
type ExportData = {
|
||||
AgentFlow: ChatFlow[]
|
||||
AgentFlowV2: ChatFlow[]
|
||||
AssistantCustom: Assistant[]
|
||||
AssistantFlow: ChatFlow[]
|
||||
AssistantOpenAI: Assistant[]
|
||||
@@ -47,6 +52,7 @@ type ExportData = {
|
||||
CustomTemplate: CustomTemplate[]
|
||||
DocumentStore: DocumentStore[]
|
||||
DocumentStoreFileChunk: DocumentStoreFileChunk[]
|
||||
Execution: Execution[]
|
||||
Tool: Tool[]
|
||||
Variable: Variable[]
|
||||
}
|
||||
@@ -55,6 +61,7 @@ const convertExportInput = (body: any): ExportInput => {
|
||||
try {
|
||||
if (!body || typeof body !== 'object') throw new Error('Invalid ExportInput object in request body')
|
||||
if (body.agentflow && typeof body.agentflow !== 'boolean') throw new Error('Invalid agentflow property in ExportInput object')
|
||||
if (body.agentflowv2 && typeof body.agentflowv2 !== 'boolean') throw new Error('Invalid agentflowv2 property in ExportInput object')
|
||||
if (body.assistant && typeof body.assistant !== 'boolean') throw new Error('Invalid assistant property in ExportInput object')
|
||||
if (body.chatflow && typeof body.chatflow !== 'boolean') throw new Error('Invalid chatflow property in ExportInput object')
|
||||
if (body.chat_message && typeof body.chat_message !== 'boolean')
|
||||
@@ -65,6 +72,7 @@ const convertExportInput = (body: any): ExportInput => {
|
||||
throw new Error('Invalid custom_template property in ExportInput object')
|
||||
if (body.document_store && typeof body.document_store !== 'boolean')
|
||||
throw new Error('Invalid document_store property in ExportInput object')
|
||||
if (body.execution && typeof body.execution !== 'boolean') throw new Error('Invalid execution property in ExportInput object')
|
||||
if (body.tool && typeof body.tool !== 'boolean') throw new Error('Invalid tool property in ExportInput object')
|
||||
if (body.variable && typeof body.variable !== 'boolean') throw new Error('Invalid variable property in ExportInput object')
|
||||
return body as ExportInput
|
||||
@@ -80,6 +88,7 @@ const FileDefaultName = 'ExportData.json'
|
||||
const exportData = async (exportInput: ExportInput): Promise<{ FileDefaultName: string } & ExportData> => {
|
||||
try {
|
||||
let AgentFlow: ChatFlow[] = exportInput.agentflow === true ? await chatflowService.getAllChatflows('MULTIAGENT') : []
|
||||
let AgentFlowV2: ChatFlow[] = exportInput.agentflowv2 === true ? await chatflowService.getAllChatflows('AGENTFLOW') : []
|
||||
|
||||
let AssistantCustom: Assistant[] = exportInput.assistantCustom === true ? await assistantService.getAllAssistants('CUSTOM') : []
|
||||
let AssistantFlow: ChatFlow[] = exportInput.assistantCustom === true ? await chatflowService.getAllChatflows('ASSISTANT') : []
|
||||
@@ -103,6 +112,9 @@ const exportData = async (exportInput: ExportInput): Promise<{ FileDefaultName:
|
||||
let DocumentStoreFileChunk: DocumentStoreFileChunk[] =
|
||||
exportInput.document_store === true ? await documenStoreService.getAllDocumentFileChunks() : []
|
||||
|
||||
const { data: totalExecutions } = exportInput.execution === true ? await executionService.getAllExecutions() : { data: [] }
|
||||
let Execution: Execution[] = exportInput.execution === true ? totalExecutions : []
|
||||
|
||||
let Tool: Tool[] = exportInput.tool === true ? await toolsService.getAllTools() : []
|
||||
|
||||
let Variable: Variable[] = exportInput.variable === true ? await variableService.getAllVariables() : []
|
||||
@@ -110,6 +122,7 @@ const exportData = async (exportInput: ExportInput): Promise<{ FileDefaultName:
|
||||
return {
|
||||
FileDefaultName,
|
||||
AgentFlow,
|
||||
AgentFlowV2,
|
||||
AssistantCustom,
|
||||
AssistantFlow,
|
||||
AssistantOpenAI,
|
||||
@@ -120,6 +133,7 @@ const exportData = async (exportInput: ExportInput): Promise<{ FileDefaultName:
|
||||
CustomTemplate,
|
||||
DocumentStore,
|
||||
DocumentStoreFileChunk,
|
||||
Execution,
|
||||
Tool,
|
||||
Variable
|
||||
}
|
||||
@@ -180,8 +194,9 @@ async function replaceDuplicateIdsForChatMessage(queryRunner: QueryRunner, origi
|
||||
})
|
||||
const originalDataChatflowIds = [
|
||||
...originalData.AssistantFlow.map((assistantFlow) => assistantFlow.id),
|
||||
...originalData.AgentFlow.map((agentflow) => agentflow.id),
|
||||
...originalData.ChatFlow.map((chatflow) => chatflow.id)
|
||||
...originalData.AgentFlow.map((agentFlow) => agentFlow.id),
|
||||
...originalData.AgentFlowV2.map((agentFlowV2) => agentFlowV2.id),
|
||||
...originalData.ChatFlow.map((chatFlow) => chatFlow.id)
|
||||
]
|
||||
chatmessageChatflowIds.forEach((item) => {
|
||||
if (originalDataChatflowIds.includes(item.id)) {
|
||||
@@ -224,6 +239,54 @@ async function replaceDuplicateIdsForChatMessage(queryRunner: QueryRunner, origi
|
||||
}
|
||||
}
|
||||
|
||||
async function replaceExecutionIdForChatMessage(queryRunner: QueryRunner, originalData: ExportData, chatMessages: ChatMessage[]) {
|
||||
try {
|
||||
// step 1 - get all execution ids from chatMessages
|
||||
const chatMessageExecutionIds = chatMessages
|
||||
.map((chatMessage) => {
|
||||
return { id: chatMessage.executionId, qty: 0 }
|
||||
})
|
||||
.filter((item): item is { id: string; qty: number } => item !== undefined)
|
||||
|
||||
// step 2 - increase qty if execution id is in importData.Execution
|
||||
const originalDataExecutionIds = originalData.Execution.map((execution) => execution.id)
|
||||
chatMessageExecutionIds.forEach((item) => {
|
||||
if (originalDataExecutionIds.includes(item.id)) {
|
||||
item.qty += 1
|
||||
}
|
||||
})
|
||||
|
||||
// step 3 - increase qty if execution id is in database
|
||||
const databaseExecutionIds = await (
|
||||
await queryRunner.manager.find(Execution, {
|
||||
where: { id: In(chatMessageExecutionIds.map((chatMessageExecutionId) => chatMessageExecutionId.id)) }
|
||||
})
|
||||
).map((execution) => execution.id)
|
||||
chatMessageExecutionIds.forEach((item) => {
|
||||
if (databaseExecutionIds.includes(item.id)) {
|
||||
item.qty += 1
|
||||
}
|
||||
})
|
||||
|
||||
// step 4 - if executionIds not found replace with NULL
|
||||
const missingExecutionIds = chatMessageExecutionIds.filter((item) => item.qty === 0).map((item) => item.id)
|
||||
chatMessages.forEach((chatMessage) => {
|
||||
if (chatMessage.executionId && missingExecutionIds.includes(chatMessage.executionId)) {
|
||||
delete chatMessage.executionId
|
||||
}
|
||||
})
|
||||
|
||||
originalData.ChatMessage = chatMessages
|
||||
|
||||
return originalData
|
||||
} catch (error) {
|
||||
throw new InternalFlowiseError(
|
||||
StatusCodes.INTERNAL_SERVER_ERROR,
|
||||
`Error: exportImportService.replaceExecutionIdForChatMessage - ${getErrorMessage(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function replaceDuplicateIdsForChatMessageFeedback(
|
||||
queryRunner: QueryRunner,
|
||||
originalData: ExportData,
|
||||
@@ -235,8 +298,9 @@ async function replaceDuplicateIdsForChatMessageFeedback(
|
||||
})
|
||||
const originalDataChatflowIds = [
|
||||
...originalData.AssistantFlow.map((assistantFlow) => assistantFlow.id),
|
||||
...originalData.AgentFlow.map((agentflow) => agentflow.id),
|
||||
...originalData.ChatFlow.map((chatflow) => chatflow.id)
|
||||
...originalData.AgentFlow.map((agentFlow) => agentFlow.id),
|
||||
...originalData.AgentFlowV2.map((agentFlowV2) => agentFlowV2.id),
|
||||
...originalData.ChatFlow.map((chatFlow) => chatFlow.id)
|
||||
]
|
||||
feedbackChatflowIds.forEach((item) => {
|
||||
if (originalDataChatflowIds.includes(item.id)) {
|
||||
@@ -412,6 +476,27 @@ async function replaceDuplicateIdsForVariable(queryRunner: QueryRunner, original
|
||||
}
|
||||
}
|
||||
|
||||
async function replaceDuplicateIdsForExecution(queryRunner: QueryRunner, originalData: ExportData, executions: Execution[]) {
|
||||
try {
|
||||
const ids = executions.map((execution) => execution.id)
|
||||
const records = await queryRunner.manager.find(Execution, {
|
||||
where: { id: In(ids) }
|
||||
})
|
||||
if (records.length < 0) return originalData
|
||||
for (let record of records) {
|
||||
const oldId = record.id
|
||||
const newId = uuidv4()
|
||||
originalData = JSON.parse(JSON.stringify(originalData).replaceAll(oldId, newId))
|
||||
}
|
||||
return originalData
|
||||
} catch (error) {
|
||||
throw new InternalFlowiseError(
|
||||
StatusCodes.INTERNAL_SERVER_ERROR,
|
||||
`Error: exportImportService.replaceDuplicateIdsForExecution - ${getErrorMessage(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function reduceSpaceForChatflowFlowData(chatflows: ChatFlow[]) {
|
||||
return chatflows.map((chatflow) => {
|
||||
return { ...chatflow, flowData: JSON.stringify(JSON.parse(chatflow.flowData)) }
|
||||
@@ -429,6 +514,10 @@ const importData = async (importData: ExportData) => {
|
||||
importData.AgentFlow = reduceSpaceForChatflowFlowData(importData.AgentFlow)
|
||||
importData = await replaceDuplicateIdsForChatFlow(queryRunner, importData, importData.AgentFlow)
|
||||
}
|
||||
if (importData.AgentFlowV2.length > 0) {
|
||||
importData.AgentFlowV2 = reduceSpaceForChatflowFlowData(importData.AgentFlowV2)
|
||||
importData = await replaceDuplicateIdsForChatFlow(queryRunner, importData, importData.AgentFlowV2)
|
||||
}
|
||||
if (importData.AssistantCustom.length > 0)
|
||||
importData = await replaceDuplicateIdsForAssistant(queryRunner, importData, importData.AssistantCustom)
|
||||
if (importData.AssistantFlow.length > 0) {
|
||||
@@ -443,8 +532,10 @@ const importData = async (importData: ExportData) => {
|
||||
importData.ChatFlow = reduceSpaceForChatflowFlowData(importData.ChatFlow)
|
||||
importData = await replaceDuplicateIdsForChatFlow(queryRunner, importData, importData.ChatFlow)
|
||||
}
|
||||
if (importData.ChatMessage.length > 0)
|
||||
if (importData.ChatMessage.length > 0) {
|
||||
importData = await replaceDuplicateIdsForChatMessage(queryRunner, importData, importData.ChatMessage)
|
||||
importData = await replaceExecutionIdForChatMessage(queryRunner, importData, importData.ChatMessage)
|
||||
}
|
||||
if (importData.ChatMessageFeedback.length > 0)
|
||||
importData = await replaceDuplicateIdsForChatMessageFeedback(queryRunner, importData, importData.ChatMessageFeedback)
|
||||
if (importData.CustomTemplate.length > 0)
|
||||
@@ -454,12 +545,15 @@ const importData = async (importData: ExportData) => {
|
||||
if (importData.DocumentStoreFileChunk.length > 0)
|
||||
importData = await replaceDuplicateIdsForDocumentStoreFileChunk(queryRunner, importData, importData.DocumentStoreFileChunk)
|
||||
if (importData.Tool.length > 0) importData = await replaceDuplicateIdsForTool(queryRunner, importData, importData.Tool)
|
||||
if (importData.Execution.length > 0)
|
||||
importData = await replaceDuplicateIdsForExecution(queryRunner, importData, importData.Execution)
|
||||
if (importData.Variable.length > 0)
|
||||
importData = await replaceDuplicateIdsForVariable(queryRunner, importData, importData.Variable)
|
||||
|
||||
await queryRunner.startTransaction()
|
||||
|
||||
if (importData.AgentFlow.length > 0) await queryRunner.manager.save(ChatFlow, importData.AgentFlow)
|
||||
if (importData.AgentFlowV2.length > 0) await queryRunner.manager.save(ChatFlow, importData.AgentFlowV2)
|
||||
if (importData.AssistantFlow.length > 0) await queryRunner.manager.save(ChatFlow, importData.AssistantFlow)
|
||||
if (importData.AssistantCustom.length > 0) await queryRunner.manager.save(Assistant, importData.AssistantCustom)
|
||||
if (importData.AssistantOpenAI.length > 0) await queryRunner.manager.save(Assistant, importData.AssistantOpenAI)
|
||||
@@ -473,6 +567,7 @@ const importData = async (importData: ExportData) => {
|
||||
if (importData.DocumentStoreFileChunk.length > 0)
|
||||
await queryRunner.manager.save(DocumentStoreFileChunk, importData.DocumentStoreFileChunk)
|
||||
if (importData.Tool.length > 0) await queryRunner.manager.save(Tool, importData.Tool)
|
||||
if (importData.Execution.length > 0) await queryRunner.manager.save(Execution, importData.Execution)
|
||||
if (importData.Variable.length > 0) await queryRunner.manager.save(Variable, importData.Variable)
|
||||
|
||||
await queryRunner.commitTransaction()
|
||||
|
||||
@@ -7,6 +7,7 @@ import { IReactFlowEdge, IReactFlowNode } from '../../Interface'
|
||||
import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
|
||||
import { DeleteResult } from 'typeorm'
|
||||
import { CustomTemplate } from '../../database/entities/CustomTemplate'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import chatflowsService from '../chatflows'
|
||||
|
||||
@@ -29,13 +30,13 @@ const getAllTemplates = async () => {
|
||||
let marketplaceDir = path.join(__dirname, '..', '..', '..', 'marketplaces', 'chatflows')
|
||||
let jsonsInDir = fs.readdirSync(marketplaceDir).filter((file) => path.extname(file) === '.json')
|
||||
let templates: any[] = []
|
||||
jsonsInDir.forEach((file, index) => {
|
||||
jsonsInDir.forEach((file) => {
|
||||
const filePath = path.join(__dirname, '..', '..', '..', 'marketplaces', 'chatflows', file)
|
||||
const fileData = fs.readFileSync(filePath)
|
||||
const fileDataObj = JSON.parse(fileData.toString()) as ITemplate
|
||||
|
||||
const template = {
|
||||
id: index,
|
||||
id: uuidv4(),
|
||||
templateName: file.split('.json')[0],
|
||||
flowData: fileData.toString(),
|
||||
badge: fileDataObj?.badge,
|
||||
@@ -50,13 +51,13 @@ const getAllTemplates = async () => {
|
||||
|
||||
marketplaceDir = path.join(__dirname, '..', '..', '..', 'marketplaces', 'tools')
|
||||
jsonsInDir = fs.readdirSync(marketplaceDir).filter((file) => path.extname(file) === '.json')
|
||||
jsonsInDir.forEach((file, index) => {
|
||||
jsonsInDir.forEach((file) => {
|
||||
const filePath = path.join(__dirname, '..', '..', '..', 'marketplaces', 'tools', file)
|
||||
const fileData = fs.readFileSync(filePath)
|
||||
const fileDataObj = JSON.parse(fileData.toString())
|
||||
const template = {
|
||||
...fileDataObj,
|
||||
id: index,
|
||||
id: uuidv4(),
|
||||
type: 'Tool',
|
||||
framework: fileDataObj?.framework,
|
||||
badge: fileDataObj?.badge,
|
||||
@@ -69,12 +70,12 @@ const getAllTemplates = async () => {
|
||||
|
||||
marketplaceDir = path.join(__dirname, '..', '..', '..', 'marketplaces', 'agentflows')
|
||||
jsonsInDir = fs.readdirSync(marketplaceDir).filter((file) => path.extname(file) === '.json')
|
||||
jsonsInDir.forEach((file, index) => {
|
||||
jsonsInDir.forEach((file) => {
|
||||
const filePath = path.join(__dirname, '..', '..', '..', 'marketplaces', 'agentflows', file)
|
||||
const fileData = fs.readFileSync(filePath)
|
||||
const fileDataObj = JSON.parse(fileData.toString())
|
||||
const template = {
|
||||
id: index,
|
||||
id: uuidv4(),
|
||||
templateName: file.split('.json')[0],
|
||||
flowData: fileData.toString(),
|
||||
badge: fileDataObj?.badge,
|
||||
@@ -86,6 +87,26 @@ const getAllTemplates = async () => {
|
||||
}
|
||||
templates.push(template)
|
||||
})
|
||||
|
||||
marketplaceDir = path.join(__dirname, '..', '..', '..', 'marketplaces', 'agentflowsv2')
|
||||
jsonsInDir = fs.readdirSync(marketplaceDir).filter((file) => path.extname(file) === '.json')
|
||||
jsonsInDir.forEach((file) => {
|
||||
const filePath = path.join(__dirname, '..', '..', '..', 'marketplaces', 'agentflowsv2', file)
|
||||
const fileData = fs.readFileSync(filePath)
|
||||
const fileDataObj = JSON.parse(fileData.toString())
|
||||
const template = {
|
||||
id: uuidv4(),
|
||||
templateName: file.split('.json')[0],
|
||||
flowData: fileData.toString(),
|
||||
badge: fileDataObj?.badge,
|
||||
framework: fileDataObj?.framework,
|
||||
usecases: fileDataObj?.usecases,
|
||||
categories: getCategories(fileDataObj),
|
||||
type: 'AgentflowV2',
|
||||
description: fileDataObj?.description || ''
|
||||
}
|
||||
templates.push(template)
|
||||
})
|
||||
const sortedTemplates = templates.sort((a, b) => a.templateName.localeCompare(b.templateName))
|
||||
const FlowiseDocsQnAIndex = sortedTemplates.findIndex((tmp) => tmp.templateName === 'Flowise Docs QnA')
|
||||
if (FlowiseDocsQnAIndex > 0) {
|
||||
@@ -200,6 +221,9 @@ const _generateExportFlowData = (flowData: any) => {
|
||||
version: node.data.version,
|
||||
name: node.data.name,
|
||||
type: node.data.type,
|
||||
color: node.data.color,
|
||||
hideOutput: node.data.hideOutput,
|
||||
hideInput: node.data.hideInput,
|
||||
baseClasses: node.data.baseClasses,
|
||||
tags: node.data.tags,
|
||||
category: node.data.category,
|
||||
|
||||
@@ -97,7 +97,10 @@ const getSingleNodeAsyncOptions = async (nodeName: string, requestBody: any): Pr
|
||||
|
||||
const dbResponse: INodeOptionsValue[] = await nodeInstance.loadMethods![methodName]!.call(nodeInstance, nodeData, {
|
||||
appDataSource: appServer.AppDataSource,
|
||||
databaseEntities: databaseEntities
|
||||
databaseEntities: databaseEntities,
|
||||
componentNodes: appServer.nodesPool.componentNodes,
|
||||
previousNodes: requestBody.previousNodes,
|
||||
currentNode: requestBody.currentNode
|
||||
})
|
||||
|
||||
return dbResponse
|
||||
|
||||
@@ -24,7 +24,7 @@ const getAssistantVectorStore = async (credentialId: string, vectorStoreId: stri
|
||||
}
|
||||
|
||||
const openai = new OpenAI({ apiKey: openAIApiKey })
|
||||
const dbResponse = await openai.beta.vectorStores.retrieve(vectorStoreId)
|
||||
const dbResponse = await openai.vectorStores.retrieve(vectorStoreId)
|
||||
return dbResponse
|
||||
} catch (error) {
|
||||
throw new InternalFlowiseError(
|
||||
@@ -51,7 +51,7 @@ const listAssistantVectorStore = async (credentialId: string) => {
|
||||
}
|
||||
|
||||
const openai = new OpenAI({ apiKey: openAIApiKey })
|
||||
const dbResponse = await openai.beta.vectorStores.list()
|
||||
const dbResponse = await openai.vectorStores.list()
|
||||
return dbResponse.data
|
||||
} catch (error) {
|
||||
throw new InternalFlowiseError(
|
||||
@@ -61,7 +61,7 @@ const listAssistantVectorStore = async (credentialId: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
const createAssistantVectorStore = async (credentialId: string, obj: OpenAI.Beta.VectorStores.VectorStoreCreateParams) => {
|
||||
const createAssistantVectorStore = async (credentialId: string, obj: OpenAI.VectorStores.VectorStoreCreateParams) => {
|
||||
try {
|
||||
const appServer = getRunningExpressApp()
|
||||
const credential = await appServer.AppDataSource.getRepository(Credential).findOneBy({
|
||||
@@ -78,7 +78,7 @@ const createAssistantVectorStore = async (credentialId: string, obj: OpenAI.Beta
|
||||
}
|
||||
|
||||
const openai = new OpenAI({ apiKey: openAIApiKey })
|
||||
const dbResponse = await openai.beta.vectorStores.create(obj)
|
||||
const dbResponse = await openai.vectorStores.create(obj)
|
||||
return dbResponse
|
||||
} catch (error) {
|
||||
throw new InternalFlowiseError(
|
||||
@@ -91,7 +91,7 @@ const createAssistantVectorStore = async (credentialId: string, obj: OpenAI.Beta
|
||||
const updateAssistantVectorStore = async (
|
||||
credentialId: string,
|
||||
vectorStoreId: string,
|
||||
obj: OpenAI.Beta.VectorStores.VectorStoreUpdateParams
|
||||
obj: OpenAI.VectorStores.VectorStoreUpdateParams
|
||||
) => {
|
||||
try {
|
||||
const appServer = getRunningExpressApp()
|
||||
@@ -109,8 +109,8 @@ const updateAssistantVectorStore = async (
|
||||
}
|
||||
|
||||
const openai = new OpenAI({ apiKey: openAIApiKey })
|
||||
const dbResponse = await openai.beta.vectorStores.update(vectorStoreId, obj)
|
||||
const vectorStoreFiles = await openai.beta.vectorStores.files.list(vectorStoreId)
|
||||
const dbResponse = await openai.vectorStores.update(vectorStoreId, obj)
|
||||
const vectorStoreFiles = await openai.vectorStores.files.list(vectorStoreId)
|
||||
if (vectorStoreFiles.data?.length) {
|
||||
const files = []
|
||||
for (const file of vectorStoreFiles.data) {
|
||||
@@ -145,7 +145,7 @@ const deleteAssistantVectorStore = async (credentialId: string, vectorStoreId: s
|
||||
}
|
||||
|
||||
const openai = new OpenAI({ apiKey: openAIApiKey })
|
||||
const dbResponse = await openai.beta.vectorStores.del(vectorStoreId)
|
||||
const dbResponse = await openai.vectorStores.del(vectorStoreId)
|
||||
return dbResponse
|
||||
} catch (error) {
|
||||
throw new InternalFlowiseError(
|
||||
@@ -190,7 +190,7 @@ const uploadFilesToAssistantVectorStore = async (
|
||||
|
||||
const file_ids = [...uploadedFiles.map((file) => file.id)]
|
||||
|
||||
const res = await openai.beta.vectorStores.fileBatches.createAndPoll(vectorStoreId, {
|
||||
const res = await openai.vectorStores.fileBatches.createAndPoll(vectorStoreId, {
|
||||
file_ids
|
||||
})
|
||||
if (res.status === 'completed' && res.file_counts.completed === uploadedFiles.length) return uploadedFiles
|
||||
@@ -232,7 +232,7 @@ const deleteFilesFromAssistantVectorStore = async (credentialId: string, vectorS
|
||||
const deletedFileIds = []
|
||||
let count = 0
|
||||
for (const file of file_ids) {
|
||||
const res = await openai.beta.vectorStores.files.del(vectorStoreId, file)
|
||||
const res = await openai.vectorStores.files.del(vectorStoreId, file)
|
||||
if (res.deleted) {
|
||||
deletedFileIds.push(file)
|
||||
count += 1
|
||||
|
||||
@@ -68,10 +68,10 @@ const getSingleOpenaiAssistant = async (credentialId: string, assistantId: strin
|
||||
if (dbResponse.tool_resources?.file_search?.vector_store_ids?.length) {
|
||||
// Since there can only be 1 vector store per assistant
|
||||
const vectorStoreId = dbResponse.tool_resources.file_search.vector_store_ids[0]
|
||||
const vectorStoreFiles = await openai.beta.vectorStores.files.list(vectorStoreId)
|
||||
const vectorStoreFiles = await openai.vectorStores.files.list(vectorStoreId)
|
||||
const fileIds = vectorStoreFiles.data?.map((file) => file.id) ?? []
|
||||
;(dbResponse.tool_resources.file_search as any).files = [...existingFiles.filter((file) => fileIds.includes(file.id))]
|
||||
;(dbResponse.tool_resources.file_search as any).vector_store_object = await openai.beta.vectorStores.retrieve(vectorStoreId)
|
||||
;(dbResponse.tool_resources.file_search as any).vector_store_object = await openai.vectorStores.retrieve(vectorStoreId)
|
||||
}
|
||||
return dbResponse
|
||||
} catch (error) {
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import { InternalFlowiseError } from '../../errors/internalFlowiseError'
|
||||
import { getErrorMessage } from '../../errors/utils'
|
||||
import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
|
||||
import { ChatFlow } from '../../database/entities/ChatFlow'
|
||||
import { INodeParams } from 'flowise-components'
|
||||
import { IReactFlowEdge, IReactFlowNode } from '../../Interface'
|
||||
|
||||
interface IValidationResult {
|
||||
id: string
|
||||
label: string
|
||||
name: string
|
||||
issues: string[]
|
||||
}
|
||||
|
||||
const checkFlowValidation = async (flowId: string): Promise<IValidationResult[]> => {
|
||||
try {
|
||||
const appServer = getRunningExpressApp()
|
||||
|
||||
const componentNodes = appServer.nodesPool.componentNodes
|
||||
|
||||
const flow = await appServer.AppDataSource.getRepository(ChatFlow).findOne({
|
||||
where: {
|
||||
id: flowId
|
||||
}
|
||||
})
|
||||
|
||||
if (!flow) {
|
||||
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `Error: validationService.checkFlowValidation - flow not found!`)
|
||||
}
|
||||
|
||||
const flowData = JSON.parse(flow.flowData)
|
||||
const nodes = flowData.nodes
|
||||
const edges = flowData.edges
|
||||
|
||||
// Store validation results
|
||||
const validationResults = []
|
||||
|
||||
// Create a map of connected nodes
|
||||
const connectedNodes = new Set<string>()
|
||||
edges.forEach((edge: IReactFlowEdge) => {
|
||||
connectedNodes.add(edge.source)
|
||||
connectedNodes.add(edge.target)
|
||||
})
|
||||
|
||||
// Validate each node
|
||||
for (const node of nodes) {
|
||||
if (node.data.name === 'stickyNoteAgentflow') continue
|
||||
|
||||
const nodeIssues = []
|
||||
|
||||
// Check if node is connected
|
||||
if (!connectedNodes.has(node.id)) {
|
||||
nodeIssues.push('This node is not connected to anything')
|
||||
}
|
||||
|
||||
// Validate input parameters
|
||||
if (node.data && node.data.inputParams && node.data.inputs) {
|
||||
for (const param of node.data.inputParams) {
|
||||
// Skip validation if the parameter has show condition that doesn't match
|
||||
if (param.show) {
|
||||
let shouldShow = true
|
||||
for (const [key, value] of Object.entries(param.show)) {
|
||||
if (node.data.inputs[key] !== value) {
|
||||
shouldShow = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!shouldShow) continue
|
||||
}
|
||||
|
||||
// Skip validation if the parameter has hide condition that matches
|
||||
if (param.hide) {
|
||||
let shouldHide = true
|
||||
for (const [key, value] of Object.entries(param.hide)) {
|
||||
if (node.data.inputs[key] !== value) {
|
||||
shouldHide = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if (shouldHide) continue
|
||||
}
|
||||
|
||||
// Check if required parameter has a value
|
||||
if (!param.optional) {
|
||||
const inputValue = node.data.inputs[param.name]
|
||||
if (inputValue === undefined || inputValue === null || inputValue === '') {
|
||||
nodeIssues.push(`${param.label} is required`)
|
||||
}
|
||||
}
|
||||
|
||||
// Check array type parameters (even if the array itself is optional)
|
||||
if (param.type === 'array' && Array.isArray(node.data.inputs[param.name])) {
|
||||
const inputValue = node.data.inputs[param.name]
|
||||
|
||||
// Only validate non-empty arrays (if array is required but empty, it's caught above)
|
||||
if (inputValue.length > 0) {
|
||||
// Check each item in the array
|
||||
inputValue.forEach((item: Record<string, any>, index: number) => {
|
||||
if (param.array) {
|
||||
param.array.forEach((arrayParam: INodeParams) => {
|
||||
// Evaluate if this parameter should be shown based on current values
|
||||
// First check show conditions
|
||||
let shouldValidate = true
|
||||
|
||||
if (arrayParam.show) {
|
||||
// Default to not showing unless conditions match
|
||||
shouldValidate = false
|
||||
|
||||
// Each key in show is a condition that must be satisfied
|
||||
for (const [conditionKey, expectedValue] of Object.entries(arrayParam.show)) {
|
||||
const isIndexCondition = conditionKey.includes('$index')
|
||||
let actualValue
|
||||
|
||||
if (isIndexCondition) {
|
||||
// Replace $index with actual index and evaluate
|
||||
const normalizedKey = conditionKey.replace(/conditions\[\$index\]\.(\w+)/, '$1')
|
||||
actualValue = item[normalizedKey]
|
||||
} else {
|
||||
// Direct property in the current item
|
||||
actualValue = item[conditionKey]
|
||||
}
|
||||
|
||||
// Check if condition is satisfied
|
||||
let conditionMet = false
|
||||
if (Array.isArray(expectedValue)) {
|
||||
conditionMet = expectedValue.includes(actualValue)
|
||||
} else {
|
||||
conditionMet = actualValue === expectedValue
|
||||
}
|
||||
|
||||
if (conditionMet) {
|
||||
shouldValidate = true
|
||||
break // One matching condition is enough
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then check hide conditions (they override show conditions)
|
||||
if (shouldValidate && arrayParam.hide) {
|
||||
for (const [conditionKey, expectedValue] of Object.entries(arrayParam.hide)) {
|
||||
const isIndexCondition = conditionKey.includes('$index')
|
||||
let actualValue
|
||||
|
||||
if (isIndexCondition) {
|
||||
// Replace $index with actual index and evaluate
|
||||
const normalizedKey = conditionKey.replace(/conditions\[\$index\]\.(\w+)/, '$1')
|
||||
actualValue = item[normalizedKey]
|
||||
} else {
|
||||
// Direct property in the current item
|
||||
actualValue = item[conditionKey]
|
||||
}
|
||||
|
||||
// Check if hide condition is met
|
||||
let shouldHide = false
|
||||
if (Array.isArray(expectedValue)) {
|
||||
shouldHide = expectedValue.includes(actualValue)
|
||||
} else {
|
||||
shouldHide = actualValue === expectedValue
|
||||
}
|
||||
|
||||
if (shouldHide) {
|
||||
shouldValidate = false
|
||||
break // One matching hide condition is enough to hide
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only validate if field should be shown
|
||||
if (shouldValidate) {
|
||||
// Check if value is required and missing
|
||||
if (
|
||||
(arrayParam.optional === undefined || !arrayParam.optional) &&
|
||||
(item[arrayParam.name] === undefined ||
|
||||
item[arrayParam.name] === null ||
|
||||
item[arrayParam.name] === '' ||
|
||||
item[arrayParam.name] === '<p></p>')
|
||||
) {
|
||||
nodeIssues.push(`${param.label} item #${index + 1}: ${arrayParam.label} is required`)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Check for credential requirements
|
||||
if (param.name === 'credential' && !param.optional) {
|
||||
const credentialValue = node.data.inputs[param.name]
|
||||
if (!credentialValue) {
|
||||
nodeIssues.push(`Credential is required`)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for nested config parameters
|
||||
const configKey = `${param.name}Config`
|
||||
if (node.data.inputs[configKey] && node.data.inputs[param.name]) {
|
||||
const componentName = node.data.inputs[param.name]
|
||||
const configValue = node.data.inputs[configKey]
|
||||
|
||||
// Check if the component exists in the componentNodes pool
|
||||
if (componentNodes[componentName] && componentNodes[componentName].inputs) {
|
||||
const componentInputParams = componentNodes[componentName].inputs
|
||||
|
||||
// Validate each required input parameter in the component
|
||||
for (const componentParam of componentInputParams) {
|
||||
// Skip validation if the parameter has show condition that doesn't match
|
||||
if (componentParam.show) {
|
||||
let shouldShow = true
|
||||
for (const [key, value] of Object.entries(componentParam.show)) {
|
||||
if (configValue[key] !== value) {
|
||||
shouldShow = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!shouldShow) continue
|
||||
}
|
||||
|
||||
// Skip validation if the parameter has hide condition that matches
|
||||
if (componentParam.hide) {
|
||||
let shouldHide = true
|
||||
for (const [key, value] of Object.entries(componentParam.hide)) {
|
||||
if (configValue[key] !== value) {
|
||||
shouldHide = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if (shouldHide) continue
|
||||
}
|
||||
|
||||
if (!componentParam.optional) {
|
||||
const nestedValue = configValue[componentParam.name]
|
||||
if (nestedValue === undefined || nestedValue === null || nestedValue === '') {
|
||||
nodeIssues.push(`${param.label} configuration: ${componentParam.label} is required`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for credential requirement in the component
|
||||
if (componentNodes[componentName].credential && !componentNodes[componentName].credential.optional) {
|
||||
if (!configValue.FLOWISE_CREDENTIAL_ID && !configValue.credential) {
|
||||
nodeIssues.push(`${param.label} requires a credential`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add node to validation results if it has issues
|
||||
if (nodeIssues.length > 0) {
|
||||
validationResults.push({
|
||||
id: node.id,
|
||||
label: node.data.label,
|
||||
name: node.data.name,
|
||||
issues: nodeIssues
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Check for hanging edges
|
||||
for (const edge of edges) {
|
||||
const sourceExists = nodes.some((node: IReactFlowNode) => node.id === edge.source)
|
||||
const targetExists = nodes.some((node: IReactFlowEdge) => node.id === edge.target)
|
||||
|
||||
if (!sourceExists || !targetExists) {
|
||||
// Find the existing node that is connected to this hanging edge
|
||||
if (!sourceExists && targetExists) {
|
||||
// Target exists but source doesn't - add issue to target node
|
||||
const targetNode = nodes.find((node: IReactFlowNode) => node.id === edge.target)
|
||||
const targetNodeResult = validationResults.find((result) => result.id === edge.target)
|
||||
|
||||
if (targetNodeResult) {
|
||||
// Add to existing validation result
|
||||
targetNodeResult.issues.push(`Connected to non-existent source node ${edge.source}`)
|
||||
} else {
|
||||
// Create new validation result for this node
|
||||
validationResults.push({
|
||||
id: targetNode.id,
|
||||
label: targetNode.data.label,
|
||||
name: targetNode.data.name,
|
||||
issues: [`Connected to non-existent source node ${edge.source}`]
|
||||
})
|
||||
}
|
||||
} else if (sourceExists && !targetExists) {
|
||||
// Source exists but target doesn't - add issue to source node
|
||||
const sourceNode = nodes.find((node: IReactFlowNode) => node.id === edge.source)
|
||||
const sourceNodeResult = validationResults.find((result) => result.id === edge.source)
|
||||
|
||||
if (sourceNodeResult) {
|
||||
// Add to existing validation result
|
||||
sourceNodeResult.issues.push(`Connected to non-existent target node ${edge.target}`)
|
||||
} else {
|
||||
// Create new validation result for this node
|
||||
validationResults.push({
|
||||
id: sourceNode.id,
|
||||
label: sourceNode.data.label,
|
||||
name: sourceNode.data.name,
|
||||
issues: [`Connected to non-existent target node ${edge.target}`]
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Both source and target don't exist - create a generic edge issue
|
||||
validationResults.push({
|
||||
id: edge.id,
|
||||
label: `Edge ${edge.id}`,
|
||||
name: 'edge',
|
||||
issues: ['Disconnected edge - both source and target nodes do not exist']
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return validationResults
|
||||
} catch (error) {
|
||||
throw new InternalFlowiseError(
|
||||
StatusCodes.INTERNAL_SERVER_ERROR,
|
||||
`Error: validationService.checkFlowValidation - ${getErrorMessage(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
checkFlowValidation
|
||||
}
|
||||
@@ -99,6 +99,16 @@ export class SSEStreamer implements IServerSideEventStreamer {
|
||||
client.response.write('message:\ndata:' + JSON.stringify(clientResponse) + '\n\n')
|
||||
}
|
||||
}
|
||||
streamCalledToolsEvent(chatId: string, data: any): void {
|
||||
const client = this.clients[chatId]
|
||||
if (client) {
|
||||
const clientResponse = {
|
||||
event: 'calledTools',
|
||||
data: data
|
||||
}
|
||||
client.response.write('message:\ndata:' + JSON.stringify(clientResponse) + '\n\n')
|
||||
}
|
||||
}
|
||||
streamFileAnnotationsEvent(chatId: string, data: any): void {
|
||||
const client = this.clients[chatId]
|
||||
if (client) {
|
||||
@@ -139,6 +149,36 @@ export class SSEStreamer implements IServerSideEventStreamer {
|
||||
client.response.write('message:\ndata:' + JSON.stringify(clientResponse) + '\n\n')
|
||||
}
|
||||
}
|
||||
streamAgentFlowEvent(chatId: string, data: any): void {
|
||||
const client = this.clients[chatId]
|
||||
if (client) {
|
||||
const clientResponse = {
|
||||
event: 'agentFlowEvent',
|
||||
data: data
|
||||
}
|
||||
client.response.write('message:\ndata:' + JSON.stringify(clientResponse) + '\n\n')
|
||||
}
|
||||
}
|
||||
streamAgentFlowExecutedDataEvent(chatId: string, data: any): void {
|
||||
const client = this.clients[chatId]
|
||||
if (client) {
|
||||
const clientResponse = {
|
||||
event: 'agentFlowExecutedData',
|
||||
data: data
|
||||
}
|
||||
client.response.write('message:\ndata:' + JSON.stringify(clientResponse) + '\n\n')
|
||||
}
|
||||
}
|
||||
streamNextAgentFlowEvent(chatId: string, data: any): void {
|
||||
const client = this.clients[chatId]
|
||||
if (client) {
|
||||
const clientResponse = {
|
||||
event: 'nextAgentFlow',
|
||||
data: data
|
||||
}
|
||||
client.response.write('message:\ndata:' + JSON.stringify(clientResponse) + '\n\n')
|
||||
}
|
||||
}
|
||||
streamActionEvent(chatId: string, data: any): void {
|
||||
const client = this.clients[chatId]
|
||||
if (client) {
|
||||
@@ -206,4 +246,15 @@ export class SSEStreamer implements IServerSideEventStreamer {
|
||||
this.streamCustomEvent(chatId, 'metadata', metadataJson)
|
||||
}
|
||||
}
|
||||
|
||||
streamUsageMetadataEvent(chatId: string, data: any): void {
|
||||
const client = this.clients[chatId]
|
||||
if (client) {
|
||||
const clientResponse = {
|
||||
event: 'usageMetadata',
|
||||
data: data
|
||||
}
|
||||
client.response.write('message:\ndata:' + JSON.stringify(clientResponse) + '\n\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -63,6 +63,7 @@ import { buildAgentGraph } from './buildAgentGraph'
|
||||
import { getErrorMessage } from '../errors/utils'
|
||||
import { FLOWISE_METRIC_COUNTERS, FLOWISE_COUNTER_STATUS, IMetricsProvider } from '../Interface.Metrics'
|
||||
import { OMIT_QUEUE_JOB_DATA } from './constants'
|
||||
import { executeAgentFlow } from './buildAgentflow'
|
||||
|
||||
/*
|
||||
* Initialize the ending node to be executed
|
||||
@@ -236,7 +237,8 @@ export const executeFlow = async ({
|
||||
baseURL,
|
||||
isInternal,
|
||||
files,
|
||||
signal
|
||||
signal,
|
||||
isTool
|
||||
}: IExecuteFlowParams) => {
|
||||
// Ensure incomingInput has all required properties with default values
|
||||
incomingInput = {
|
||||
@@ -260,8 +262,8 @@ export const executeFlow = async ({
|
||||
*/
|
||||
let fileUploads: IFileUpload[] = []
|
||||
let uploadedFilesContent = ''
|
||||
if (incomingInput.uploads) {
|
||||
fileUploads = incomingInput.uploads
|
||||
if (uploads) {
|
||||
fileUploads = uploads
|
||||
for (let i = 0; i < fileUploads.length; i += 1) {
|
||||
const upload = fileUploads[i]
|
||||
|
||||
@@ -373,6 +375,26 @@ export const executeFlow = async ({
|
||||
}
|
||||
}
|
||||
|
||||
const isAgentFlowV2 = chatflow.type === 'AGENTFLOW'
|
||||
if (isAgentFlowV2) {
|
||||
return executeAgentFlow({
|
||||
componentNodes,
|
||||
incomingInput,
|
||||
chatflow,
|
||||
chatId,
|
||||
appDataSource,
|
||||
telemetry,
|
||||
cachePool,
|
||||
sseStreamer,
|
||||
baseURL,
|
||||
isInternal,
|
||||
uploadedFilesContent,
|
||||
fileUploads,
|
||||
signal,
|
||||
isTool
|
||||
})
|
||||
}
|
||||
|
||||
/*** Get chatflows and prepare data ***/
|
||||
const flowData = chatflow.flowData
|
||||
const parsedFlowData: IReactFlowObject = JSON.parse(flowData)
|
||||
@@ -498,7 +520,7 @@ export const executeFlow = async ({
|
||||
memoryType,
|
||||
sessionId,
|
||||
createdDate: userMessageDateTime,
|
||||
fileUploads: incomingInput.uploads ? JSON.stringify(fileUploads) : undefined,
|
||||
fileUploads: uploads ? JSON.stringify(fileUploads) : undefined,
|
||||
leadEmail: incomingInput.leadEmail
|
||||
}
|
||||
await utilAddChatMessage(userMessage, appDataSource)
|
||||
@@ -819,12 +841,14 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals
|
||||
}
|
||||
|
||||
const isAgentFlow = chatflow.type === 'MULTIAGENT'
|
||||
|
||||
const httpProtocol = req.get('x-forwarded-proto') || req.protocol
|
||||
const baseURL = `${httpProtocol}://${req.get('host')}`
|
||||
const incomingInput: IncomingInput = req.body || {} // Ensure incomingInput is never undefined
|
||||
const chatId = incomingInput.chatId ?? incomingInput.overrideConfig?.sessionId ?? uuidv4()
|
||||
const files = (req.files as Express.Multer.File[]) || []
|
||||
const abortControllerId = `${chatflow.id}_${chatId}`
|
||||
const isTool = req.get('flowise-tool') === 'true'
|
||||
|
||||
try {
|
||||
// Validate API Key if its external API request
|
||||
@@ -846,7 +870,8 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals
|
||||
sseStreamer: appServer.sseStreamer,
|
||||
telemetry: appServer.telemetry,
|
||||
cachePool: appServer.cachePool,
|
||||
componentNodes: appServer.nodesPool.componentNodes
|
||||
componentNodes: appServer.nodesPool.componentNodes,
|
||||
isTool // used to disable streaming if incoming request its from ChatflowTool
|
||||
}
|
||||
|
||||
if (process.env.MODE === MODE.QUEUE) {
|
||||
@@ -868,7 +893,6 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals
|
||||
const signal = new AbortController()
|
||||
appServer.abortControllerPool.add(abortControllerId, signal)
|
||||
executeData.signal = signal
|
||||
|
||||
const result = await executeFlow(executeData)
|
||||
|
||||
appServer.abortControllerPool.remove(abortControllerId)
|
||||
|
||||
@@ -3,6 +3,7 @@ export const WHITELIST_URLS = [
|
||||
'/api/v1/chatflows/apikey/',
|
||||
'/api/v1/public-chatflows',
|
||||
'/api/v1/public-chatbotConfig',
|
||||
'/api/v1/public-executions',
|
||||
'/api/v1/prediction/',
|
||||
'/api/v1/vector/upsert/',
|
||||
'/api/v1/node-icon/',
|
||||
@@ -28,6 +29,7 @@ export const INPUT_PARAMS_TYPE = [
|
||||
'asyncMultiOptions',
|
||||
'options',
|
||||
'multiOptions',
|
||||
'array',
|
||||
'datagrid',
|
||||
'string',
|
||||
'number',
|
||||
|
||||
@@ -52,6 +52,7 @@ export const utilGetChatMessage = async ({
|
||||
|
||||
// do the join with chat message feedback based on messageId for each chat message in the chatflow
|
||||
query
|
||||
.leftJoinAndSelect('chat_message.execution', 'execution')
|
||||
.leftJoinAndMapOne('chat_message.feedback', ChatMessageFeedback, 'feedback', 'feedback.messageId = chat_message.id')
|
||||
.where('chat_message.chatflowid = :chatflowid', { chatflowid })
|
||||
|
||||
@@ -121,6 +122,9 @@ export const utilGetChatMessage = async ({
|
||||
createdDate: createdDateQuery,
|
||||
id: messageId ?? undefined
|
||||
},
|
||||
relations: {
|
||||
execution: true
|
||||
},
|
||||
order: {
|
||||
createdDate: sortOrder === 'DESC' ? 'DESC' : 'ASC'
|
||||
}
|
||||
|
||||
@@ -93,22 +93,43 @@ export const utilGetUploadsConfig = async (chatflowid: string): Promise<IUploadC
|
||||
'seqStart'
|
||||
]
|
||||
|
||||
if (nodes.some((node) => imgUploadAllowedNodes.includes(node.data.name))) {
|
||||
nodes.forEach((node: IReactFlowNode) => {
|
||||
const data = node.data
|
||||
if (data.category === 'Chat Models' && data.inputs?.['allowImageUploads'] === true) {
|
||||
// TODO: for now the maxUploadSize is hardcoded to 5MB, we need to add it to the node properties
|
||||
node.data.inputParams.map((param: INodeParams) => {
|
||||
if (param.name === 'allowImageUploads' && node.data.inputs?.['allowImageUploads']) {
|
||||
imgUploadSizeAndTypes.push({
|
||||
fileTypes: 'image/gif;image/jpeg;image/png;image/webp;'.split(';'),
|
||||
maxUploadSize: 5
|
||||
})
|
||||
isImageUploadAllowed = true
|
||||
}
|
||||
})
|
||||
const isAgentflow = nodes.some((node) => node.data.category === 'Agent Flows')
|
||||
|
||||
if (isAgentflow) {
|
||||
// check through all the nodes and check if any of the nodes data inputs agentModelConfig or llmModelConfig or conditionAgentModelConfig has allowImageUploads
|
||||
nodes.forEach((node) => {
|
||||
if (node.data.category === 'Agent Flows') {
|
||||
if (
|
||||
node.data.inputs?.agentModelConfig?.allowImageUploads ||
|
||||
node.data.inputs?.llmModelConfig?.allowImageUploads ||
|
||||
node.data.inputs?.conditionAgentModelConfig?.allowImageUploads
|
||||
) {
|
||||
imgUploadSizeAndTypes.push({
|
||||
fileTypes: 'image/gif;image/jpeg;image/png;image/webp;'.split(';'),
|
||||
maxUploadSize: 5
|
||||
})
|
||||
isImageUploadAllowed = true
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
if (nodes.some((node) => imgUploadAllowedNodes.includes(node.data.name))) {
|
||||
nodes.forEach((node: IReactFlowNode) => {
|
||||
const data = node.data
|
||||
if (data.category === 'Chat Models' && data.inputs?.['allowImageUploads'] === true) {
|
||||
// TODO: for now the maxUploadSize is hardcoded to 5MB, we need to add it to the node properties
|
||||
node.data.inputParams.map((param: INodeParams) => {
|
||||
if (param.name === 'allowImageUploads' && node.data.inputs?.['allowImageUploads']) {
|
||||
imgUploadSizeAndTypes.push({
|
||||
fileTypes: 'image/gif;image/jpeg;image/png;image/webp;'.split(';'),
|
||||
maxUploadSize: 5
|
||||
})
|
||||
isImageUploadAllowed = true
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -64,10 +64,11 @@ import {
|
||||
SecretsManagerClientConfig
|
||||
} from '@aws-sdk/client-secrets-manager'
|
||||
|
||||
const QUESTION_VAR_PREFIX = 'question'
|
||||
const FILE_ATTACHMENT_PREFIX = 'file_attachment'
|
||||
const CHAT_HISTORY_VAR_PREFIX = 'chat_history'
|
||||
const REDACTED_CREDENTIAL_VALUE = '_FLOWISE_BLANK_07167752-1a71-43b1-bf8f-4f32252165db'
|
||||
export const QUESTION_VAR_PREFIX = 'question'
|
||||
export const FILE_ATTACHMENT_PREFIX = 'file_attachment'
|
||||
export const CHAT_HISTORY_VAR_PREFIX = 'chat_history'
|
||||
export const RUNTIME_MESSAGES_LENGTH_VAR_PREFIX = 'runtime_messages_length'
|
||||
export const REDACTED_CREDENTIAL_VALUE = '_FLOWISE_BLANK_07167752-1a71-43b1-bf8f-4f32252165db'
|
||||
|
||||
let secretsManagerClient: SecretsManagerClient | null = null
|
||||
const USE_AWS_SECRETS_MANAGER = process.env.SECRETKEY_STORAGE_TYPE === 'aws'
|
||||
@@ -238,6 +239,22 @@ export const getStartingNodes = (graph: INodeDirectedGraph, endNodeId: string) =
|
||||
return { startingNodeIds, depthQueue: depthQueueReversed }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get starting node and check if flow is valid
|
||||
* @param {INodeDependencies} nodeDependencies
|
||||
*/
|
||||
export const getStartingNode = (nodeDependencies: INodeDependencies) => {
|
||||
// Find starting node
|
||||
const startingNodeIds = [] as string[]
|
||||
Object.keys(nodeDependencies).forEach((nodeId) => {
|
||||
if (nodeDependencies[nodeId] === 0) {
|
||||
startingNodeIds.push(nodeId)
|
||||
}
|
||||
})
|
||||
|
||||
return { startingNodeIds }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all connected nodes from startnode
|
||||
* @param {INodeDependencies} graph
|
||||
@@ -763,7 +780,7 @@ export const clearSessionMemory = async (
|
||||
}
|
||||
}
|
||||
|
||||
const getGlobalVariable = async (
|
||||
export const getGlobalVariable = async (
|
||||
overrideConfig?: ICommonObject,
|
||||
availableVariables: IVariable[] = [],
|
||||
variableOverrides: ICommonObject[] = []
|
||||
@@ -990,7 +1007,6 @@ export const resolveVariables = async (
|
||||
variableOverrides: ICommonObject[] = []
|
||||
): Promise<INodeData> => {
|
||||
let flowNodeData = cloneDeep(reactFlowNodeData)
|
||||
const types = 'inputs'
|
||||
|
||||
const getParamValues = async (paramsObj: ICommonObject) => {
|
||||
for (const key in paramsObj) {
|
||||
@@ -1030,7 +1046,7 @@ export const resolveVariables = async (
|
||||
}
|
||||
}
|
||||
|
||||
const paramsObj = flowNodeData[types] ?? {}
|
||||
const paramsObj = flowNodeData['inputs'] ?? {}
|
||||
await getParamValues(paramsObj)
|
||||
|
||||
return flowNodeData
|
||||
@@ -1244,7 +1260,8 @@ export const findAvailableConfigs = (reactFlowNodes: IReactFlowNode[], component
|
||||
|
||||
for (const flowNode of reactFlowNodes) {
|
||||
for (const inputParam of flowNode.data.inputParams) {
|
||||
let obj: IOverrideConfig
|
||||
let obj: IOverrideConfig | undefined
|
||||
|
||||
if (inputParam.type === 'file') {
|
||||
obj = {
|
||||
node: flowNode.data.label,
|
||||
@@ -1285,6 +1302,34 @@ export const findAvailableConfigs = (reactFlowNodes: IReactFlowNode[], component
|
||||
}
|
||||
}
|
||||
continue
|
||||
} else if (inputParam.type === 'array') {
|
||||
// get array item schema
|
||||
const arrayItem = inputParam.array
|
||||
if (Array.isArray(arrayItem)) {
|
||||
const arraySchema = []
|
||||
// Each array item is a field definition
|
||||
for (const item of arrayItem) {
|
||||
let itemType = item.type
|
||||
if (itemType === 'options') {
|
||||
const availableOptions = item.options?.map((option) => option.name).join(', ')
|
||||
itemType = `(${availableOptions})`
|
||||
} else if (itemType === 'file') {
|
||||
itemType = item.fileType ?? item.type
|
||||
}
|
||||
arraySchema.push({
|
||||
name: item.name,
|
||||
type: itemType
|
||||
})
|
||||
}
|
||||
obj = {
|
||||
node: flowNode.data.label,
|
||||
nodeId: flowNode.data.id,
|
||||
label: inputParam.label,
|
||||
name: inputParam.name,
|
||||
type: inputParam.type,
|
||||
schema: arraySchema
|
||||
}
|
||||
}
|
||||
} else {
|
||||
obj = {
|
||||
node: flowNode.data.label,
|
||||
@@ -1294,7 +1339,7 @@ export const findAvailableConfigs = (reactFlowNodes: IReactFlowNode[], component
|
||||
type: inputParam.type === 'password' ? 'string' : inputParam.type
|
||||
}
|
||||
}
|
||||
if (!configs.some((config) => JSON.stringify(config) === JSON.stringify(obj))) {
|
||||
if (obj && !configs.some((config) => JSON.stringify(config) === JSON.stringify(obj))) {
|
||||
configs.push(obj)
|
||||
}
|
||||
}
|
||||
@@ -1814,3 +1859,70 @@ export const getMulterStorage = () => {
|
||||
return multer({ dest: getUploadPath() })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate depth of each node from starting nodes
|
||||
* @param {INodeDirectedGraph} graph
|
||||
* @param {string[]} startingNodeIds
|
||||
* @returns {Record<string, number>} Map of nodeId to its depth
|
||||
*/
|
||||
export const calculateNodesDepth = (graph: INodeDirectedGraph, startingNodeIds: string[]): Record<string, number> => {
|
||||
const depths: Record<string, number> = {}
|
||||
const visited = new Set<string>()
|
||||
|
||||
// Initialize all nodes with depth -1 (unvisited)
|
||||
for (const nodeId in graph) {
|
||||
depths[nodeId] = -1
|
||||
}
|
||||
|
||||
// BFS queue with [nodeId, depth]
|
||||
const queue: [string, number][] = startingNodeIds.map((id) => [id, 0])
|
||||
|
||||
// Set starting nodes depth to 0
|
||||
startingNodeIds.forEach((id) => {
|
||||
depths[id] = 0
|
||||
})
|
||||
|
||||
while (queue.length > 0) {
|
||||
const [currentNode, currentDepth] = queue.shift()!
|
||||
|
||||
if (visited.has(currentNode)) continue
|
||||
visited.add(currentNode)
|
||||
|
||||
// Process all neighbors
|
||||
for (const neighbor of graph[currentNode]) {
|
||||
if (!visited.has(neighbor)) {
|
||||
// Update depth if unvisited or found shorter path
|
||||
if (depths[neighbor] === -1 || depths[neighbor] > currentDepth + 1) {
|
||||
depths[neighbor] = currentDepth + 1
|
||||
}
|
||||
queue.push([neighbor, currentDepth + 1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return depths
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get all nodes in a path starting from a node
|
||||
* @param {INodeDirectedGraph} graph
|
||||
* @param {string} startNode
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export const getAllNodesInPath = (startNode: string, graph: INodeDirectedGraph): string[] => {
|
||||
const nodes = new Set<string>()
|
||||
const queue = [startNode]
|
||||
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift()!
|
||||
if (nodes.has(current)) continue
|
||||
|
||||
nodes.add(current)
|
||||
if (graph[current]) {
|
||||
queue.push(...graph[current])
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(nodes)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user