mirror of
https://github.com/run-llama/flow-maker.git
synced 2026-06-30 21:17:56 -04:00
Refactored decision node
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
# Llama Agent Creator Architecture
|
||||
|
||||
This document outlines the core architecture of the Llama Agent Creator application, focusing on how the visual workflow graph is translated into executable logic.
|
||||
|
||||
## Core Components and Data Flow
|
||||
|
||||
The application has a clear separation between the visual graph representation, an intermediate JSON format, and two distinct execution engines.
|
||||
|
||||
`Visual Graph (React Flow)` --> `workflow-compiler.ts` --> `WorkflowJson (Intermediate State)` --> `[RunView.tsx OR typescript-compiler.ts]`
|
||||
|
||||
1. **Visual Graph Editor (`src/components/AgentFlow.tsx`)**:
|
||||
* This is where the user builds the agent workflow using a drag-and-drop interface powered by `@xyflow/react`.
|
||||
* The state of the graph (nodes and edges) is saved to `localStorage`.
|
||||
|
||||
2. **Workflow Compiler (`src/lib/workflow-compiler.ts`)**:
|
||||
* This is a critical module that acts as a bridge between the UI and the execution logic.
|
||||
* It takes the raw nodes and edges from the React Flow state.
|
||||
* It processes this graph structure and compiles it into a standardized intermediate representation, `WorkflowJson`. This JSON object describes the nodes, their connections (via an event-based system), and their configurations.
|
||||
|
||||
3. **Execution Engines**:
|
||||
The `WorkflowJson` can be consumed by two different parts of the application to execute the workflow:
|
||||
|
||||
* **Interactive Runner (`src/components/RunView.tsx`)**:
|
||||
* Provides an in-browser, step-by-step execution of the workflow.
|
||||
* It loads the graph from `localStorage`, uses `workflow-compiler.ts` to get the `WorkflowJson`, and then walks through the nodes.
|
||||
* It implements the logic for each node type (e.g., 'promptLLM', 'userInput', 'decision') directly, making API calls to the backend (`/api/...`) as needed.
|
||||
* It visually highlights the currently active node on the graph and provides a chat interface for user interaction.
|
||||
* This provides a way to debug and test the workflow interactively.
|
||||
|
||||
* **TypeScript Code Generator (`src/lib/typescript-compiler.ts`)**:
|
||||
* Takes the same `WorkflowJson` and generates a standalone, executable TypeScript file.
|
||||
* This generated script uses the `@llamaindex/workflow-core` library to define the workflow programmatically.
|
||||
* The logic within this compiler *mirrors* the logic in `RunView.tsx`, but instead of executing nodes directly, it generates code that will execute them in a Node.js environment. For example, it generates LLM calls, tool definitions, and the event handling logic that connects the nodes.
|
||||
* This allows the user to export their visually-designed agent as a portable script.
|
||||
|
||||
## Key Takeaway
|
||||
|
||||
When modifying or debugging, it's essential to understand this dual-execution model. Changes to the logic of a node type (e.g., how a 'decision' node works) often need to be implemented in **both** `RunView.tsx` (for interactive execution) and `typescript-compiler.ts` (for code generation) to ensure consistent behavior across both modes. The `workflow-compiler.ts` is central to ensuring both execution engines receive the same structured information from the UI.
|
||||
+38
-43
@@ -1,43 +1,38 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
# Llama Agent Creator Architecture
|
||||
|
||||
This document outlines the core architecture of the Llama Agent Creator application, focusing on how the visual workflow graph is translated into executable logic.
|
||||
|
||||
## Core Components and Data Flow
|
||||
|
||||
The application has a clear separation between the visual graph representation, an intermediate JSON format, and two distinct execution engines.
|
||||
|
||||
`Visual Graph (React Flow)` --> `workflow-compiler.ts` --> `WorkflowJson (Intermediate State)` --> `[RunView.tsx OR typescript-compiler.ts]`
|
||||
|
||||
1. **Visual Graph Editor (`src/components/AgentFlow.tsx`)**:
|
||||
* This is where the user builds the agent workflow using a drag-and-drop interface powered by `@xyflow/react`.
|
||||
* The state of the graph (nodes and edges) is saved to `localStorage`.
|
||||
|
||||
2. **Workflow Compiler (`src/lib/workflow-compiler.ts`)**:
|
||||
* This is a critical module that acts as a bridge between the UI and the execution logic.
|
||||
* It takes the raw nodes and edges from the React Flow state.
|
||||
* It processes this graph structure and compiles it into a standardized intermediate representation, `WorkflowJson`. This JSON object describes the nodes, their connections (via an event-based system), and their configurations.
|
||||
|
||||
3. **Execution Engines**:
|
||||
The `WorkflowJson` can be consumed by two different parts of the application to execute the workflow:
|
||||
|
||||
* **Interactive Runner (`src/components/RunView.tsx`)**:
|
||||
* Provides an in-browser, step-by-step execution of the workflow.
|
||||
* It loads the graph from `localStorage`, uses `workflow-compiler.ts` to get the `WorkflowJson`, and then walks through the nodes.
|
||||
* It implements the logic for each node type (e.g., 'promptLLM', 'userInput', 'decision') directly, making API calls to the backend (`/api/...`) as needed.
|
||||
* It visually highlights the currently active node on the graph and provides a chat interface for user interaction.
|
||||
* This provides a way to debug and test the workflow interactively.
|
||||
|
||||
* **TypeScript Code Generator (`src/lib/typescript-compiler.ts`)**:
|
||||
* Takes the same `WorkflowJson` and generates a standalone, executable TypeScript file.
|
||||
* This generated script uses the `@llamaindex/workflow-core` library to define the workflow programmatically.
|
||||
* The logic within this compiler *mirrors* the logic in `RunView.tsx`, but instead of executing nodes directly, it generates code that will execute them in a Node.js environment. For example, it generates LLM calls, tool definitions, and the event handling logic that connects the nodes.
|
||||
* This allows the user to export their visually-designed agent as a portable script.
|
||||
|
||||
## Key Takeaway
|
||||
|
||||
When modifying or debugging, it's essential to understand this dual-execution model. Changes to the logic of a node type (e.g., how a 'decision' node works) often need to be implemented in **both** `RunView.tsx` (for interactive execution) and `typescript-compiler.ts` (for code generation) to ensure consistent behavior across both modes. The `workflow-compiler.ts` is central to ensuring both execution engines receive the same structured information from the UI.
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
# Llama Agent Creator: Technical Details Supplement
|
||||
|
||||
This document supplements the main `architecture.mdc` file by providing specific details about the technologies and implementation of the Llama Agent Creator.
|
||||
|
||||
## Application Stack
|
||||
|
||||
- **Framework**: [Next.js](mdc:https:/nextjs.org)
|
||||
- **Language**: [TypeScript](mdc:https:/www.typescriptlang.org)
|
||||
- **UI Library**: [React](mdc:https:/react.dev) with [shadcn/ui](mdc:https:/ui.shadcn.com) components.
|
||||
- **Graph Visualization**: [`@xyflow/react`](mdc:https:/reactflow.dev) is used to build the interactive workflow editor in `AgentFlow.tsx`.
|
||||
- **State & Storage**:
|
||||
- The visual graph's state (nodes and edges) is persisted to `localStorage`.
|
||||
- Global chat state is managed via React Context in `src/components/ChatProvider.tsx`.
|
||||
- **Core Workflow Engine**: [`@llamaindex/workflow-core`](mdc:https:/www.npmjs.com/package/@llamaindex/workflow-core) provides the foundational classes and logic for creating and running workflows in the generated TypeScript code.
|
||||
- **LLM Integrations**: The application uses `@llamaindex/openai`, `@llamaindex/anthropic`, and `@llamaindex/google` to connect to various language models.
|
||||
- **Testing**: The project is set up with [Vitest](mdc:https:/vitest.dev) for testing.
|
||||
|
||||
## File-Specific Details
|
||||
|
||||
This section clarifies the roles of key files and directories not fully detailed in the high-level architecture diagram.
|
||||
|
||||
- `app/api/`: This directory contains the backend serverless functions that the interactive runner (`RunView.tsx`) calls. For instance, when a `promptLLM` node is executed in the UI, it makes a POST request to `app/api/llm/call/route.ts` to get the model's response.
|
||||
|
||||
- `src/components/nodes/`: This directory contains the React components for each type of node that can be dragged onto the canvas (e.g., `PromptLLMNode.tsx`, `DecisionNode.tsx`, `UserInputNode.tsx`). These components define the appearance and configuration options of each node in the editor.
|
||||
|
||||
- `src/components/CompileView.tsx`: This component provides the user interface for triggering the TypeScript compilation and viewing/downloading the generated code.
|
||||
|
||||
- `src/lib/llm-utils.ts`: Contains utility functions for configuring and preparing the LLM providers, abstracting some of the setup required for making LLM calls.
|
||||
|
||||
- `src/lib/initial-graph.ts`: Defines the default set of nodes and edges that are loaded when a user first visits the application or clears the canvas.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
# Understanding @llamaindex/workflow-core (TypeScript)
|
||||
|
||||
This document explains the core concepts of the `@llamaindex/workflow-core` library for TypeScript, which is used by `typescript-compiler.ts` to generate executable agent scripts. The library uses a functional, event-driven paradigm.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
The workflow is built by defining events and then creating handlers that listen for those events. The execution flow is determined by which events are emitted from handlers.
|
||||
|
||||
1. **Event-Driven Flow**: The entire workflow is orchestrated by events. A handler function executes when it receives an event of a type it's registered to handle. After executing its logic, the handler can emit a new event, which in turn triggers another handler.
|
||||
|
||||
2. **Functional Approach**: Instead of class-based inheritance (like in Python), the TypeScript version uses factory functions to build the workflow.
|
||||
- `workflowEvent<T>()`: Creates a new type of event.
|
||||
- `createWorkflow()`: Creates a new workflow instance.
|
||||
- `workflow.handle([...events], handlerFn)`: Registers a function `handlerFn` to be called when any of the specified `events` are detected.
|
||||
|
||||
3. **Asynchronous by Design**: All handlers can be `async` and are expected to return `Promise`s, making them suitable for I/O-bound tasks like LLM calls and tool interactions.
|
||||
|
||||
## Key Components
|
||||
|
||||
### 1. `workflowEvent`
|
||||
This function is used to define the different types of events that will flow through your system. They are the data carriers.
|
||||
|
||||
```typescript
|
||||
import { workflowEvent } from "@llamaindex/workflow-core";
|
||||
|
||||
// Define an event that carries a string payload
|
||||
const startEvent = workflowEvent<string>();
|
||||
|
||||
// Define an event that carries a structured object
|
||||
interface ToolCall {
|
||||
id: string;
|
||||
name: string;
|
||||
args: any;
|
||||
}
|
||||
const toolCallEvent = workflowEvent<ToolCall>();
|
||||
```
|
||||
|
||||
### 2. `createWorkflow` and `.handle`
|
||||
`createWorkflow()` creates the main workflow object. The `.handle()` method is then used to register listeners for specific events. The return value of a handler function becomes a new event that is dispatched into the workflow.
|
||||
|
||||
```typescript
|
||||
import { createWorkflow, workflowEvent } from "@llamaindex/workflow-core";
|
||||
|
||||
const workflow = createWorkflow();
|
||||
const startEvent = workflowEvent<string>();
|
||||
const stopEvent = workflowEvent<string>();
|
||||
|
||||
// Register a handler for 'startEvent'
|
||||
workflow.handle([startEvent], async (event) => {
|
||||
console.log(`Received start event with: ${event.data}`);
|
||||
// The return value is automatically dispatched as a new event
|
||||
return stopEvent("Workflow finished!");
|
||||
});
|
||||
|
||||
// Register a handler for 'stopEvent'
|
||||
workflow.handle([stopEvent], (event) => {
|
||||
console.log(event.data); // "Workflow finished!"
|
||||
});
|
||||
```
|
||||
|
||||
### 3. `Context`
|
||||
For more complex workflows, you can access the workflow's context within a handler using `getContext()`. This provides more advanced capabilities.
|
||||
|
||||
```typescript
|
||||
import { getContext } from "@llamaindex/workflow-core";
|
||||
|
||||
workflow.handle([someEvent], (event) => {
|
||||
const { sendEvent, stream, signal } = getContext();
|
||||
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
- **`sendEvent(event)`**: Manually dispatches an event. This is the key to creating "fan-out" patterns where one handler can trigger multiple downstream handlers in parallel.
|
||||
- **`stream`**: An async iterable representing the stream of all events in the workflow. It has powerful utility methods like `.filter()`, `.until()`, and `.toArray()` to manage complex flows, like waiting for the results of parallel tasks.
|
||||
- **`signal`**: An `AbortSignal` that can be used to manage cancellation of long-running async tasks if the workflow is aborted.
|
||||
|
||||
## Control Flow Patterns
|
||||
|
||||
The event-driven architecture enables flexible control flow.
|
||||
|
||||
- **Branching**: A handler can implement conditional logic and return different event types. The workflow will then proceed down the path corresponding to the event that was returned.
|
||||
- **Parallel Execution (Fan-out)**: A handler can call `sendEvent()` multiple times to emit several events simultaneously, triggering multiple downstream handlers to run in parallel.
|
||||
- **Joining/Aggregation**: The `stream` object in the context is used to collect results from parallel tasks. You can use `stream.filter(...).until(...).toArray()` to wait for a specific number of events to complete before proceeding.
|
||||
- **Middleware**: The library supports middleware to add functionality like state management (`createStatefulMiddleware`) or validation (`withValidation`) to your workflows.
|
||||
|
||||
This functional and event-driven approach provides a powerful and flexible way to define complex agentic logic that can be compiled into a standalone, executable script. For more details, you can refer to the official documentation at [https://ts.llamaindex.ai/docs/workflows](mdc:https:/ts.llamaindex.ai/docs/workflows).
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getLlm } from "@/lib/llm-utils";
|
||||
import { MessageContent } from "@llamaindex/core/llms";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
@@ -12,8 +13,41 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
const llm = getLlm(settings, node);
|
||||
|
||||
let finalInput = typeof input === 'string' ? input : JSON.stringify(input);
|
||||
|
||||
if (node.type === 'decision' && node.data.question) {
|
||||
finalInput = `Given the following information, answer the question with only "true" or "false".
|
||||
|
||||
Information:
|
||||
${finalInput}
|
||||
|
||||
Question: ${node.data.question}`;
|
||||
const response = await llm.chat({
|
||||
messages: [{ role: "user", content: finalInput }]
|
||||
});
|
||||
|
||||
const content: MessageContent = response.message.content;
|
||||
let textContent: string | null = null;
|
||||
|
||||
if (typeof content === 'string') {
|
||||
textContent = content;
|
||||
} else if (Array.isArray(content)) {
|
||||
const textBlock = content.find(c => c.type === 'text');
|
||||
if (textBlock && 'text' in textBlock) {
|
||||
textContent = textBlock.text;
|
||||
}
|
||||
}
|
||||
|
||||
const result = textContent?.toLowerCase().trim();
|
||||
return NextResponse.json({ output: result === 'true' });
|
||||
}
|
||||
|
||||
if (node.data.promptPrefix) {
|
||||
finalInput = `${node.data.promptPrefix}\n\n${finalInput}`;
|
||||
}
|
||||
|
||||
const response = await llm.chat({
|
||||
messages: [{ role: "user", content: typeof input === 'string' ? input : JSON.stringify(input) }]
|
||||
messages: [{ role: "user", content: finalInput }]
|
||||
});
|
||||
|
||||
return NextResponse.json({ output: response.message.content });
|
||||
|
||||
+26
-8
@@ -230,17 +230,35 @@ All colors MUST be HSL.
|
||||
}
|
||||
|
||||
.node-decision {
|
||||
border-color: hsl(var(--node-decision));
|
||||
background: hsl(var(--card));
|
||||
transform: rotate(45deg);
|
||||
width: 100px !important;
|
||||
height: 100px !important;
|
||||
min-width: 100px !important;
|
||||
min-height: 100px !important;
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.glow-path {
|
||||
filter: drop-shadow(0 0 5px hsl(var(--primary-glow)));
|
||||
}
|
||||
|
||||
.node-decision .node-content {
|
||||
transform: rotate(-45deg);
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.node-decision .node-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.node-decision .textarea {
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
/* Handle Styles */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import { useCallback, useRef, useState, useEffect, Dispatch, SetStateAction } from 'react';
|
||||
import { useCallback, useRef, useState, useEffect, Dispatch, SetStateAction, useMemo } from 'react';
|
||||
import {
|
||||
ReactFlow,
|
||||
Background,
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
ReactFlowProvider,
|
||||
OnNodesChange,
|
||||
OnEdgesChange,
|
||||
NodeProps,
|
||||
} from '@xyflow/react';
|
||||
|
||||
// Import custom nodes
|
||||
@@ -106,11 +107,22 @@ const AgentFlowInner = ({ nodes, edges, onNodesChange, onEdgesChange, setNodes,
|
||||
|
||||
const onConnect = useCallback(
|
||||
(params: Connection) => {
|
||||
const sourceNode = nodes.find(node => node.id === params.source);
|
||||
let label;
|
||||
if (sourceNode?.type === 'decision') {
|
||||
if (params.sourceHandle === 'true') {
|
||||
label = 'True';
|
||||
} else if (params.sourceHandle === 'false') {
|
||||
label = 'False';
|
||||
}
|
||||
}
|
||||
|
||||
const newEdge: Edge = {
|
||||
...params,
|
||||
id: `edge-${params.source}-${params.target}`,
|
||||
type: 'default',
|
||||
animated: false,
|
||||
label,
|
||||
style: {
|
||||
strokeWidth: 2,
|
||||
stroke: 'hsl(var(--muted-foreground))'
|
||||
@@ -121,7 +133,7 @@ const AgentFlowInner = ({ nodes, edges, onNodesChange, onEdgesChange, setNodes,
|
||||
};
|
||||
setEdges((eds) => addEdge(newEdge, eds));
|
||||
},
|
||||
[setEdges]
|
||||
[setEdges, nodes]
|
||||
);
|
||||
|
||||
const onDragOver = useCallback((event: React.DragEvent) => {
|
||||
|
||||
+14
-29
@@ -387,13 +387,16 @@ const RunViewInner = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pass the input along to the next node
|
||||
setWorkflowState((prevState) => ({ ...prevState, [nodeId]: input }));
|
||||
|
||||
const thinkingMessageId = Math.random().toString();
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: thinkingMessageId,
|
||||
role: 'assistant',
|
||||
content: `Thinking with ${node.data.label}...`,
|
||||
content: `Evaluating: ${node.data.question}...`,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -401,19 +404,7 @@ const RunViewInner = () => {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
input: input,
|
||||
node: {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
prompt: `Given the input, which of the following paths should be taken? The choices are: ${Object.keys(
|
||||
node.data.choices,
|
||||
).join(', ')}. Please respond with only the name of the path.
|
||||
|
||||
Input:
|
||||
${input}
|
||||
`,
|
||||
},
|
||||
},
|
||||
node: node,
|
||||
settings: loadSavedSettings(),
|
||||
}),
|
||||
headers: {
|
||||
@@ -427,8 +418,7 @@ ${input}
|
||||
);
|
||||
}
|
||||
|
||||
const { output: llmOutput } = await llmResponse.json();
|
||||
const decision = getMessageContent(llmOutput).trim();
|
||||
const { output: decision } = await llmResponse.json();
|
||||
|
||||
setMessages((prev) =>
|
||||
prev.map((msg) =>
|
||||
@@ -439,20 +429,15 @@ ${input}
|
||||
);
|
||||
|
||||
// Find the output handle that matches the decision
|
||||
const choiceHandleId = (node.data.choices as any)[decision];
|
||||
if (choiceHandleId) {
|
||||
const nextDecisionNode = workflow.nodes.find(
|
||||
(n) => n.accepts === choiceHandleId,
|
||||
);
|
||||
if (nextDecisionNode) {
|
||||
nextNodeId = nextDecisionNode.id.replace('node-', '');
|
||||
} else {
|
||||
setExecutionStatus('finished');
|
||||
}
|
||||
const handleId = decision ? 'true' : 'false';
|
||||
const nextEdge = workflow.nodes.find(
|
||||
(n) => n.accepts === (node.emits as any)[handleId],
|
||||
);
|
||||
|
||||
if (nextEdge) {
|
||||
nextNodeId = nextEdge.id.replace('node-', '');
|
||||
} else {
|
||||
setError(`No path found for decision: ${decision}`);
|
||||
setExecutionStatus('error');
|
||||
return;
|
||||
setExecutionStatus('finished');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,32 +1,93 @@
|
||||
import { memo } from 'react';
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { Handle, Position, useReactFlow } from '@xyflow/react';
|
||||
import { GitBranch } from 'lucide-react';
|
||||
import { Textarea } from '../ui/textarea';
|
||||
|
||||
interface DecisionNodeProps {
|
||||
id: string;
|
||||
data: {
|
||||
label?: string;
|
||||
condition?: string;
|
||||
question?: string;
|
||||
};
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
const DecisionNode = memo(({ data, selected }: DecisionNodeProps) => {
|
||||
const DecisionNode = memo(({ id, data, selected }: DecisionNodeProps) => {
|
||||
const { setNodes } = useReactFlow();
|
||||
const size = 240;
|
||||
const halfSize = size / 2;
|
||||
const strokeWidth = 2;
|
||||
const path = `M ${halfSize},${strokeWidth} L ${size - strokeWidth},${halfSize} L ${halfSize},${size - strokeWidth} L ${strokeWidth},${halfSize} Z`;
|
||||
|
||||
const onChange = useCallback(
|
||||
(evt: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const newQuestion = evt.target.value;
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
if (node.id === id) {
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
question: newQuestion,
|
||||
},
|
||||
};
|
||||
}
|
||||
return node;
|
||||
}),
|
||||
);
|
||||
},
|
||||
[id, setNodes],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`agent-node node-decision ${selected ? 'selected' : ''}`}>
|
||||
<div className="node-content flex flex-col items-center justify-center text-foreground text-center">
|
||||
<GitBranch className="w-5 h-5 mb-2" />
|
||||
<span className="text-sm font-medium">{data.label || 'Decision'}</span>
|
||||
<div
|
||||
className={`relative ${selected ? 'selected' : ''}`}
|
||||
style={{ width: size, height: size }}
|
||||
>
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
className="absolute"
|
||||
>
|
||||
<path
|
||||
d={path}
|
||||
fill="hsl(var(--card))"
|
||||
stroke="hsl(var(--node-decision))"
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
{selected && (
|
||||
<path
|
||||
d={path}
|
||||
fill="none"
|
||||
stroke="hsl(var(--primary))"
|
||||
strokeWidth={strokeWidth * 2}
|
||||
className="glow-path"
|
||||
/>
|
||||
)}
|
||||
</svg>
|
||||
<div className="relative flex flex-col items-center justify-center h-full text-foreground p-5">
|
||||
<div className="flex items-center mb-2">
|
||||
<GitBranch className="w-5 h-5 mr-2" />
|
||||
<span className="text-sm font-medium">{data.label || 'Decision'}</span>
|
||||
</div>
|
||||
<Textarea
|
||||
className="nodrag nowheel text-sm text-center bg-transparent border-none focus:outline-none"
|
||||
value={data.question || ''}
|
||||
onChange={onChange}
|
||||
placeholder="Condition..."
|
||||
data-id="question"
|
||||
style={{ resize: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
position={Position.Top}
|
||||
id="input"
|
||||
style={{
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
left: '0%',
|
||||
top: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
border: '3px solid hsl(var(--primary))',
|
||||
backgroundColor: 'hsl(var(--card))',
|
||||
}}
|
||||
@@ -38,11 +99,6 @@ const DecisionNode = memo(({ data, selected }: DecisionNodeProps) => {
|
||||
style={{
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
right: '0%',
|
||||
top: '50%',
|
||||
transform: 'translate(50%, -50%)',
|
||||
border: '2px solid hsl(var(--primary))',
|
||||
backgroundColor: 'hsl(var(--primary))',
|
||||
}}
|
||||
/>
|
||||
<Handle
|
||||
@@ -52,11 +108,6 @@ const DecisionNode = memo(({ data, selected }: DecisionNodeProps) => {
|
||||
style={{
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
left: '50%',
|
||||
bottom: '0%',
|
||||
transform: 'translate(-50%, 50%)',
|
||||
border: '2px solid hsl(var(--primary))',
|
||||
backgroundColor: 'hsl(var(--primary))',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -6,16 +6,48 @@ interface PromptLLMNodeProps {
|
||||
id: string;
|
||||
data: {
|
||||
label?: string;
|
||||
promptPrefix?: string;
|
||||
};
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
const PromptLLMNode = memo(({ id, data, selected }: PromptLLMNodeProps) => {
|
||||
const { setNodes } = useReactFlow();
|
||||
const [prefix, setPrefix] = useState(data.promptPrefix || '');
|
||||
|
||||
const onPrefixChange = useCallback(
|
||||
(evt: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setPrefix(evt.target.value);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setNodes(nds =>
|
||||
nds.map(node => {
|
||||
if (node.id === id) {
|
||||
node.data = {
|
||||
...node.data,
|
||||
promptPrefix: prefix,
|
||||
};
|
||||
}
|
||||
return node;
|
||||
}),
|
||||
);
|
||||
}, [id, prefix, setNodes]);
|
||||
|
||||
return (
|
||||
<div className={`agent-node node-llm ${selected ? 'selected' : ''}`}>
|
||||
<div className="node-content flex flex-col items-center justify-center text-foreground p-4 min-w-[180px]">
|
||||
<Brain className="w-5 h-5 mb-2" />
|
||||
<span className="text-sm font-medium mb-2">{data.label || 'Prompt LLM'}</span>
|
||||
<label className="text-xs font-medium mt-2">Prompt Prefix</label>
|
||||
<textarea
|
||||
value={prefix}
|
||||
onChange={onPrefixChange}
|
||||
className="nodrag nowheel w-full text-xs p-1 mt-1 rounded-md bg-background border border-border"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<Handle
|
||||
type="target"
|
||||
|
||||
+113
-53
@@ -1,5 +1,7 @@
|
||||
import { getLlmModelName } from "./llm-utils";
|
||||
import { WorkflowJson, WorkflowNodeJson } from "./workflow-compiler";
|
||||
import { createWorkflow, workflowEvent } from "@llamaindex/workflow-core";
|
||||
import { MessageContent } from "@llamaindex/core/llms";
|
||||
|
||||
const toCamelCase = (str: string): string => {
|
||||
if (!str) return '';
|
||||
@@ -312,79 +314,137 @@ workflow.handle([${startEventName}], async (ctx) => {
|
||||
});
|
||||
`);
|
||||
} else if (node.type === 'userInput') {
|
||||
const incomingEvent = getEventName(node.accepts as string);
|
||||
const incomingEvents = Array.isArray(node.accepts) ? node.accepts.map(getEventName) : [getEventName(node.accepts as string)];
|
||||
const outgoingEvent = getEventName(node.emits as string);
|
||||
const promptText = node.data.prompt || 'Please provide input:';
|
||||
handlerLines.push(`
|
||||
workflow.handle([${incomingEvent}], async () => {
|
||||
for (const event of incomingEvents) {
|
||||
handlerLines.push(`
|
||||
workflow.handle([${event}], async () => {
|
||||
return need_input_for_${outgoingEvent}.with("${promptText}");
|
||||
});
|
||||
`);
|
||||
} else if (node.type === 'promptAgent') {
|
||||
const incomingEvent = getEventName(node.accepts as string);
|
||||
const outgoingEvent = getEventName(node.emits as string);
|
||||
|
||||
const toolNames = (node.data.tools || []).map((tool: any) => toCamelCase(tool.name));
|
||||
const modelName = getLlmModelName(json.settings, { data: node.data });
|
||||
const llmVarName = toCamelCase(modelName);
|
||||
const systemPrompt = node.data.prompt;
|
||||
}
|
||||
} else if (node.type === "splitter") {
|
||||
const outgoingEvent = getEventName(node.emits as string);
|
||||
handlerLines.push(
|
||||
`// Splitter node ${node.id} - fans out to multiple branches`,
|
||||
);
|
||||
} else if (node.type === "collector") {
|
||||
// Collector nodes are implicitly handled by having multiple events
|
||||
// trigger the same handler. The handler will receive an array of results.
|
||||
handlerLines.push(`// Collector node ${node.id} - logic is in the handler`);
|
||||
} else if (node.type === "promptAgent") {
|
||||
const llmIdentifier = toCamelCase(getLlmModelName(json.settings, node));
|
||||
const toolVars = (node.data.tools || []).map((tool: any) =>
|
||||
toCamelCase(
|
||||
tool.name || `search_index_${tool.id.replace(/-/g, "_")}`,
|
||||
),
|
||||
);
|
||||
const systemPrompt = node.data.prompt
|
||||
? `\`${node.data.prompt}\``
|
||||
: "undefined";
|
||||
const outgoingEvent = getEventName(node.emits as string);
|
||||
const agentVar = toCamelCase(`${node.id}Agent`);
|
||||
|
||||
const agentProperties = [
|
||||
`llm: ${llmVarName}`,
|
||||
`tools: [${toolNames.join(", ")}]`,
|
||||
];
|
||||
handlerLines.push(`
|
||||
const ${agentVar} = agent({
|
||||
llm: ${llmIdentifier},
|
||||
tools: [${toolVars.join(", ")}],
|
||||
systemPrompt: ${systemPrompt},
|
||||
});`);
|
||||
|
||||
if (systemPrompt) {
|
||||
agentProperties.push(`systemPrompt: ${JSON.stringify(systemPrompt)}`);
|
||||
}
|
||||
|
||||
handlerLines.push(`
|
||||
workflow.handle([${incomingEvent}], async (ctx) => {
|
||||
const configuredAgent = agent({
|
||||
${agentProperties.join(",\n ")}
|
||||
});
|
||||
const result = await configuredAgent.run(ctx.data);
|
||||
const incomingEvents = Array.isArray(node.accepts) ? node.accepts.map(getEventName) : [getEventName(node.accepts as string)];
|
||||
for (const event of incomingEvents) {
|
||||
handlerLines.push(`
|
||||
workflow.handle([${event}], async (ctx) => {
|
||||
console.log("Executing node ${node.id}");
|
||||
const result = await ${agentVar}.run(ctx.data);
|
||||
return ${outgoingEvent}.with(result.data.result);
|
||||
});
|
||||
`);
|
||||
} else if (node.type === 'stop') {
|
||||
const incomingEvent = getEventName(node.accepts as string);
|
||||
handlerLines.push(`
|
||||
workflow.handle([${incomingEvent}], async (ctx) => {
|
||||
return stopEvent.with(ctx.data);
|
||||
});
|
||||
`);
|
||||
});`);
|
||||
}
|
||||
} else if (node.type === "promptLLM") {
|
||||
const llmIdentifier = toCamelCase(getLlmModelName(json.settings, node));
|
||||
const inputVar = toCamelCase(node.id);
|
||||
const promptPrefix = node.data.promptPrefix
|
||||
? `\`${node.data.promptPrefix}\\n\\n\${JSON.stringify(${inputVar})}\``
|
||||
: `JSON.stringify(${inputVar})`;
|
||||
const outgoingEvent = getEventName(node.emits as string);
|
||||
|
||||
const incomingEvents = Array.isArray(node.accepts) ? node.accepts.map(getEventName) : [getEventName(node.accepts as string)];
|
||||
for (const event of incomingEvents) {
|
||||
handlerLines.push(`
|
||||
workflow.handle([${event}], async ( ${inputVar} ) => {
|
||||
console.log("Executing node ${node.id}");
|
||||
const response = await ${llmIdentifier}.chat({
|
||||
messages: [{ role: "user", content: ${promptPrefix} }]
|
||||
});
|
||||
const content: MessageContent = response.message.content;
|
||||
let textContent: string | null = null;
|
||||
if (typeof content === 'string') {
|
||||
textContent = content;
|
||||
} else if (Array.isArray(content)) {
|
||||
const textBlock = content.find(c => c.type === 'text');
|
||||
if (textBlock && 'text' in textBlock) {
|
||||
textContent = textBlock.text;
|
||||
}
|
||||
}
|
||||
return ${outgoingEvent}.with(textContent);
|
||||
});`);
|
||||
}
|
||||
} else if (node.type === "stop") {
|
||||
const incomingEvents = Array.isArray(node.accepts) ? node.accepts.map(getEventName) : [getEventName(node.accepts as string)];
|
||||
for (const event of incomingEvents) {
|
||||
handlerLines.push(
|
||||
`workflow.handle([${event}], async (result) => {
|
||||
console.log("Workflow finished with result:", result);
|
||||
return stopEvent.with(result.data);
|
||||
});`,
|
||||
);
|
||||
}
|
||||
} else if (node.type === 'decision') {
|
||||
const trueEvent = (node.emits as { [key: string]: string })?.true;
|
||||
const falseEvent = (node.emits as { [key: string]: string })?.false;
|
||||
const trueEventName = getEventName((node.emits as { [key: string]: string })?.true);
|
||||
const falseEventName = getEventName((node.emits as { [key: string]: string })?.false);
|
||||
const question = node.data.question;
|
||||
const llmVar = toCamelCase(getLlmModelName(json.settings, { data: node.data }));
|
||||
|
||||
const incomingEvents = Array.isArray(node.accepts) ? node.accepts.map(getEventName) : [getEventName(node.accepts as string)];
|
||||
for (const event of incomingEvents) {
|
||||
handlerLines.push(`
|
||||
workflow.handle([${event}], async (ctx) => {
|
||||
console.log("Evaluating question: ${question}");
|
||||
const llm = ${llmVar};
|
||||
const response = await llm.chat({
|
||||
messages: [{ role: "user", content: \`Given the following information, answer the question with only "true" or "false".
|
||||
|
||||
let trueTarget, falseTarget;
|
||||
Information:
|
||||
\${JSON.stringify(ctx.data)}
|
||||
|
||||
if (trueEvent) {
|
||||
trueTarget = json.nodes.find(n => n.accepts === trueEvent);
|
||||
}
|
||||
if (falseEvent) {
|
||||
falseTarget = json.nodes.find(n => n.accepts === falseEvent);
|
||||
}
|
||||
|
||||
handlerLines.push(`
|
||||
workflow.handle([${getEventName(node.accepts as string)}], async (ctx) => {
|
||||
console.log("Executing condition:", \`${node.data.condition}\`);
|
||||
const llm = ${toCamelCase(getLlmModelName(json.settings, node))};
|
||||
const response = await llm.complete({
|
||||
prompt: \`You are a decision-making AI. Based on the following context, is the condition true or false? Condition: ${node.data.condition}. Context: \${JSON.stringify(ctx.data)}\`,
|
||||
Question: ${question}\`}]
|
||||
});
|
||||
|
||||
const conditionResult = response.text.toLowerCase().includes('true');
|
||||
const content: MessageContent = response.message.content;
|
||||
let textContent: string | null = null;
|
||||
if (typeof content === 'string') {
|
||||
textContent = content;
|
||||
} else if (Array.isArray(content)) {
|
||||
const textBlock = content.find(c => c.type === 'text');
|
||||
if (textBlock && 'text' in textBlock) {
|
||||
textContent = textBlock.text;
|
||||
}
|
||||
}
|
||||
const result = textContent?.toLowerCase().trim();
|
||||
const conditionResult = result === 'true';
|
||||
|
||||
console.log("Condition result:", conditionResult);
|
||||
|
||||
if (conditionResult) {
|
||||
${trueTarget ? `await workflow.emit("${trueEvent}", { ...ctx.data });` : ''}
|
||||
return ${trueEventName}.with(ctx.data);
|
||||
} else {
|
||||
${falseTarget ? `await workflow.emit("${falseEvent}", { ...ctx.data });` : ''}
|
||||
return ${falseEventName}.with(ctx.data);
|
||||
}
|
||||
});
|
||||
`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return handlerLines.join("\n");
|
||||
|
||||
+35
-133
@@ -14,72 +14,6 @@ export interface WorkflowJson {
|
||||
settings: any;
|
||||
}
|
||||
|
||||
function buildNodeJson(node: Node, allNodes: Node[], allEdges: Edge[], processedNodeIds: Set<string>): any {
|
||||
if (!node || processedNodeIds.has(node.id)) {
|
||||
return null;
|
||||
}
|
||||
processedNodeIds.add(node.id);
|
||||
|
||||
const outgoingEdges = allEdges.filter(edge => edge.source === node.id);
|
||||
const nodeJson: any = {
|
||||
id: `node-${node.id}`,
|
||||
type: node.type,
|
||||
};
|
||||
|
||||
if (node.type === 'promptAgent') {
|
||||
nodeJson.tools = [];
|
||||
const toolEdges = outgoingEdges.filter(edge => {
|
||||
const targetNode = allNodes.find(n => n.id === edge.target);
|
||||
return targetNode && targetNode.type === 'agentTool';
|
||||
});
|
||||
|
||||
for (const edge of toolEdges) {
|
||||
const toolNode = allNodes.find(n => n.id === edge.target);
|
||||
if (toolNode) {
|
||||
// Recursively build the tool node JSON, but don't add it to the main processed list yet
|
||||
const toolJson = buildNodeJson(toolNode, allNodes, allEdges, new Set());
|
||||
if (toolJson) {
|
||||
nodeJson.tools.push(toolJson);
|
||||
// Mark the tool node as processed so it's not added to the top level
|
||||
processedNodeIds.add(toolNode.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find the primary outgoing edge that is NOT a tool connection for promptAgent
|
||||
let primaryOutgoingEdge;
|
||||
if (node.type === 'promptAgent') {
|
||||
primaryOutgoingEdge = outgoingEdges.find(edge => {
|
||||
const targetNode = allNodes.find(n => n.id === edge.target);
|
||||
return targetNode && targetNode.type !== 'agentTool';
|
||||
});
|
||||
} else {
|
||||
primaryOutgoingEdge = outgoingEdges[0];
|
||||
}
|
||||
|
||||
|
||||
if (primaryOutgoingEdge) {
|
||||
const nextNode = allNodes.find(node => node.id === primaryOutgoingEdge.target);
|
||||
if (nextNode) {
|
||||
const nextNodeJson = buildNodeJson(nextNode, allNodes, allEdges, processedNodeIds);
|
||||
if (nextNodeJson) {
|
||||
nodeJson.next = nextNodeJson;
|
||||
// Simplified event-based connection for top-level nodes
|
||||
nodeJson.emits = `event-${primaryOutgoingEdge.id}`;
|
||||
nextNodeJson.accepts = `event-${primaryOutgoingEdge.id}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove label from the final JSON
|
||||
if (node.data && 'label' in node.data) {
|
||||
// keeping other data properties if they exist
|
||||
}
|
||||
|
||||
return nodeJson;
|
||||
}
|
||||
|
||||
export function compileWorkflow(nodes: Node[], edges: Edge[]): any {
|
||||
let settings = {};
|
||||
if (typeof window !== "undefined") {
|
||||
@@ -98,43 +32,28 @@ export function compileWorkflow(nodes: Node[], edges: Edge[]): any {
|
||||
return { error: "No start node found" };
|
||||
}
|
||||
|
||||
const processedNodeIds = new Set<string>();
|
||||
const allNodeJsons: any[] = [];
|
||||
const allReachableNodes = new Set<Node>();
|
||||
const queue: Node[] = [startNode];
|
||||
const visitedNodeIds = new Set<string>([startNode.id]);
|
||||
|
||||
// First pass: handle promptAgent and its tools
|
||||
nodes.forEach(node => {
|
||||
if (node.type === 'promptAgent') {
|
||||
const nodeJson = buildNodeJson(node, nodes, edges, processedNodeIds);
|
||||
if (nodeJson) {
|
||||
allNodeJsons.push(nodeJson);
|
||||
while (queue.length > 0) {
|
||||
const currentNode = queue.shift()!;
|
||||
allReachableNodes.add(currentNode);
|
||||
|
||||
const outgoingEdges = edges.filter(edge => edge.source === currentNode.id);
|
||||
for (const edge of outgoingEdges) {
|
||||
const targetNode = nodes.find(n => n.id === edge.target);
|
||||
if (targetNode && !visitedNodeIds.has(targetNode.id)) {
|
||||
visitedNodeIds.add(targetNode.id);
|
||||
queue.push(targetNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const allProcessedNodes = Array.from(allReachableNodes);
|
||||
|
||||
// Second pass: handle all other nodes that haven't been processed
|
||||
nodes.forEach(node => {
|
||||
if (!processedNodeIds.has(node.id)) {
|
||||
const nodeJson = buildNodeJson(node, nodes, edges, processedNodeIds);
|
||||
if (nodeJson) {
|
||||
allNodeJsons.push(nodeJson);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// The final JSON should be a flat list of nodes, where tools are nested.
|
||||
// We need to rebuild the structure from the startNode to get the correct order and nesting.
|
||||
processedNodeIds.clear();
|
||||
const finalJson = buildNodeJson(startNode, nodes, edges, processedNodeIds);
|
||||
|
||||
// After building the main path, we need to collect all nodes that were processed.
|
||||
const allProcessedNodes = nodes.filter(n => processedNodeIds.has(n.id));
|
||||
|
||||
// Now, we create the flat list for the output, but the nesting logic is handled inside buildNodeJson
|
||||
// Now, we create the flat list for the output
|
||||
const resultNodes = allProcessedNodes.map(node => {
|
||||
// Re-running buildNodeJson without recursion, just to get the final structure for each node.
|
||||
// This is not efficient and needs a better approach.
|
||||
const tempProcessedIds = new Set<string>();
|
||||
|
||||
// We can't just call buildNodeJson again. We need a new function to format the output.
|
||||
const nodeJson: any = {
|
||||
id: `node-${node.id}`,
|
||||
@@ -186,7 +105,14 @@ export function compileWorkflow(nodes: Node[], edges: Edge[]): any {
|
||||
nodeJson.data.prompt = node.data.prompt;
|
||||
}
|
||||
|
||||
if (node.type === 'decision') {
|
||||
if (node.type === 'promptLLM' && node.data && node.data.promptPrefix) {
|
||||
nodeJson.data.promptPrefix = node.data.promptPrefix;
|
||||
}
|
||||
|
||||
if (node.type === 'decision' && node.data) {
|
||||
if (node.data.question) {
|
||||
nodeJson.data.question = node.data.question;
|
||||
}
|
||||
const trueEdge = outgoingEdges.find(e => e.sourceHandle === 'true');
|
||||
const falseEdge = outgoingEdges.find(e => e.sourceHandle === 'false');
|
||||
const emits: { [key: string]: string } = {};
|
||||
@@ -209,12 +135,17 @@ export function compileWorkflow(nodes: Node[], edges: Edge[]): any {
|
||||
|
||||
const incomingEdges = edges.filter(e => e.target === node.id);
|
||||
if (incomingEdges.length > 0) {
|
||||
const edge = incomingEdges.find(e => {
|
||||
// This logic is getting complicated. Let's simplify.
|
||||
// A node accepts an event from its incoming connection.
|
||||
const relevantEdges = incomingEdges.filter(e => {
|
||||
const source = nodes.find(n => n.id === e.source);
|
||||
return !(source?.type === 'promptAgent' && nodeJson.type === 'agentTool');
|
||||
});
|
||||
if (edge) {
|
||||
nodeJson.accepts = `event-${edge.id}`;
|
||||
})
|
||||
|
||||
if (relevantEdges.length > 1) {
|
||||
nodeJson.accepts = relevantEdges.map(e => `event-${e.id}`);
|
||||
} else if (relevantEdges.length === 1) {
|
||||
nodeJson.accepts = `event-${relevantEdges[0].id}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,35 +168,6 @@ export function compileWorkflow(nodes: Node[], edges: Edge[]): any {
|
||||
});
|
||||
|
||||
const finalNodes = resultNodes.filter(n => !toolNodeIds.has(n.id.replace('node-','')));
|
||||
// Find the primary incoming edge for each node to set the 'accepts' property
|
||||
finalNodes.forEach(nodeJson => {
|
||||
const nodeId = nodeJson.id.replace('node-','');
|
||||
const incomingEdge = edges.find(e => e.target === nodeId && nodes.find(n => n.id === e.source)?.type !== 'promptAgent' && nodes.find(n => n.id === e.target)?.type === 'agentTool');
|
||||
const primaryIncomingEdge = edges.find(e => {
|
||||
const sourceNode = nodes.find(n => n.id === e.source);
|
||||
if (!sourceNode) return false;
|
||||
// if the target is me, and the source is not a prompt agent sending me a tool connection
|
||||
return e.target === nodeId && (sourceNode.type !== 'promptAgent' || nodes.find(n => n.id === e.target)?.type !== 'agentTool');
|
||||
});
|
||||
|
||||
const incomingEdges = edges.filter(e => e.target === nodeId);
|
||||
if (incomingEdges.length > 0) {
|
||||
// This logic is getting complicated. Let's simplify.
|
||||
// A node accepts an event from its incoming connection.
|
||||
const edge = incomingEdges.find(e => {
|
||||
const source = nodes.find(n => n.id === e.source);
|
||||
return !(source?.type === 'promptAgent' && nodeJson.type === 'agentTool');
|
||||
})
|
||||
if (edge) {
|
||||
nodeJson.accepts = `event-${edge.id}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (nodeJson.type === 'start') {
|
||||
delete nodeJson.accepts;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return { nodes: finalNodes, settings };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user