mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-27 17:36:25 +02:00
Agent pipelines (#193)
* Partial: Backend implementation for agent pipelines * Add v1 functionality for pipelines * Add ability to delete pipelines * Improve agent addition modal * Add transition messages for pipeline agents * Update agent config for pipeline agents * Modify configs for pipeline agents * Fix agent type and output viz for pipeline agents
This commit is contained in:
parent
97fad8633f
commit
96d87b6bdb
11 changed files with 1416 additions and 218 deletions
|
|
@ -129,4 +129,22 @@ export const TASK_TYPE_INSTRUCTIONS = (): string => `
|
||||||
- Reading the messages in the chat history will give you context about the conversation.
|
- Reading the messages in the chat history will give you context about the conversation.
|
||||||
- Seeing the tool calls that transfer / handoff control will help you understand the flow of the conversation and which agent produced each message.
|
- Seeing the tool calls that transfer / handoff control will help you understand the flow of the conversation and which agent produced each message.
|
||||||
- These are high level instructions only. The user will provide more specific instructions which will be below.
|
- These are high level instructions only. The user will provide more specific instructions which will be below.
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const PIPELINE_TYPE_INSTRUCTIONS = (): string => `
|
||||||
|
- You are a pipeline agent that is part of a sequential execution chain within a larger workflow.
|
||||||
|
- You are executing as one step in a multi-step pipeline process.
|
||||||
|
- Your input comes from the previous step in the pipeline (or the initial input if you're the first step).
|
||||||
|
- Your output will be passed to the next step in the pipeline (or returned as the final result if you're the last step).
|
||||||
|
- CRITICAL: You CANNOT transfer to other agents or pipelines. You can only use tools to complete your specific task.
|
||||||
|
- Focus ONLY on your designated role in the pipeline. Process the input, perform your specific task, and provide clear output.
|
||||||
|
- Use the JSON format to convey your responses. The JSON should have 3 keys:
|
||||||
|
- "thought": Analyze the input from the previous pipeline step and plan what you need to do
|
||||||
|
- "response": Your processed output that will be passed to the next pipeline step. Make this clear and actionable.
|
||||||
|
- "pipeline_context": Brief notes about what you accomplished for the pipeline flow
|
||||||
|
- Do NOT attempt to handle tasks outside your specific pipeline role.
|
||||||
|
- Do NOT mention other agents or the pipeline structure to users.
|
||||||
|
- Your response should be self-contained and ready to be consumed by the next pipeline step.
|
||||||
|
- Reading the message history will show you the pipeline execution flow up to your step.
|
||||||
|
- These are high level instructions only. The user will provide more specific instructions which will be below.
|
||||||
`;
|
`;
|
||||||
|
|
@ -16,11 +16,12 @@ import { getMcpClient } from "./mcp";
|
||||||
import { dataSourceDocsCollection, dataSourcesCollection, projectsCollection } from "./mongodb";
|
import { dataSourceDocsCollection, dataSourcesCollection, projectsCollection } from "./mongodb";
|
||||||
import { qdrantClient } from '../lib/qdrant';
|
import { qdrantClient } from '../lib/qdrant';
|
||||||
import { EmbeddingRecord } from "./types/datasource_types";
|
import { EmbeddingRecord } from "./types/datasource_types";
|
||||||
import { ConnectedEntity, sanitizeTextWithMentions, Workflow, WorkflowAgent, WorkflowPrompt, WorkflowTool } from "./types/workflow_types";
|
import { ConnectedEntity, sanitizeTextWithMentions, Workflow, WorkflowAgent, WorkflowPipeline, WorkflowPrompt, WorkflowTool } from "./types/workflow_types";
|
||||||
import { CHILD_TRANSFER_RELATED_INSTRUCTIONS, CONVERSATION_TYPE_INSTRUCTIONS, RAG_INSTRUCTIONS, TASK_TYPE_INSTRUCTIONS } from "./agent_instructions";
|
import { CHILD_TRANSFER_RELATED_INSTRUCTIONS, CONVERSATION_TYPE_INSTRUCTIONS, PIPELINE_TYPE_INSTRUCTIONS, RAG_INSTRUCTIONS, TASK_TYPE_INSTRUCTIONS } from "./agent_instructions";
|
||||||
import { PrefixLogger } from "./utils";
|
import { PrefixLogger } from "./utils";
|
||||||
import { Message, AssistantMessage, AssistantMessageWithToolCalls, ToolMessage } from "./types/types";
|
import { Message, AssistantMessage, AssistantMessageWithToolCalls, ToolMessage } from "./types/types";
|
||||||
|
|
||||||
|
// Make everything available as a promise
|
||||||
const PROVIDER_API_KEY = process.env.PROVIDER_API_KEY || process.env.OPENAI_API_KEY || '';
|
const PROVIDER_API_KEY = process.env.PROVIDER_API_KEY || process.env.OPENAI_API_KEY || '';
|
||||||
const PROVIDER_BASE_URL = process.env.PROVIDER_BASE_URL || undefined;
|
const PROVIDER_BASE_URL = process.env.PROVIDER_BASE_URL || undefined;
|
||||||
const MODEL = process.env.PROVIDER_DEFAULT_MODEL || 'gpt-4o';
|
const MODEL = process.env.PROVIDER_DEFAULT_MODEL || 'gpt-4o';
|
||||||
|
|
@ -518,7 +519,11 @@ ${config.description}
|
||||||
|
|
||||||
## About You
|
## About You
|
||||||
|
|
||||||
${config.outputVisibility === 'user_facing' ? CONVERSATION_TYPE_INSTRUCTIONS() : TASK_TYPE_INSTRUCTIONS()}
|
${config.outputVisibility === 'user_facing'
|
||||||
|
? CONVERSATION_TYPE_INSTRUCTIONS()
|
||||||
|
: config.type === 'pipeline'
|
||||||
|
? PIPELINE_TYPE_INSTRUCTIONS()
|
||||||
|
: TASK_TYPE_INSTRUCTIONS()}
|
||||||
|
|
||||||
## Instructions
|
## Instructions
|
||||||
|
|
||||||
|
|
@ -531,20 +536,16 @@ ${'-'.repeat(100)}
|
||||||
${CHILD_TRANSFER_RELATED_INSTRUCTIONS}
|
${CHILD_TRANSFER_RELATED_INSTRUCTIONS}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
let { sanitized, entities } = sanitizeTextWithMentions(instructions, workflow);
|
let { sanitized, entities } = sanitizeTextWithMentions(instructions, workflow, config);
|
||||||
|
|
||||||
|
// Remove agent transfer instructions for pipeline agents
|
||||||
|
if (config.type === 'pipeline') {
|
||||||
|
sanitized = sanitized.replace(CHILD_TRANSFER_RELATED_INSTRUCTIONS, '');
|
||||||
|
}
|
||||||
|
|
||||||
agentLogger.log(`instructions: ${JSON.stringify(sanitized)}`);
|
agentLogger.log(`instructions: ${JSON.stringify(sanitized)}`);
|
||||||
agentLogger.log(`mentions: ${JSON.stringify(entities)}`);
|
agentLogger.log(`mentions: ${JSON.stringify(entities)}`);
|
||||||
|
|
||||||
// // add prompts to instructions
|
|
||||||
// for (const e of entities) {
|
|
||||||
// if (e.type === 'prompt') {
|
|
||||||
// const prompt = promptConfig[e.name];
|
|
||||||
// if (prompt) {
|
|
||||||
// compiledInstructions = compiledInstructions + '\n\n# ' + prompt.name + '\n' + prompt.prompt;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
const agentTools = entities.filter(e => e.type === 'tool').map(e => tools[e.name]).filter(Boolean) as Tool[];
|
const agentTools = entities.filter(e => e.type === 'tool').map(e => tools[e.name]).filter(Boolean) as Tool[];
|
||||||
|
|
||||||
// Add RAG tool if needed
|
// Add RAG tool if needed
|
||||||
|
|
@ -641,22 +642,23 @@ function getStartOfTurnAgentName(
|
||||||
logger.log(`last agent ${lastAgentName} not found in agent config, returning start agent: ${workflow.startAgent}`);
|
logger.log(`last agent ${lastAgentName} not found in agent config, returning start agent: ${workflow.startAgent}`);
|
||||||
return workflow.startAgent;
|
return workflow.startAgent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For other agents, check control type
|
||||||
switch (lastAgentConfig.controlType) {
|
switch (lastAgentConfig.controlType) {
|
||||||
case 'retain':
|
case 'retain':
|
||||||
logger.log(`last agent ${lastAgentName} control type is retain, returning last agent: ${lastAgentName}`);
|
logger.log(`last agent ${lastAgentName} control type is retain, returning last agent: ${lastAgentName}`);
|
||||||
return lastAgentName;
|
return lastAgentName;
|
||||||
case 'relinquish_to_parent':
|
case 'relinquish_to_parent':
|
||||||
const parentAgentName = startAgentStack.pop() || workflow.startAgent;
|
const parentAgentName = startAgentStack.pop() || workflow.startAgent;
|
||||||
if (startAgentStack.length > 0) {
|
|
||||||
logger.log(`popped agent from stack: ${lastAgentName} || reason: relinquish to parent triggered`);
|
|
||||||
} else {
|
|
||||||
logger.log(`using start agent: ${lastAgentName} || reason: empty stack`);
|
|
||||||
}
|
|
||||||
logger.log(`last agent ${lastAgentName} control type is relinquish_to_parent, returning most recent parent: ${parentAgentName}`);
|
logger.log(`last agent ${lastAgentName} control type is relinquish_to_parent, returning most recent parent: ${parentAgentName}`);
|
||||||
return parentAgentName;
|
return parentAgentName;
|
||||||
case 'relinquish_to_start':
|
case 'relinquish_to_start':
|
||||||
logger.log(`last agent ${lastAgentName} control type is relinquish_to_start, returning start agent: ${workflow.startAgent}`);
|
logger.log(`last agent ${lastAgentName} control type is relinquish_to_start, returning start agent: ${workflow.startAgent}`);
|
||||||
return workflow.startAgent;
|
return workflow.startAgent;
|
||||||
|
default:
|
||||||
|
// Fallback for any unexpected control type
|
||||||
|
logger.log(`last agent ${lastAgentName} has unexpected control type: ${lastAgentConfig.controlType}, returning start agent: ${workflow.startAgent}`);
|
||||||
|
return workflow.startAgent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -767,6 +769,7 @@ function mapConfig(workflow: z.infer<typeof Workflow>): {
|
||||||
agentConfig: Record<string, z.infer<typeof WorkflowAgent>>;
|
agentConfig: Record<string, z.infer<typeof WorkflowAgent>>;
|
||||||
toolConfig: Record<string, z.infer<typeof WorkflowTool>>;
|
toolConfig: Record<string, z.infer<typeof WorkflowTool>>;
|
||||||
promptConfig: Record<string, z.infer<typeof WorkflowPrompt>>;
|
promptConfig: Record<string, z.infer<typeof WorkflowPrompt>>;
|
||||||
|
pipelineConfig: Record<string, z.infer<typeof WorkflowPipeline>>;
|
||||||
} {
|
} {
|
||||||
const agentConfig: Record<string, z.infer<typeof WorkflowAgent>> = workflow.agents.reduce((acc, agent) => ({
|
const agentConfig: Record<string, z.infer<typeof WorkflowAgent>> = workflow.agents.reduce((acc, agent) => ({
|
||||||
...acc,
|
...acc,
|
||||||
|
|
@ -780,7 +783,13 @@ function mapConfig(workflow: z.infer<typeof Workflow>): {
|
||||||
...acc,
|
...acc,
|
||||||
[prompt.name]: prompt
|
[prompt.name]: prompt
|
||||||
}), {});
|
}), {});
|
||||||
return { agentConfig, toolConfig, promptConfig };
|
|
||||||
|
const pipelineConfig: Record<string, z.infer<typeof WorkflowPipeline>> = (workflow.pipelines || []).reduce((acc, pipeline) => ({
|
||||||
|
...acc,
|
||||||
|
[pipeline.name]: pipeline
|
||||||
|
}), {});
|
||||||
|
|
||||||
|
return { agentConfig, toolConfig, promptConfig, pipelineConfig };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function* emitGreetingTurn(logger: PrefixLogger, workflow: z.infer<typeof Workflow>): AsyncIterable<z.infer<typeof ZOutMessage> | z.infer<typeof ZUsage>> {
|
async function* emitGreetingTurn(logger: PrefixLogger, workflow: z.infer<typeof Workflow>): AsyncIterable<z.infer<typeof ZOutMessage> | z.infer<typeof ZUsage>> {
|
||||||
|
|
@ -807,27 +816,29 @@ function createTools(
|
||||||
toolConfig: Record<string, z.infer<typeof WorkflowTool>>,
|
toolConfig: Record<string, z.infer<typeof WorkflowTool>>,
|
||||||
): Record<string, Tool> {
|
): Record<string, Tool> {
|
||||||
const tools: Record<string, Tool> = {};
|
const tools: Record<string, Tool> = {};
|
||||||
|
const toolLogger = logger.child('createTools');
|
||||||
|
|
||||||
|
toolLogger.log(`=== CREATING ${Object.keys(toolConfig).length} TOOLS ===`);
|
||||||
|
|
||||||
for (const [toolName, config] of Object.entries(toolConfig)) {
|
for (const [toolName, config] of Object.entries(toolConfig)) {
|
||||||
if (workflow.mockTools?.[toolName]) {
|
toolLogger.log(`creating tool: ${toolName} (type: ${config.mockTool ? 'mock' : config.isMcp ? 'mcp' : config.isComposio ? 'composio' : 'webhook'})`);
|
||||||
tools[toolName] = createMockTool(logger, {
|
|
||||||
...config,
|
if (config.mockTool) {
|
||||||
mockInstructions: workflow.mockTools?.[toolName], // override mock instructions
|
|
||||||
});
|
|
||||||
logger.log(`created mock tool: ${toolName}`);
|
|
||||||
} else if (config.mockTool) {
|
|
||||||
tools[toolName] = createMockTool(logger, config);
|
tools[toolName] = createMockTool(logger, config);
|
||||||
logger.log(`created mock tool: ${toolName}`);
|
toolLogger.log(`✓ created mock tool: ${toolName}`);
|
||||||
} else if (config.isMcp) {
|
} else if (config.isMcp) {
|
||||||
tools[toolName] = createMcpTool(logger, config, projectId);
|
tools[toolName] = createMcpTool(logger, config, projectId);
|
||||||
logger.log(`created mcp tool: ${toolName}`);
|
toolLogger.log(`✓ created mcp tool: ${toolName} (server: ${config.mcpServerName || 'unknown'})`);
|
||||||
} else if (config.isComposio) {
|
} else if (config.isComposio) {
|
||||||
tools[toolName] = createComposioTool(logger, config, projectId);
|
tools[toolName] = createComposioTool(logger, config, projectId);
|
||||||
logger.log(`created composio tool: ${toolName}`);
|
toolLogger.log(`✓ created composio tool: ${toolName}`);
|
||||||
} else {
|
} else {
|
||||||
tools[toolName] = createWebhookTool(logger, config, projectId);
|
tools[toolName] = createWebhookTool(logger, config, projectId);
|
||||||
logger.log(`created webhook tool: ${toolName}`);
|
toolLogger.log(`✓ created webhook tool: ${toolName} (fallback)`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toolLogger.log(`=== TOOL CREATION COMPLETE ===`);
|
||||||
return tools;
|
return tools;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -838,14 +849,34 @@ function createAgents(
|
||||||
agentConfig: Record<string, z.infer<typeof WorkflowAgent>>,
|
agentConfig: Record<string, z.infer<typeof WorkflowAgent>>,
|
||||||
tools: Record<string, Tool>,
|
tools: Record<string, Tool>,
|
||||||
promptConfig: Record<string, z.infer<typeof WorkflowPrompt>>,
|
promptConfig: Record<string, z.infer<typeof WorkflowPrompt>>,
|
||||||
|
pipelineConfig: Record<string, z.infer<typeof WorkflowPipeline>>,
|
||||||
): { agents: Record<string, Agent>, mentions: Record<string, z.infer<typeof ConnectedEntity>[]>, originalInstructions: Record<string, string>, originalHandoffs: Record<string, Agent[]> } {
|
): { agents: Record<string, Agent>, mentions: Record<string, z.infer<typeof ConnectedEntity>[]>, originalInstructions: Record<string, string>, originalHandoffs: Record<string, Agent[]> } {
|
||||||
|
const agentsLogger = logger.child('createAgents');
|
||||||
const agents: Record<string, Agent> = {};
|
const agents: Record<string, Agent> = {};
|
||||||
const mentions: Record<string, z.infer<typeof ConnectedEntity>[]> = {};
|
const mentions: Record<string, z.infer<typeof ConnectedEntity>[]> = {};
|
||||||
const originalInstructions: Record<string, string> = {};
|
const originalInstructions: Record<string, string> = {};
|
||||||
const originalHandoffs: Record<string, Agent[]> = {};
|
const originalHandoffs: Record<string, Agent[]> = {};
|
||||||
|
|
||||||
|
agentsLogger.log(`=== CREATING ${Object.keys(agentConfig).length} AGENTS ===`);
|
||||||
|
|
||||||
|
// Create pipeline entities that will be available for @ referencing
|
||||||
|
const pipelineEntities: z.infer<typeof ConnectedEntity>[] = Object.keys(pipelineConfig).map(pipelineName => ({
|
||||||
|
type: 'pipeline' as const,
|
||||||
|
name: pipelineName,
|
||||||
|
}));
|
||||||
|
if (pipelineEntities.length > 0) {
|
||||||
|
agentsLogger.log(`available pipeline entities for @ referencing: ${pipelineEntities.map(p => p.name).join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
// create agents
|
// create agents
|
||||||
for (const [agentName, config] of Object.entries(agentConfig)) {
|
for (const [agentName, config] of Object.entries(agentConfig)) {
|
||||||
|
agentsLogger.log(`creating agent: ${agentName} (type: ${config.outputVisibility}, control: ${config.controlType})`);
|
||||||
|
|
||||||
|
// Pipeline agents get special handling:
|
||||||
|
// - Different instruction template (PIPELINE_TYPE_INSTRUCTIONS)
|
||||||
|
// - Filtered mentions (tools only, no agents)
|
||||||
|
// - No agent transfer instructions
|
||||||
|
|
||||||
const { agent, entities } = createAgent(
|
const { agent, entities } = createAgent(
|
||||||
logger,
|
logger,
|
||||||
projectId,
|
projectId,
|
||||||
|
|
@ -855,18 +886,96 @@ function createAgents(
|
||||||
promptConfig,
|
promptConfig,
|
||||||
);
|
);
|
||||||
agents[agentName] = agent;
|
agents[agentName] = agent;
|
||||||
mentions[agentName] = entities;
|
|
||||||
|
// Add pipeline entities to the agent's available mentions (unless it's a pipeline agent itself)
|
||||||
|
// Pipeline agents cannot reference other agents or pipelines, only tools
|
||||||
|
let agentEntities = entities;
|
||||||
|
if (config.type !== 'pipeline') {
|
||||||
|
agentEntities = [...entities, ...pipelineEntities];
|
||||||
|
agentsLogger.log(`${agentName} can reference: ${entities.length} entities + ${pipelineEntities.length} pipelines`);
|
||||||
|
} else {
|
||||||
|
agentsLogger.log(`${agentName} (pipeline agent) can reference: ${entities.length} entities only`);
|
||||||
|
}
|
||||||
|
|
||||||
|
mentions[agentName] = agentEntities;
|
||||||
originalInstructions[agentName] = agent.instructions as string;
|
originalInstructions[agentName] = agent.instructions as string;
|
||||||
// handoffs will be set after all agents are created
|
// handoffs will be set after all agents are created
|
||||||
}
|
}
|
||||||
|
|
||||||
|
agentsLogger.log(`=== SETTING UP HANDOFFS ===`);
|
||||||
|
|
||||||
// set handoffs
|
// set handoffs
|
||||||
for (const [agentName, agent] of Object.entries(agents)) {
|
for (const [agentName, agent] of Object.entries(agents)) {
|
||||||
const connectedAgentNames = (mentions[agentName] || []).filter(e => e.type === 'agent').map(e => e.name);
|
const connectedAgentNames = (mentions[agentName] || []).filter(e => e.type === 'agent').map(e => e.name);
|
||||||
|
const connectedPipelineNames = (mentions[agentName] || []).filter(e => e.type === 'pipeline').map(e => e.name);
|
||||||
|
|
||||||
|
// Pipeline agents have no agent handoffs (filtered out in validatePipelineAgentMentions)
|
||||||
|
// They only have tool connections, no agent transfers allowed
|
||||||
|
|
||||||
|
// Filter out pipeline agents from being handoff targets
|
||||||
|
// Only allow handoffs to non-pipeline agents
|
||||||
|
const validAgentNames = connectedAgentNames.filter(name => {
|
||||||
|
const targetConfig = agentConfig[name];
|
||||||
|
return targetConfig && targetConfig.type !== 'pipeline';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert pipeline mentions to handoffs to the first agent in each pipeline
|
||||||
|
const pipelineFirstAgents: string[] = [];
|
||||||
|
for (const pipelineName of connectedPipelineNames) {
|
||||||
|
const pipeline = pipelineConfig[pipelineName];
|
||||||
|
if (pipeline && pipeline.agents.length > 0) {
|
||||||
|
const firstAgent = pipeline.agents[0];
|
||||||
|
if (agentConfig[firstAgent] && !pipelineFirstAgents.includes(firstAgent)) {
|
||||||
|
pipelineFirstAgents.push(firstAgent);
|
||||||
|
agentsLogger.log(`${agentName} pipeline mention ${pipelineName} -> handoff to first agent: ${firstAgent}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine regular agent handoffs with pipeline first agents
|
||||||
|
const allHandoffTargets = [...validAgentNames, ...pipelineFirstAgents];
|
||||||
|
|
||||||
// Only store Agent objects in handoffs (filter out Handoff if present)
|
// Only store Agent objects in handoffs (filter out Handoff if present)
|
||||||
agent.handoffs = connectedAgentNames.map(e => agents[e]).filter(Boolean) as Agent[];
|
const agentHandoffs = allHandoffTargets.map(e => agents[e]).filter(Boolean) as Agent[];
|
||||||
originalHandoffs[agentName] = agent.handoffs.filter(h => h instanceof Agent);
|
agent.handoffs = agentHandoffs;
|
||||||
logger.log(`set handoffs for ${agentName}: ${JSON.stringify(connectedAgentNames)}`);
|
originalHandoffs[agentName] = agentHandoffs.filter(h => h instanceof Agent);
|
||||||
|
agentsLogger.log(`set handoffs for ${agentName}: ${JSON.stringify(allHandoffTargets)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up pipeline agent handoff chains
|
||||||
|
agentsLogger.log(`=== SETTING UP PIPELINE CHAINS ===`);
|
||||||
|
for (const [pipelineName, pipeline] of Object.entries(pipelineConfig)) {
|
||||||
|
agentsLogger.log(`setting up pipeline chain: ${pipelineName} -> [${pipeline.agents.join(' -> ')}]`);
|
||||||
|
|
||||||
|
for (let i = 0; i < pipeline.agents.length; i++) {
|
||||||
|
const currentAgentName = pipeline.agents[i];
|
||||||
|
const currentAgent = agents[currentAgentName];
|
||||||
|
|
||||||
|
if (!currentAgent) {
|
||||||
|
agentsLogger.log(`warning: pipeline agent ${currentAgentName} not found in agent config`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pipeline agents have NO handoffs - they just execute once
|
||||||
|
currentAgent.handoffs = [];
|
||||||
|
|
||||||
|
// Add pipeline metadata to the agent for easy lookup
|
||||||
|
(currentAgent as any).pipelineName = pipelineName;
|
||||||
|
(currentAgent as any).pipelineIndex = i;
|
||||||
|
(currentAgent as any).isLastInPipeline = i === pipeline.agents.length - 1;
|
||||||
|
|
||||||
|
// Update originalHandoffs to reflect the final pipeline state
|
||||||
|
originalHandoffs[currentAgentName] = [];
|
||||||
|
|
||||||
|
agentsLogger.log(`pipeline agent ${currentAgentName} has no handoffs (will be controlled by pipeline controller)`);
|
||||||
|
agentsLogger.log(`pipeline agent ${currentAgentName} metadata: pipeline=${pipelineName}, index=${i}, isLast=${i === pipeline.agents.length - 1}`);
|
||||||
|
|
||||||
|
// Configure pipeline agents to relinquish control after completing their task
|
||||||
|
const agentConfigObj = agentConfig[currentAgentName];
|
||||||
|
if (agentConfigObj && agentConfigObj.type === 'pipeline') {
|
||||||
|
agentsLogger.log(`configuring pipeline agent ${currentAgentName} to relinquish control after task completion`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { agents, mentions, originalInstructions, originalHandoffs };
|
return { agents, mentions, originalInstructions, originalHandoffs };
|
||||||
|
|
@ -910,10 +1019,19 @@ function maybeInjectGiveUpControlInstructions(
|
||||||
|
|
||||||
const agentConfigObj = agentConfig[childAgentName];
|
const agentConfigObj = agentConfig[childAgentName];
|
||||||
const isInternal = agentConfigObj?.outputVisibility === 'internal';
|
const isInternal = agentConfigObj?.outputVisibility === 'internal';
|
||||||
|
const isPipeline = agentConfigObj?.type === 'pipeline';
|
||||||
const isRetain = agentConfigObj?.controlType === 'retain';
|
const isRetain = agentConfigObj?.controlType === 'retain';
|
||||||
const injectLogger = logger.child(`inject`);
|
const injectLogger = logger.child(`inject`);
|
||||||
injectLogger.log(`isInternal: ${isInternal}`);
|
injectLogger.log(`isInternal: ${isInternal}`);
|
||||||
|
injectLogger.log(`isPipeline: ${isPipeline}`);
|
||||||
injectLogger.log(`isRetain: ${isRetain}`);
|
injectLogger.log(`isRetain: ${isRetain}`);
|
||||||
|
|
||||||
|
// For pipeline agents, they should continue pipeline execution, so no need to inject give up control
|
||||||
|
if (isPipeline) {
|
||||||
|
injectLogger.log(`Pipeline agent ${childAgentName} continues pipeline execution, no give up control needed`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isInternal && isRetain) {
|
if (!isInternal && isRetain) {
|
||||||
// inject give up control instructions
|
// inject give up control instructions
|
||||||
agents[childAgentName].instructions = getGiveUpControlInstructions(agents[childAgentName], parentAgentName, injectLogger);
|
agents[childAgentName].instructions = getGiveUpControlInstructions(agents[childAgentName], parentAgentName, injectLogger);
|
||||||
|
|
@ -926,6 +1044,64 @@ function maybeInjectGiveUpControlInstructions(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pipeline controller function to handle pipeline agent execution and transfers
|
||||||
|
function handlePipelineAgentExecution(
|
||||||
|
currentAgent: Agent,
|
||||||
|
currentAgentName: string,
|
||||||
|
pipelineConfig: Record<string, z.infer<typeof WorkflowPipeline>>,
|
||||||
|
stack: string[],
|
||||||
|
logger: PrefixLogger,
|
||||||
|
turnMsgs: z.infer<typeof Message>[],
|
||||||
|
transferCounter: AgentTransferCounter,
|
||||||
|
createTransferEvents: (fromAgent: string, toAgent: string) => [z.infer<typeof AssistantMessageWithToolCalls>, z.infer<typeof ToolMessage>]
|
||||||
|
): { nextAgentName: string | null; shouldContinue: boolean; transferEvents?: [z.infer<typeof AssistantMessageWithToolCalls>, z.infer<typeof ToolMessage>] } {
|
||||||
|
const pipelineName = (currentAgent as any).pipelineName;
|
||||||
|
const pipelineIndex = (currentAgent as any).pipelineIndex;
|
||||||
|
const isLastInPipeline = (currentAgent as any).isLastInPipeline;
|
||||||
|
|
||||||
|
if (!pipelineName || pipelineIndex === undefined) {
|
||||||
|
logger.log(`warning: pipeline agent ${currentAgentName} missing pipeline metadata`);
|
||||||
|
return { nextAgentName: null, shouldContinue: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const pipeline = pipelineConfig[pipelineName];
|
||||||
|
if (!pipeline) {
|
||||||
|
logger.log(`warning: pipeline ${pipelineName} not found in config`);
|
||||||
|
return { nextAgentName: null, shouldContinue: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextAgentName: string | null = null;
|
||||||
|
|
||||||
|
if (!isLastInPipeline) {
|
||||||
|
// Not the last agent - continue to next agent in pipeline
|
||||||
|
nextAgentName = pipeline.agents[pipelineIndex + 1];
|
||||||
|
logger.log(`-- pipeline controller: ${currentAgentName} -> ${nextAgentName} (continuing pipeline ${pipelineName})`);
|
||||||
|
} else {
|
||||||
|
// Last agent - return to calling agent
|
||||||
|
nextAgentName = stack.pop()!;
|
||||||
|
logger.log(`-- pipeline controller: ${currentAgentName} -> ${nextAgentName} (pipeline ${pipelineName} complete, returning to caller)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextAgentName) {
|
||||||
|
// Create transfer events for pipeline continuation
|
||||||
|
const transferEvents = createTransferEvents(currentAgentName, nextAgentName);
|
||||||
|
const [transferStart, transferComplete] = transferEvents;
|
||||||
|
|
||||||
|
// Add messages to turn
|
||||||
|
turnMsgs.push(transferStart);
|
||||||
|
turnMsgs.push(transferComplete);
|
||||||
|
|
||||||
|
// Update transfer counter
|
||||||
|
transferCounter.increment(currentAgentName, nextAgentName);
|
||||||
|
|
||||||
|
logger.log(`switched to agent: ${nextAgentName} || reason: pipeline controller transfer`);
|
||||||
|
|
||||||
|
return { nextAgentName, shouldContinue: true, transferEvents };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { nextAgentName: null, shouldContinue: false };
|
||||||
|
}
|
||||||
|
|
||||||
// Main function to stream an agentic response
|
// Main function to stream an agentic response
|
||||||
// using OpenAI Agents SDK
|
// using OpenAI Agents SDK
|
||||||
export async function* streamResponse(
|
export async function* streamResponse(
|
||||||
|
|
@ -949,8 +1125,16 @@ export async function* streamResponse(
|
||||||
}
|
}
|
||||||
|
|
||||||
// create map of agent, tool and prompt configs
|
// create map of agent, tool and prompt configs
|
||||||
const { agentConfig, toolConfig, promptConfig } = mapConfig(workflow);
|
const { agentConfig, toolConfig, promptConfig, pipelineConfig } = mapConfig(workflow);
|
||||||
|
|
||||||
|
// Debug: Log configuration summary
|
||||||
|
logger.log(`=== WORKFLOW CONFIGURATION ===`);
|
||||||
|
logger.log(`agents: ${Object.keys(agentConfig).length} (${Object.keys(agentConfig).join(', ')})`);
|
||||||
|
logger.log(`tools: ${Object.keys(toolConfig).length} (${Object.keys(toolConfig).join(', ')})`);
|
||||||
|
logger.log(`prompts: ${Object.keys(promptConfig).length} (${Object.keys(promptConfig).join(', ')})`);
|
||||||
|
logger.log(`pipelines: ${Object.keys(pipelineConfig).length} (${Object.keys(pipelineConfig).join(', ')})`);
|
||||||
|
logger.log(`start agent: ${workflow.startAgent}`);
|
||||||
|
logger.log(`=== END CONFIGURATION ===`);
|
||||||
|
|
||||||
const stack: string[] = [];
|
const stack: string[] = [];
|
||||||
logger.log(`initialized stack: ${JSON.stringify(stack)}`);
|
logger.log(`initialized stack: ${JSON.stringify(stack)}`);
|
||||||
|
|
@ -959,35 +1143,45 @@ export async function* streamResponse(
|
||||||
const tools = createTools(logger, projectId, workflow, toolConfig);
|
const tools = createTools(logger, projectId, workflow, toolConfig);
|
||||||
|
|
||||||
// create agents
|
// create agents
|
||||||
const { agents, originalInstructions, originalHandoffs } = createAgents(logger, projectId, workflow, agentConfig, tools, promptConfig);
|
const { agents, originalInstructions, originalHandoffs } = createAgents(logger, projectId, workflow, agentConfig, tools, promptConfig, pipelineConfig);
|
||||||
|
|
||||||
// track agent to agent calls
|
// track agent to agent calls
|
||||||
const transferCounter = new AgentTransferCounter();
|
const transferCounter = new AgentTransferCounter();
|
||||||
|
|
||||||
// track usage
|
// get the agent that should be starting this turn
|
||||||
|
const startOfTurnAgentName = getStartOfTurnAgentName(logger, messages, agentConfig, workflow);
|
||||||
|
logger.log(`🎯 START AGENT DECISION: ${startOfTurnAgentName}`);
|
||||||
|
|
||||||
|
let agentName = startOfTurnAgentName;
|
||||||
|
|
||||||
|
// start the turn loop
|
||||||
const usageTracker = new UsageTracker();
|
const usageTracker = new UsageTracker();
|
||||||
|
|
||||||
// get next agent name
|
|
||||||
let agentName = getStartOfTurnAgentName(logger, messages, agentConfig, workflow);
|
|
||||||
|
|
||||||
// set up initial state for loop
|
|
||||||
logger.log('@@ starting agent turn @@');
|
|
||||||
let iter = 0;
|
|
||||||
const turnMsgs: z.infer<typeof Message>[] = [...messages];
|
const turnMsgs: z.infer<typeof Message>[] = [...messages];
|
||||||
|
|
||||||
|
logger.log('🎬 STARTING AGENT TURN');
|
||||||
|
|
||||||
|
// stack-based agent execution loop
|
||||||
|
let iter = 0;
|
||||||
|
const MAXTURNITERATIONS = 10;
|
||||||
|
|
||||||
// loop indefinitely
|
// loop indefinitely
|
||||||
turnLoop: while (true) {
|
turnLoop: while (true) {
|
||||||
|
|
||||||
logger.log(`starting turn loop iteration: ${iter}`);
|
logger.log(`🔄 TURN ITERATION: ${iter + 1}/${MAXTURNITERATIONS}`);
|
||||||
|
const loopLogger = logger.child(`iter-${iter + 1}`);
|
||||||
|
|
||||||
|
loopLogger.log(`🤖 CURRENT AGENT: ${agentName}`);
|
||||||
|
loopLogger.log(`📚 AGENT STACK: [${stack.join(' -> ')}]`);
|
||||||
|
|
||||||
// increment loop counter
|
// increment loop counter
|
||||||
iter++;
|
iter++;
|
||||||
|
|
||||||
// set up logging
|
// set up logging
|
||||||
const loopLogger = logger.child(`iter-${iter}`);
|
// const loopLogger = logger.child(`iter-${iter}`);
|
||||||
|
|
||||||
// log agent info
|
// log agent info
|
||||||
loopLogger.log(`agent name: ${agentName}`);
|
// loopLogger.log(`agent name: ${agentName}`);
|
||||||
loopLogger.log(`stack: ${JSON.stringify(stack)}`);
|
// loopLogger.log(`stack: ${JSON.stringify(stack)}`);
|
||||||
if (!agents[agentName]) {
|
if (!agents[agentName]) {
|
||||||
throw new Error(`agent not found in agent config!`);
|
throw new Error(`agent not found in agent config!`);
|
||||||
}
|
}
|
||||||
|
|
@ -1050,9 +1244,11 @@ export async function* streamResponse(
|
||||||
case 'run_item_stream_event':
|
case 'run_item_stream_event':
|
||||||
// handle handoff event
|
// handle handoff event
|
||||||
if (event.name === 'handoff_occurred' && event.item.type === 'handoff_output_item') {
|
if (event.name === 'handoff_occurred' && event.item.type === 'handoff_output_item') {
|
||||||
|
eventLogger.log(`🔄 HANDOFF EVENT: ${agentName} -> ${event.item.targetAgent.name}`);
|
||||||
|
|
||||||
// skip if its the same agent
|
// skip if its the same agent
|
||||||
if (agentName === event.item.targetAgent.name) {
|
if (agentName === event.item.targetAgent.name) {
|
||||||
eventLogger.log(`skipping handoff to same agent: ${agentName}`);
|
eventLogger.log(`⚠️ SKIPPING: handoff to same agent: ${agentName}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1062,9 +1258,10 @@ export async function* streamResponse(
|
||||||
const maxCalls = targetAgentConfig?.maxCallsPerParentAgent || 3;
|
const maxCalls = targetAgentConfig?.maxCallsPerParentAgent || 3;
|
||||||
const currentCalls = transferCounter.get(agentName, event.item.targetAgent.name);
|
const currentCalls = transferCounter.get(agentName, event.item.targetAgent.name);
|
||||||
if (currentCalls >= maxCalls) {
|
if (currentCalls >= maxCalls) {
|
||||||
eventLogger.log(`skipping handoff to ${event.item.targetAgent.name} || reason: max calls ${maxCalls} exceeded from ${agentName} to internal agent ${event.item.targetAgent.name}`);
|
eventLogger.log(`⚠️ SKIPPING: handoff to ${event.item.targetAgent.name} - max calls ${maxCalls} exceeded from ${agentName}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
eventLogger.log(`📊 TRANSFER COUNT: ${agentName} -> ${event.item.targetAgent.name} = ${currentCalls}/${maxCalls}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// inject give up control instructions if needed (parent handing off to child)
|
// inject give up control instructions if needed (parent handing off to child)
|
||||||
|
|
@ -1094,13 +1291,14 @@ export async function* streamResponse(
|
||||||
|
|
||||||
const newAgentName = event.item.targetAgent.name;
|
const newAgentName = event.item.targetAgent.name;
|
||||||
|
|
||||||
loopLogger.log(`switched to agent: ${newAgentName} || reason: handoff by ${agentName}`);
|
loopLogger.log(`🔄 AGENT SWITCH: ${agentName} -> ${newAgentName} (reason: handoff)`);
|
||||||
|
|
||||||
// add current agent to stack only if new agent is internal
|
// add current agent to stack only if new agent is internal
|
||||||
if (agentConfig[newAgentName]?.outputVisibility === 'internal') {
|
const newAgentConfig = agentConfig[newAgentName];
|
||||||
|
if (newAgentConfig?.outputVisibility === 'internal' || newAgentConfig?.type === 'pipeline') {
|
||||||
stack.push(agentName);
|
stack.push(agentName);
|
||||||
loopLogger.log(`-- pushed agent to stack: ${agentName} || reason: new agent ${newAgentName} is internal`);
|
loopLogger.log(`📚 STACK PUSH: ${agentName} (new agent ${newAgentName} is internal/pipeline)`);
|
||||||
loopLogger.log(`-- stack is now: ${JSON.stringify(stack)}`);
|
loopLogger.log(`📚 STACK NOW: [${stack.join(' -> ')}]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// set this as the new agent name
|
// set this as the new agent name
|
||||||
|
|
@ -1132,7 +1330,8 @@ export async function* streamResponse(
|
||||||
event.item.rawItem.type === 'message' &&
|
event.item.rawItem.type === 'message' &&
|
||||||
event.item.rawItem.status === 'completed') {
|
event.item.rawItem.status === 'completed') {
|
||||||
// check response visibility
|
// check response visibility
|
||||||
const isInternal = agentConfig[agentName]?.outputVisibility === 'internal';
|
const agentConfigObj = agentConfig[agentName];
|
||||||
|
const isInternal = agentConfigObj?.outputVisibility === 'internal' || agentConfigObj?.type === 'pipeline';
|
||||||
for (const content of event.item.rawItem.content) {
|
for (const content of event.item.rawItem.content) {
|
||||||
if (content.type === 'output_text') {
|
if (content.type === 'output_text') {
|
||||||
// create message
|
// create message
|
||||||
|
|
@ -1151,40 +1350,71 @@ export async function* streamResponse(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if this is an internal agent, switch to previous agent
|
// if this is an internal agent or pipeline agent, switch to previous agent
|
||||||
if (isInternal) {
|
if (isInternal) {
|
||||||
const current = agentName;
|
const current = agentName;
|
||||||
|
const currentAgentConfig = agentConfig[agentName];
|
||||||
|
|
||||||
// if the control type is relinquish_to_parent or retain, we need to pop the stack, else if the control type is relinquish_to_start, we need to use the start agent
|
// Check if this is a pipeline agent that needs to continue the pipeline
|
||||||
if (agentConfig[agentName]?.controlType === 'relinquish_to_parent' || agentConfig[agentName]?.controlType === 'retain') {
|
if (currentAgentConfig?.type === 'pipeline') {
|
||||||
|
const result = handlePipelineAgentExecution(
|
||||||
|
agents[current], // Use the correct agent from agents collection
|
||||||
|
current,
|
||||||
|
pipelineConfig,
|
||||||
|
stack,
|
||||||
|
loopLogger,
|
||||||
|
turnMsgs,
|
||||||
|
transferCounter,
|
||||||
|
createTransferEvents
|
||||||
|
);
|
||||||
|
|
||||||
|
// Emit transfer events if they exist
|
||||||
|
if (result.transferEvents) {
|
||||||
|
const [transferStart, transferComplete] = result.transferEvents;
|
||||||
|
yield* emitEvent(eventLogger, transferStart);
|
||||||
|
yield* emitEvent(eventLogger, transferComplete);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.shouldContinue) {
|
||||||
|
agentName = result.nextAgentName!;
|
||||||
|
// Run the turn from the next agent
|
||||||
|
continue turnLoop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check control type to determine next action for non-pipeline agents
|
||||||
|
if (currentAgentConfig?.controlType === 'relinquish_to_parent' || currentAgentConfig?.controlType === 'retain') {
|
||||||
agentName = stack.pop()!;
|
agentName = stack.pop()!;
|
||||||
loopLogger.log(`-- popped agent from stack: ${agentName} || reason: ${current} is an internal agent, it put out a message and it has a control type of ${agentConfig[agentName]?.controlType}, hence the flow of control needs to return to the previous agent`);
|
loopLogger.log(`-- popped agent from stack: ${agentName} || reason: ${current} is an internal agent, it put out a message and it has a control type of ${currentAgentConfig?.controlType}, hence the flow of control needs to return to the previous agent`);
|
||||||
} else if (agentConfig[agentName]?.controlType === 'relinquish_to_start') {
|
} else if (currentAgentConfig?.controlType === 'relinquish_to_start') {
|
||||||
agentName = workflow.startAgent;
|
agentName = workflow.startAgent;
|
||||||
loopLogger.log(`-- using start agent: ${agentName} || reason: ${current} is an internal agent, it put out a message and it has a control type of ${agentConfig[agentName]?.controlType}, hence the flow of control needs to return to the start agent`);
|
loopLogger.log(`-- using start agent: ${agentName} || reason: ${current} is an internal agent, it put out a message and it has a control type of ${currentAgentConfig?.controlType}, hence the flow of control needs to return to the start agent`);
|
||||||
}
|
}
|
||||||
|
|
||||||
loopLogger.log(`-- stack is now: ${JSON.stringify(stack)}`);
|
// Only emit transfer events if we're actually changing agents
|
||||||
|
if (agentName !== current) {
|
||||||
|
loopLogger.log(`-- stack is now: ${JSON.stringify(stack)}`);
|
||||||
|
|
||||||
// emit transfer tool call invocation
|
// emit transfer tool call invocation
|
||||||
const [transferStart, transferComplete] = createTransferEvents(current, agentName);
|
const [transferStart, transferComplete] = createTransferEvents(current, agentName);
|
||||||
|
|
||||||
// add messages to turn
|
// add messages to turn
|
||||||
turnMsgs.push(transferStart);
|
turnMsgs.push(transferStart);
|
||||||
turnMsgs.push(transferComplete);
|
turnMsgs.push(transferComplete);
|
||||||
|
|
||||||
// emit events
|
// emit events
|
||||||
yield* emitEvent(eventLogger, transferStart);
|
yield* emitEvent(eventLogger, transferStart);
|
||||||
yield* emitEvent(eventLogger, transferComplete);
|
yield* emitEvent(eventLogger, transferComplete);
|
||||||
|
|
||||||
// update transfer counter
|
// update transfer counter
|
||||||
transferCounter.increment(current, agentName);
|
transferCounter.increment(current, agentName);
|
||||||
|
|
||||||
// set this as the new agent name
|
// set this as the new agent name
|
||||||
loopLogger.log(`switched to agent: ${agentName} || reason: internal agent (${current}) put out a message`);
|
loopLogger.log(`switched to agent: ${agentName} || reason: internal agent (${current}) put out a message`);
|
||||||
|
|
||||||
// run the turn from the previous agent
|
// run the turn from the previous agent
|
||||||
continue turnLoop;
|
continue turnLoop;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,29 +8,52 @@ interface CreateAtMentionsProps {
|
||||||
agents: any[];
|
agents: any[];
|
||||||
prompts: any[];
|
prompts: any[];
|
||||||
tools: any[];
|
tools: any[];
|
||||||
|
pipelines?: any[];
|
||||||
currentAgentName?: string;
|
currentAgentName?: string;
|
||||||
|
currentAgent?: any; // Add current agent object to know its outputVisibility
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createAtMentions({ agents, prompts, tools, currentAgentName }: CreateAtMentionsProps): AtMentionItem[] {
|
export function createAtMentions({ agents, prompts, tools, pipelines = [], currentAgentName, currentAgent }: CreateAtMentionsProps): AtMentionItem[] {
|
||||||
const atMentions: AtMentionItem[] = [];
|
const atMentions: AtMentionItem[] = [];
|
||||||
|
|
||||||
// Add agents
|
// For pipeline agents, only add tools and prompts - no agents or pipelines
|
||||||
for (const a of agents) {
|
const isCurrentAgentPipeline = currentAgent?.type === 'pipeline';
|
||||||
if (a.disabled || a.name === currentAgentName) {
|
|
||||||
continue;
|
// Add agents (excluding pipeline agents and disabled agents)
|
||||||
|
// Also exclude ALL agents if current agent is a pipeline agent
|
||||||
|
if (!isCurrentAgentPipeline) {
|
||||||
|
for (const a of agents) {
|
||||||
|
if (a.disabled || a.name === currentAgentName || a.type === 'pipeline') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const id = `agent:${a.name}`;
|
||||||
|
atMentions.push({
|
||||||
|
id,
|
||||||
|
value: id,
|
||||||
|
label: `Agent: ${a.name}`,
|
||||||
|
denotationChar: "@", // Add required properties for Match type
|
||||||
|
link: id,
|
||||||
|
target: "_self"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const id = `agent:${a.name}`;
|
|
||||||
atMentions.push({
|
|
||||||
id,
|
|
||||||
value: id,
|
|
||||||
label: `Agent: ${a.name}`,
|
|
||||||
denotationChar: "@", // Add required properties for Match type
|
|
||||||
link: id,
|
|
||||||
target: "_self"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add prompts
|
// Add pipelines (only if current agent is not a pipeline agent)
|
||||||
|
if (!isCurrentAgentPipeline) {
|
||||||
|
for (const pipeline of pipelines) {
|
||||||
|
const id = `pipeline:${pipeline.name}`;
|
||||||
|
atMentions.push({
|
||||||
|
id,
|
||||||
|
value: id,
|
||||||
|
label: `Pipeline: ${pipeline.name}`,
|
||||||
|
denotationChar: "@",
|
||||||
|
link: id,
|
||||||
|
target: "_self"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add prompts (always allowed)
|
||||||
for (const prompt of prompts) {
|
for (const prompt of prompts) {
|
||||||
const id = `prompt:${prompt.name}`;
|
const id = `prompt:${prompt.name}`;
|
||||||
atMentions.push({
|
atMentions.push({
|
||||||
|
|
@ -43,7 +66,7 @@ export function createAtMentions({ agents, prompts, tools, currentAgentName }: C
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add tools
|
// Add tools (always allowed)
|
||||||
for (const tool of tools) {
|
for (const tool of tools) {
|
||||||
const id = `tool:${tool.name}`;
|
const id = `tool:${tool.name}`;
|
||||||
atMentions.push({
|
atMentions.push({
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ export const WorkflowAgent = z.object({
|
||||||
'conversation',
|
'conversation',
|
||||||
'post_process',
|
'post_process',
|
||||||
'escalation',
|
'escalation',
|
||||||
|
'pipeline',
|
||||||
]),
|
]),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
disabled: z.boolean().default(false).optional(),
|
disabled: z.boolean().default(false).optional(),
|
||||||
|
|
@ -23,8 +24,32 @@ export const WorkflowAgent = z.object({
|
||||||
'retain',
|
'retain',
|
||||||
'relinquish_to_parent',
|
'relinquish_to_parent',
|
||||||
'relinquish_to_start',
|
'relinquish_to_start',
|
||||||
]).default('retain').describe('Whether this agent retains control after a turn, relinquishes to the parent agent, or relinquishes to the start agent'),
|
]).optional().describe('Whether this agent retains control after a turn, relinquishes to the parent agent, or relinquishes to the start agent'),
|
||||||
maxCallsPerParentAgent: z.number().default(3).describe('Maximum number of times this agent can be called by a parent agent in a single turn').optional(),
|
maxCallsPerParentAgent: z.number().default(3).describe('Maximum number of times this agent can be called by a parent agent in a single turn').optional(),
|
||||||
|
}).refine((data) => {
|
||||||
|
// Pipeline agents should have internal output visibility and relinquish_to_parent control type
|
||||||
|
if (data.type === 'pipeline' && data.outputVisibility !== 'internal') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (data.type === 'pipeline' && data.controlType !== 'relinquish_to_parent') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Internal agents should have relinquish_to_parent control type
|
||||||
|
if (data.outputVisibility === 'internal' && data.controlType !== 'relinquish_to_parent') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// User-facing agents should not have relinquish_to_parent control type
|
||||||
|
if (data.outputVisibility === 'user_facing' && data.controlType === 'relinquish_to_parent') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// All agents should have a control type
|
||||||
|
if (data.controlType === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}, {
|
||||||
|
message: "Pipeline agents must have 'internal' output visibility and 'relinquish_to_parent' control type, while other agents must have appropriate control types",
|
||||||
|
path: ["controlType", "outputVisibility"]
|
||||||
});
|
});
|
||||||
export const WorkflowPrompt = z.object({
|
export const WorkflowPrompt = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
|
@ -58,10 +83,19 @@ export const WorkflowTool = z.object({
|
||||||
logo: z.string(), // the logo for the Composio tool
|
logo: z.string(), // the logo for the Composio tool
|
||||||
}).optional(), // the data for the Composio tool, if it is a Composio tool
|
}).optional(), // the data for the Composio tool, if it is a Composio tool
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const WorkflowPipeline = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
agents: z.array(z.string()), // ordered list of agent names in the pipeline
|
||||||
|
order: z.number().int().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const Workflow = z.object({
|
export const Workflow = z.object({
|
||||||
agents: z.array(WorkflowAgent),
|
agents: z.array(WorkflowAgent),
|
||||||
prompts: z.array(WorkflowPrompt),
|
prompts: z.array(WorkflowPrompt),
|
||||||
tools: z.array(WorkflowTool),
|
tools: z.array(WorkflowTool),
|
||||||
|
pipelines: z.array(WorkflowPipeline).optional(),
|
||||||
startAgent: z.string(),
|
startAgent: z.string(),
|
||||||
lastUpdatedAt: z.string().datetime(),
|
lastUpdatedAt: z.string().datetime(),
|
||||||
mockTools: z.record(z.string(), z.string()).optional(), // a dict of toolName => mockInstructions
|
mockTools: z.record(z.string(), z.string()).optional(), // a dict of toolName => mockInstructions
|
||||||
|
|
@ -76,7 +110,7 @@ export const WorkflowTemplate = Workflow
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ConnectedEntity = z.object({
|
export const ConnectedEntity = z.object({
|
||||||
type: z.enum(['tool', 'prompt', 'agent']),
|
type: z.enum(['tool', 'prompt', 'agent', 'pipeline']),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -86,13 +120,15 @@ export function sanitizeTextWithMentions(
|
||||||
agents: z.infer<typeof WorkflowAgent>[],
|
agents: z.infer<typeof WorkflowAgent>[],
|
||||||
tools: z.infer<typeof WorkflowTool>[],
|
tools: z.infer<typeof WorkflowTool>[],
|
||||||
prompts: z.infer<typeof WorkflowPrompt>[],
|
prompts: z.infer<typeof WorkflowPrompt>[],
|
||||||
|
pipelines?: z.infer<typeof WorkflowPipeline>[],
|
||||||
},
|
},
|
||||||
|
currentAgent?: z.infer<typeof WorkflowAgent>,
|
||||||
): {
|
): {
|
||||||
sanitized: string;
|
sanitized: string;
|
||||||
entities: z.infer<typeof ConnectedEntity>[];
|
entities: z.infer<typeof ConnectedEntity>[];
|
||||||
} {
|
} {
|
||||||
// Regex to match [@type:name](#type:something) pattern where type is tool/prompt/agent
|
// Regex to match [@type:name](#type:something) pattern where type is tool/prompt/agent/pipeline
|
||||||
const mentionRegex = /\[@(tool|prompt|agent):([^\]]+)\]\(#mention\)/g;
|
const mentionRegex = /\[@(tool|prompt|agent|pipeline):([^\]]+)\]\(#mention\)/g;
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
|
|
||||||
// collect entities
|
// collect entities
|
||||||
|
|
@ -107,18 +143,28 @@ export function sanitizeTextWithMentions(
|
||||||
})
|
})
|
||||||
.map(match => {
|
.map(match => {
|
||||||
return {
|
return {
|
||||||
type: match[1] as 'tool' | 'prompt' | 'agent',
|
type: match[1] as 'tool' | 'prompt' | 'agent' | 'pipeline',
|
||||||
name: match[2],
|
name: match[2],
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter(entity => {
|
.filter(entity => {
|
||||||
seen.add(entity.name);
|
seen.add(entity.name);
|
||||||
|
|
||||||
|
// For pipeline agents, only allow tool and prompt mentions
|
||||||
|
if (currentAgent?.type === 'pipeline') {
|
||||||
|
return entity.type === 'tool' || entity.type === 'prompt';
|
||||||
|
}
|
||||||
|
|
||||||
if (entity.type === 'agent') {
|
if (entity.type === 'agent') {
|
||||||
return workflow.agents.some(a => a.name === entity.name);
|
// Filter out pipeline agents - they should not be @ referenceable
|
||||||
|
const agent = workflow.agents.find(a => a.name === entity.name);
|
||||||
|
return agent && agent.type !== 'pipeline';
|
||||||
} else if (entity.type === 'tool') {
|
} else if (entity.type === 'tool') {
|
||||||
return workflow.tools.some(t => t.name === entity.name);
|
return workflow.tools.some(t => t.name === entity.name);
|
||||||
} else if (entity.type === 'prompt') {
|
} else if (entity.type === 'prompt') {
|
||||||
return workflow.prompts.some(p => p.name === entity.name);
|
return workflow.prompts.some(p => p.name === entity.name);
|
||||||
|
} else if (entity.type === 'pipeline') {
|
||||||
|
return workflow.pipelines?.some(p => p.name === entity.name);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { WithStringId } from "../../../lib/types/types";
|
||||||
import { WorkflowPrompt, WorkflowAgent, Workflow, WorkflowTool } from "../../../lib/types/workflow_types";
|
import { WorkflowPrompt, WorkflowAgent, Workflow, WorkflowTool } from "../../../lib/types/workflow_types";
|
||||||
import { DataSource } from "../../../lib/types/datasource_types";
|
import { DataSource } from "../../../lib/types/datasource_types";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { PlusIcon, Sparkles, X as XIcon, ChevronDown, ChevronRight, Trash2, Maximize2, Minimize2, StarIcon, DatabaseIcon, UserIcon, Settings } from "lucide-react";
|
import { PlusIcon, Sparkles, X as XIcon, ChevronDown, ChevronRight, Trash2, Maximize2, Minimize2, StarIcon, DatabaseIcon, UserIcon, Settings, Info } from "lucide-react";
|
||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { usePreviewModal } from "../workflow/preview-modal";
|
import { usePreviewModal } from "../workflow/preview-modal";
|
||||||
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Select, SelectItem, Chip, SelectSection } from "@heroui/react";
|
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Select, SelectItem, Chip, SelectSection } from "@heroui/react";
|
||||||
|
|
@ -19,7 +19,7 @@ import clsx from "clsx";
|
||||||
import { InputField } from "@/app/lib/components/input-field";
|
import { InputField } from "@/app/lib/components/input-field";
|
||||||
import { USE_TRANSFER_CONTROL_OPTIONS } from "@/app/lib/feature_flags";
|
import { USE_TRANSFER_CONTROL_OPTIONS } from "@/app/lib/feature_flags";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Info } from "lucide-react";
|
import { Info as InfoIcon } from "lucide-react";
|
||||||
import { useCopilot } from "../copilot/use-copilot";
|
import { useCopilot } from "../copilot/use-copilot";
|
||||||
import { BillingUpgradeModal } from "@/components/common/billing-upgrade-modal";
|
import { BillingUpgradeModal } from "@/components/common/billing-upgrade-modal";
|
||||||
import { ModelsResponse } from "@/app/lib/types/billing_types";
|
import { ModelsResponse } from "@/app/lib/types/billing_types";
|
||||||
|
|
@ -39,6 +39,7 @@ export function AgentConfig({
|
||||||
workflow,
|
workflow,
|
||||||
agent,
|
agent,
|
||||||
usedAgentNames,
|
usedAgentNames,
|
||||||
|
usedPipelineNames,
|
||||||
agents,
|
agents,
|
||||||
tools,
|
tools,
|
||||||
prompts,
|
prompts,
|
||||||
|
|
@ -54,6 +55,7 @@ export function AgentConfig({
|
||||||
workflow: z.infer<typeof Workflow>,
|
workflow: z.infer<typeof Workflow>,
|
||||||
agent: z.infer<typeof WorkflowAgent>,
|
agent: z.infer<typeof WorkflowAgent>,
|
||||||
usedAgentNames: Set<string>,
|
usedAgentNames: Set<string>,
|
||||||
|
usedPipelineNames: Set<string>,
|
||||||
agents: z.infer<typeof WorkflowAgent>[],
|
agents: z.infer<typeof WorkflowAgent>[],
|
||||||
tools: z.infer<typeof WorkflowTool>[],
|
tools: z.infer<typeof WorkflowTool>[],
|
||||||
prompts: z.infer<typeof WorkflowPrompt>[],
|
prompts: z.infer<typeof WorkflowPrompt>[],
|
||||||
|
|
@ -78,6 +80,9 @@ export function AgentConfig({
|
||||||
const [billingError, setBillingError] = useState<string | null>(null);
|
const [billingError, setBillingError] = useState<string | null>(null);
|
||||||
const [showSavedBanner, setShowSavedBanner] = useState(false);
|
const [showSavedBanner, setShowSavedBanner] = useState(false);
|
||||||
|
|
||||||
|
// Check if this agent is a pipeline agent
|
||||||
|
const isPipelineAgent = agent.type === 'pipeline';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
start: startCopilotChat,
|
start: startCopilotChat,
|
||||||
} = useCopilot({
|
} = useCopilot({
|
||||||
|
|
@ -117,14 +122,31 @@ export function AgentConfig({
|
||||||
setShowRagCta(false);
|
setShowRagCta(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add effect to handle control type update when transfer control is disabled or when internal agents have invalid control type
|
// Add effect to handle control type update to ensure agents have correct control types
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!USE_TRANSFER_CONTROL_OPTIONS && agent.controlType !== 'retain') {
|
let correctControlType: "retain" | "relinquish_to_parent" | "relinquish_to_start" | undefined = undefined;
|
||||||
handleUpdate({ ...agent, controlType: 'retain' });
|
|
||||||
|
// Determine the correct control type based on agent type and output visibility
|
||||||
|
if (agent.type === "pipeline") {
|
||||||
|
correctControlType = "relinquish_to_parent";
|
||||||
|
} else if (agent.outputVisibility === "internal") {
|
||||||
|
correctControlType = "relinquish_to_parent";
|
||||||
|
} else if (agent.outputVisibility === "user_facing") {
|
||||||
|
correctControlType = "retain";
|
||||||
}
|
}
|
||||||
// For internal agents, "retain" is not a valid option, so change it to "relinquish_to_parent"
|
|
||||||
if (agent.outputVisibility === "internal" && agent.controlType === 'retain') {
|
// Handle undefined control type
|
||||||
handleUpdate({ ...agent, controlType: 'relinquish_to_parent' });
|
if (agent.controlType === undefined) {
|
||||||
|
if (agent.outputVisibility === "user_facing") {
|
||||||
|
correctControlType = "retain";
|
||||||
|
} else {
|
||||||
|
correctControlType = "relinquish_to_parent";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update if the control type is incorrect
|
||||||
|
if (correctControlType && agent.controlType !== correctControlType) {
|
||||||
|
handleUpdate({ ...agent, controlType: correctControlType });
|
||||||
}
|
}
|
||||||
}, [agent.controlType, agent.outputVisibility, agent, handleUpdate]);
|
}, [agent.controlType, agent.outputVisibility, agent, handleUpdate]);
|
||||||
|
|
||||||
|
|
@ -151,7 +173,12 @@ export function AgentConfig({
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (value !== agent.name && usedAgentNames.has(value)) {
|
if (value !== agent.name && usedAgentNames.has(value)) {
|
||||||
setNameError("This name is already taken");
|
setNameError("This name is already taken by another agent");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Check for conflicts with pipeline names
|
||||||
|
if (usedPipelineNames.has(value)) {
|
||||||
|
setNameError("This name is already taken by a pipeline");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!/^[a-zA-Z0-9_-\s]+$/.test(value)) {
|
if (!/^[a-zA-Z0-9_-\s]+$/.test(value)) {
|
||||||
|
|
@ -175,10 +202,12 @@ export function AgentConfig({
|
||||||
};
|
};
|
||||||
|
|
||||||
const atMentions = createAtMentions({
|
const atMentions = createAtMentions({
|
||||||
agents,
|
agents: agents,
|
||||||
prompts,
|
prompts,
|
||||||
tools,
|
tools,
|
||||||
currentAgentName: agent.name
|
pipelines: agent.type === "pipeline" ? [] : (workflow.pipelines || []), // Pipeline agents can't reference pipelines
|
||||||
|
currentAgentName: agent.name,
|
||||||
|
currentAgent: agent
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add local state for max calls input
|
// Add local state for max calls input
|
||||||
|
|
@ -211,7 +240,7 @@ export function AgentConfig({
|
||||||
<div className="flex flex-col gap-6 p-4 h-[calc(100vh-100px)] min-h-0 flex-1">
|
<div className="flex flex-col gap-6 p-4 h-[calc(100vh-100px)] min-h-0 flex-1">
|
||||||
{/* Saved Banner */}
|
{/* Saved Banner */}
|
||||||
{showSavedBanner && (
|
{showSavedBanner && (
|
||||||
<div className="absolute top-4 right-4 z-10 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 animate-in slide-in-from-top-2 duration-300">
|
<div className="absolute top-4 left-4 z-10 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 animate-in slide-in-from-top-2 duration-300">
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -246,7 +275,7 @@ export function AgentConfig({
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
{/* Saved Banner for maximized instructions */}
|
{/* Saved Banner for maximized instructions */}
|
||||||
{showSavedBanner && (
|
{showSavedBanner && (
|
||||||
<div className="absolute top-4 right-4 z-10 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 animate-in slide-in-from-top-2 duration-300">
|
<div className="absolute top-4 left-4 z-10 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 animate-in slide-in-from-top-2 duration-300">
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -368,7 +397,7 @@ export function AgentConfig({
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
{/* Saved Banner for maximized examples */}
|
{/* Saved Banner for maximized examples */}
|
||||||
{showSavedBanner && (
|
{showSavedBanner && (
|
||||||
<div className="absolute top-4 right-4 z-10 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 animate-in slide-in-from-top-2 duration-300">
|
<div className="absolute top-4 left-4 z-10 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 animate-in slide-in-from-top-2 duration-300">
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -500,20 +529,30 @@ export function AgentConfig({
|
||||||
<div className="flex flex-col md:flex-row md:items-start gap-1 md:gap-0">
|
<div className="flex flex-col md:flex-row md:items-start gap-1 md:gap-0">
|
||||||
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 md:w-32 mb-1 md:mb-0 md:pr-4">Agent Type</label>
|
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 md:w-32 mb-1 md:mb-0 md:pr-4">Agent Type</label>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<CustomDropdown
|
{isPipelineAgent ? (
|
||||||
value={agent.outputVisibility}
|
// For pipeline agents, show read-only display
|
||||||
options={[
|
<div className="flex items-center gap-2 px-3 py-2 border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-750 rounded-lg">
|
||||||
{ key: "user_facing", label: "Conversation Agent" },
|
<span className="text-sm text-gray-900 dark:text-gray-100">
|
||||||
{ key: "internal", label: "Task Agent" }
|
Pipeline Agent
|
||||||
]}
|
</span>
|
||||||
onChange={(value) => {
|
</div>
|
||||||
handleUpdate({
|
) : (
|
||||||
...agent,
|
// For non-pipeline agents, show dropdown without pipeline option
|
||||||
outputVisibility: value as z.infer<typeof WorkflowAgent>["outputVisibility"]
|
<CustomDropdown
|
||||||
});
|
value={agent.outputVisibility}
|
||||||
showSavedMessage();
|
options={[
|
||||||
}}
|
{ key: "user_facing", label: "Conversation Agent" },
|
||||||
/>
|
{ key: "internal", label: "Task Agent" }
|
||||||
|
]}
|
||||||
|
onChange={(value) => {
|
||||||
|
handleUpdate({
|
||||||
|
...agent,
|
||||||
|
outputVisibility: value as z.infer<typeof WorkflowAgent>["outputVisibility"]
|
||||||
|
});
|
||||||
|
showSavedMessage();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col md:flex-row md:items-start gap-1 md:gap-0">
|
<div className="flex flex-col md:flex-row md:items-start gap-1 md:gap-0">
|
||||||
|
|
@ -585,7 +624,7 @@ export function AgentConfig({
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{agent.outputVisibility === "internal" && (
|
{agent.outputVisibility === "internal" && !isPipelineAgent && (
|
||||||
<div className="flex flex-col md:flex-row md:items-start gap-1 md:gap-0">
|
<div className="flex flex-col md:flex-row md:items-start gap-1 md:gap-0">
|
||||||
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 md:w-32 mb-1 md:mb-0 md:pr-4">Max Calls From Parent</label>
|
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 md:w-32 mb-1 md:mb-0 md:pr-4">Max Calls From Parent</label>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
|
@ -622,14 +661,18 @@ export function AgentConfig({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{USE_TRANSFER_CONTROL_OPTIONS && (
|
{USE_TRANSFER_CONTROL_OPTIONS && !isPipelineAgent && (
|
||||||
<div className="flex flex-col md:flex-row md:items-start gap-1 md:gap-0">
|
<div className="flex flex-col md:flex-row md:items-start gap-1 md:gap-0">
|
||||||
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 md:w-32 mb-1 md:mb-0 md:pr-4">After Turn</label>
|
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 md:w-32 mb-1 md:mb-0 md:pr-4">After Turn</label>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<CustomDropdown
|
<CustomDropdown
|
||||||
value={agent.controlType}
|
value={agent.controlType || 'retain'}
|
||||||
options={
|
options={
|
||||||
agent.outputVisibility === "internal"
|
agent.type === "pipeline"
|
||||||
|
? [
|
||||||
|
{ key: "relinquish_to_parent", label: "Relinquish to parent" }
|
||||||
|
]
|
||||||
|
: agent.outputVisibility === "internal"
|
||||||
? [
|
? [
|
||||||
{ key: "relinquish_to_parent", label: "Relinquish to parent" },
|
{ key: "relinquish_to_parent", label: "Relinquish to parent" },
|
||||||
{ key: "relinquish_to_start", label: "Relinquish to 'start' agent" }
|
{ key: "relinquish_to_start", label: "Relinquish to 'start' agent" }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,182 @@
|
||||||
|
"use client";
|
||||||
|
import { WorkflowPipeline, WorkflowAgent, Workflow } from "../../../lib/types/workflow_types";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { X as XIcon, Settings } from "lucide-react";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Panel } from "@/components/common/panel-common";
|
||||||
|
import { Button as CustomButton } from "@/components/ui/button";
|
||||||
|
import { InputField } from "@/app/lib/components/input-field";
|
||||||
|
import { SectionCard } from "@/components/common/section-card";
|
||||||
|
|
||||||
|
// Common section header styles
|
||||||
|
const sectionHeaderStyles = "block text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400";
|
||||||
|
|
||||||
|
export function PipelineConfig({
|
||||||
|
projectId,
|
||||||
|
workflow,
|
||||||
|
pipeline,
|
||||||
|
usedPipelineNames,
|
||||||
|
usedAgentNames,
|
||||||
|
agents,
|
||||||
|
pipelines,
|
||||||
|
handleUpdate,
|
||||||
|
handleClose,
|
||||||
|
}: {
|
||||||
|
projectId: string,
|
||||||
|
workflow: z.infer<typeof Workflow>,
|
||||||
|
pipeline: z.infer<typeof WorkflowPipeline>,
|
||||||
|
usedPipelineNames: Set<string>,
|
||||||
|
usedAgentNames: Set<string>,
|
||||||
|
agents: z.infer<typeof WorkflowAgent>[],
|
||||||
|
pipelines: z.infer<typeof WorkflowPipeline>[],
|
||||||
|
handleUpdate: (pipeline: z.infer<typeof WorkflowPipeline>) => void,
|
||||||
|
handleClose: () => void,
|
||||||
|
}) {
|
||||||
|
const [localName, setLocalName] = useState(pipeline.name);
|
||||||
|
const [nameError, setNameError] = useState<string | null>(null);
|
||||||
|
const [showSavedBanner, setShowSavedBanner] = useState(false);
|
||||||
|
|
||||||
|
// Function to show saved banner
|
||||||
|
const showSavedMessage = () => {
|
||||||
|
setShowSavedBanner(true);
|
||||||
|
setTimeout(() => setShowSavedBanner(false), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLocalName(pipeline.name);
|
||||||
|
}, [pipeline.name]);
|
||||||
|
|
||||||
|
const validateName = (value: string) => {
|
||||||
|
if (value.length === 0) {
|
||||||
|
setNameError("Name cannot be empty");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Check for conflicts with other pipeline names
|
||||||
|
if (value !== pipeline.name && usedPipelineNames.has(value)) {
|
||||||
|
setNameError("This name is already taken by another pipeline");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Check for conflicts with agent names
|
||||||
|
if (usedAgentNames.has(value)) {
|
||||||
|
setNameError("This name is already taken by an agent");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!/^[a-zA-Z0-9_-\s]+$/.test(value)) {
|
||||||
|
setNameError("Name must contain only letters, numbers, underscores, hyphens, and spaces");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
setNameError(null);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNameChange = (value: string) => {
|
||||||
|
setLocalName(value);
|
||||||
|
|
||||||
|
if (validateName(value)) {
|
||||||
|
handleUpdate({
|
||||||
|
...pipeline,
|
||||||
|
name: value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
showSavedMessage();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Panel
|
||||||
|
title={
|
||||||
|
<div className="flex items-center justify-between w-full">
|
||||||
|
<div className="text-base font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
{pipeline.name}
|
||||||
|
</div>
|
||||||
|
<CustomButton
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleClose}
|
||||||
|
showHoverContent={true}
|
||||||
|
hoverContent="Close"
|
||||||
|
>
|
||||||
|
<XIcon className="w-4 h-4" />
|
||||||
|
</CustomButton>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-6 p-4 h-[calc(100vh-100px)] min-h-0 flex-1">
|
||||||
|
{/* Saved Banner */}
|
||||||
|
{showSavedBanner && (
|
||||||
|
<div className="absolute top-4 left-4 z-10 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 animate-in slide-in-from-top-2 duration-300">
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
<span className="text-sm font-medium">Changes saved</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Pipeline Configuration */}
|
||||||
|
<div className="flex flex-col gap-4 pb-4 pt-0">
|
||||||
|
{/* Identity Section Card */}
|
||||||
|
<SectionCard
|
||||||
|
icon={<Settings className="w-5 h-5 text-indigo-500" />}
|
||||||
|
title="Identity"
|
||||||
|
labelWidth="md:w-32"
|
||||||
|
className="mb-1"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div className="flex flex-col md:flex-row md:items-start gap-1 md:gap-0">
|
||||||
|
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 md:w-32 mb-1 md:mb-0 md:pr-4">Name</label>
|
||||||
|
<div className="flex-1">
|
||||||
|
<InputField
|
||||||
|
type="text"
|
||||||
|
value={localName}
|
||||||
|
onChange={handleNameChange}
|
||||||
|
error={nameError}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col md:flex-row md:items-start gap-1 md:gap-0">
|
||||||
|
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 md:w-32 mb-1 md:mb-0 md:pr-4">Description</label>
|
||||||
|
<div className="flex-1">
|
||||||
|
<InputField
|
||||||
|
type="text"
|
||||||
|
value={pipeline.description || ""}
|
||||||
|
onChange={(value: string) => {
|
||||||
|
handleUpdate({ ...pipeline, description: value });
|
||||||
|
showSavedMessage();
|
||||||
|
}}
|
||||||
|
multiline={true}
|
||||||
|
placeholder="Enter a description for this pipeline"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SectionCard>
|
||||||
|
|
||||||
|
{/* Pipeline Info */}
|
||||||
|
<SectionCard
|
||||||
|
icon={<Settings className="w-5 h-5 text-indigo-500" />}
|
||||||
|
title="Behavior"
|
||||||
|
labelWidth="md:w-32"
|
||||||
|
className="mb-1"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
<div className="mb-2">
|
||||||
|
<span className="font-medium">Agents in Pipeline:</span> {pipeline.agents.length}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-950/30 p-3 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||||
|
<div className="font-medium mb-2">How Pipelines Work:</div>
|
||||||
|
<ul className="text-xs space-y-1 list-disc list-inside">
|
||||||
|
<li>Agents execute sequentially in the order shown</li>
|
||||||
|
<li>Output from one agent flows as input to the next</li>
|
||||||
|
<li>Add agents to this pipeline from the agents panel</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SectionCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -79,7 +79,7 @@ export function PromptConfig({
|
||||||
<div className="flex flex-col gap-6 p-4">
|
<div className="flex flex-col gap-6 p-4">
|
||||||
{/* Saved Banner */}
|
{/* Saved Banner */}
|
||||||
{showSavedBanner && (
|
{showSavedBanner && (
|
||||||
<div className="absolute top-4 right-4 z-10 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 animate-in slide-in-from-top-2 duration-300">
|
<div className="absolute top-4 left-4 z-10 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 animate-in slide-in-from-top-2 duration-300">
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
|
@ -358,7 +358,7 @@ export function ToolConfig({
|
||||||
<div className="flex flex-col gap-4 pb-4 pt-4 p-4">
|
<div className="flex flex-col gap-4 pb-4 pt-4 p-4">
|
||||||
{/* Saved Banner */}
|
{/* Saved Banner */}
|
||||||
{showSavedBanner && (
|
{showSavedBanner && (
|
||||||
<div className="absolute top-4 right-4 z-10 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 animate-in slide-in-from-top-2 duration-300">
|
<div className="absolute top-4 left-4 z-10 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 animate-in slide-in-from-top-2 duration-300">
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
|
@ -89,20 +89,21 @@ function InternalAssistantMessage({ content, sender, latency, delta, showJsonMod
|
||||||
<span>{sender ?? 'Assistant'}</span>
|
<span>{sender ?? 'Assistant'}</span>
|
||||||
{(Boolean(showDebugMessages && typeof onFix === 'function' && !isFirstAssistant)
|
{(Boolean(showDebugMessages && typeof onFix === 'function' && !isFirstAssistant)
|
||||||
|| Boolean(showDebugMessages && typeof onExplain === 'function' && !isFirstAssistant)
|
|| Boolean(showDebugMessages && typeof onExplain === 'function' && !isFirstAssistant)
|
||||||
|| Boolean(isJsonContent && hasResponseKey)) && (
|
|| Boolean(isJsonContent)) && (
|
||||||
<MessageActionsMenu
|
<MessageActionsMenu
|
||||||
showFix={Boolean(showDebugMessages && typeof onFix === 'function' && !isFirstAssistant)}
|
showFix={Boolean(showDebugMessages && typeof onFix === 'function' && !isFirstAssistant)}
|
||||||
showExplain={Boolean(showDebugMessages && typeof onExplain === 'function' && !isFirstAssistant)}
|
showExplain={Boolean(showDebugMessages && typeof onExplain === 'function' && !isFirstAssistant)}
|
||||||
showJson={Boolean(isJsonContent && hasResponseKey)}
|
showJson={Boolean(isJsonContent)}
|
||||||
onFix={onFix ? () => onFix(content, index) : () => {}}
|
onFix={onFix ? () => onFix(content, index) : () => {}}
|
||||||
onExplain={onExplain ? () => onExplain('assistant', content, index) : () => {}}
|
onExplain={onExplain ? () => onExplain('assistant', content, index) : () => {}}
|
||||||
onJson={() => {}}
|
onJson={() => setJsonMode(!jsonMode)}
|
||||||
|
jsonLabel={jsonMode ? 'View formatted content' : 'View complete JSON'}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-50 dark:bg-zinc-800 px-4 py-2.5 rounded-2xl rounded-bl-lg text-sm leading-relaxed text-gray-700 dark:text-gray-200 border-none shadow-sm animate-slideUpAndFade flex flex-col items-stretch">
|
<div className="bg-gray-50 dark:bg-zinc-800 px-4 py-2.5 rounded-2xl rounded-bl-lg text-sm leading-relaxed text-gray-700 dark:text-gray-200 border-none shadow-sm animate-slideUpAndFade flex flex-col items-stretch">
|
||||||
<div className="text-left mb-2">
|
<div className="text-left mb-2">
|
||||||
{isJsonContent && hasResponseKey && jsonMode && (
|
{isJsonContent && jsonMode && (
|
||||||
<div className="mb-2 flex gap-4">
|
<div className="mb-2 flex gap-4">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-1 text-xs text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-300 hover:underline self-start"
|
className="flex items-center gap-1 text-xs text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-300 hover:underline self-start"
|
||||||
|
|
@ -113,7 +114,7 @@ function InternalAssistantMessage({ content, sender, latency, delta, showJsonMod
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isJsonContent && hasResponseKey && jsonMode ? (
|
{isJsonContent && jsonMode ? (
|
||||||
<pre
|
<pre
|
||||||
className={`text-xs leading-snug bg-zinc-50 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-200 rounded-lg px-2 py-1 font-mono shadow-sm border border-zinc-100 dark:border-zinc-700 ${
|
className={`text-xs leading-snug bg-zinc-50 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-200 rounded-lg px-2 py-1 font-mono shadow-sm border border-zinc-100 dark:border-zinc-700 ${
|
||||||
wrapText ? 'whitespace-pre-wrap break-words' : 'overflow-x-auto whitespace-pre'
|
wrapText ? 'whitespace-pre-wrap break-words' : 'overflow-x-auto whitespace-pre'
|
||||||
|
|
@ -728,9 +729,14 @@ export function Messages({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then check for internal messages
|
// Then check for internal messages (including pipeline agents)
|
||||||
if (message.content && message.responseType === 'internal') {
|
// Check both responseType === 'internal' and pipeline agents by type
|
||||||
// Skip internal messages if debug mode is off
|
const agentConfig = workflow.agents.find(a => a.name === message.agentName);
|
||||||
|
const isInternalOrPipeline = message.responseType === 'internal' ||
|
||||||
|
(agentConfig && (agentConfig.outputVisibility === 'internal' || agentConfig.type === 'pipeline'));
|
||||||
|
|
||||||
|
if (message.content && isInternalOrPipeline) {
|
||||||
|
// Skip internal/pipeline messages if debug mode is off
|
||||||
if (!showDebugMessages) {
|
if (!showDebugMessages) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { forwardRef, useImperativeHandle } from "react";
|
import React, { forwardRef, useImperativeHandle } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { WorkflowPrompt, WorkflowAgent, WorkflowTool, Workflow } from "../../../lib/types/workflow_types";
|
import { WorkflowPrompt, WorkflowAgent, WorkflowTool, WorkflowPipeline, Workflow } from "../../../lib/types/workflow_types";
|
||||||
import { Project } from "../../../lib/types/project_types";
|
import { Project } from "../../../lib/types/project_types";
|
||||||
import { DataSource } from "../../../lib/types/datasource_types";
|
import { DataSource } from "../../../lib/types/datasource_types";
|
||||||
import { WithStringId } from "../../../lib/types/types";
|
import { WithStringId } from "../../../lib/types/types";
|
||||||
|
|
@ -46,25 +46,30 @@ interface EntityListProps {
|
||||||
agents: z.infer<typeof WorkflowAgent>[];
|
agents: z.infer<typeof WorkflowAgent>[];
|
||||||
tools: z.infer<typeof WorkflowTool>[];
|
tools: z.infer<typeof WorkflowTool>[];
|
||||||
prompts: z.infer<typeof WorkflowPrompt>[];
|
prompts: z.infer<typeof WorkflowPrompt>[];
|
||||||
|
pipelines: z.infer<typeof WorkflowPipeline>[];
|
||||||
dataSources: WithStringId<z.infer<typeof DataSource>>[];
|
dataSources: WithStringId<z.infer<typeof DataSource>>[];
|
||||||
workflow: z.infer<typeof Workflow>;
|
workflow: z.infer<typeof Workflow>;
|
||||||
selectedEntity: {
|
selectedEntity: {
|
||||||
type: "agent" | "tool" | "prompt" | "datasource" | "visualise";
|
type: "agent" | "tool" | "prompt" | "datasource" | "pipeline" | "visualise";
|
||||||
name: string;
|
name: string;
|
||||||
} | null;
|
} | null;
|
||||||
startAgentName: string | null;
|
startAgentName: string | null;
|
||||||
onSelectAgent: (name: string) => void;
|
onSelectAgent: (name: string) => void;
|
||||||
onSelectTool: (name: string) => void;
|
onSelectTool: (name: string) => void;
|
||||||
onSelectPrompt: (name: string) => void;
|
onSelectPrompt: (name: string) => void;
|
||||||
|
onSelectPipeline: (name: string) => void;
|
||||||
onSelectDataSource?: (id: string) => void;
|
onSelectDataSource?: (id: string) => void;
|
||||||
onAddAgent: (agent: Partial<z.infer<typeof WorkflowAgent>>) => void;
|
onAddAgent: (agent: Partial<z.infer<typeof WorkflowAgent>>) => void;
|
||||||
onAddTool: (tool: Partial<z.infer<typeof WorkflowTool>>) => void;
|
onAddTool: (tool: Partial<z.infer<typeof WorkflowTool>>) => void;
|
||||||
onAddPrompt: (prompt: Partial<z.infer<typeof WorkflowPrompt>>) => void;
|
onAddPrompt: (prompt: Partial<z.infer<typeof WorkflowPrompt>>) => void;
|
||||||
|
onAddPipeline: (pipeline: Partial<z.infer<typeof WorkflowPipeline>>) => void;
|
||||||
|
onAddAgentToPipeline: (pipelineName: string) => void;
|
||||||
onToggleAgent: (name: string) => void;
|
onToggleAgent: (name: string) => void;
|
||||||
onSetMainAgent: (name: string) => void;
|
onSetMainAgent: (name: string) => void;
|
||||||
onDeleteAgent: (name: string) => void;
|
onDeleteAgent: (name: string) => void;
|
||||||
onDeleteTool: (name: string) => void;
|
onDeleteTool: (name: string) => void;
|
||||||
onDeletePrompt: (name: string) => void;
|
onDeletePrompt: (name: string) => void;
|
||||||
|
onDeletePipeline: (name: string) => void;
|
||||||
onShowVisualise: (name: string) => void;
|
onShowVisualise: (name: string) => void;
|
||||||
onProjectToolsUpdated?: () => void;
|
onProjectToolsUpdated?: () => void;
|
||||||
onDataSourcesUpdated?: () => void;
|
onDataSourcesUpdated?: () => void;
|
||||||
|
|
@ -72,6 +77,7 @@ interface EntityListProps {
|
||||||
useRagUploads: boolean;
|
useRagUploads: boolean;
|
||||||
useRagS3Uploads: boolean;
|
useRagS3Uploads: boolean;
|
||||||
useRagScraping: boolean;
|
useRagScraping: boolean;
|
||||||
|
onReorderPipelines: (pipelines: z.infer<typeof WorkflowPipeline>[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EmptyStateProps {
|
interface EmptyStateProps {
|
||||||
|
|
@ -174,7 +180,7 @@ interface ServerCardProps {
|
||||||
serverName: string;
|
serverName: string;
|
||||||
tools: z.infer<typeof WorkflowTool>[];
|
tools: z.infer<typeof WorkflowTool>[];
|
||||||
selectedEntity: {
|
selectedEntity: {
|
||||||
type: "agent" | "tool" | "prompt" | "datasource" | "visualise";
|
type: "agent" | "tool" | "prompt" | "datasource" | "pipeline" | "visualise";
|
||||||
name: string;
|
name: string;
|
||||||
} | null;
|
} | null;
|
||||||
onSelectTool: (name: string) => void;
|
onSelectTool: (name: string) => void;
|
||||||
|
|
@ -258,6 +264,163 @@ type ComposioToolkit = {
|
||||||
tools: z.infer<typeof WorkflowTool>[];
|
tools: z.infer<typeof WorkflowTool>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PipelineCardProps {
|
||||||
|
pipeline: z.infer<typeof WorkflowPipeline>;
|
||||||
|
agents: z.infer<typeof WorkflowAgent>[];
|
||||||
|
selectedEntity: {
|
||||||
|
type: "agent" | "tool" | "prompt" | "datasource" | "pipeline" | "visualise";
|
||||||
|
name: string;
|
||||||
|
} | null;
|
||||||
|
onSelectPipeline: (name: string) => void;
|
||||||
|
onSelectAgent: (name: string) => void;
|
||||||
|
onDeletePipeline: (name: string) => void;
|
||||||
|
onDeleteAgent: (name: string) => void;
|
||||||
|
onAddAgentToPipeline: (pipelineName: string) => void;
|
||||||
|
selectedRef: React.RefObject<HTMLButtonElement | null>;
|
||||||
|
startAgentName: string | null;
|
||||||
|
dragHandle?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PipelineCard = ({
|
||||||
|
pipeline,
|
||||||
|
agents,
|
||||||
|
selectedEntity,
|
||||||
|
onSelectPipeline,
|
||||||
|
onSelectAgent,
|
||||||
|
onDeletePipeline,
|
||||||
|
onDeleteAgent,
|
||||||
|
onAddAgentToPipeline,
|
||||||
|
selectedRef,
|
||||||
|
startAgentName,
|
||||||
|
dragHandle,
|
||||||
|
}: PipelineCardProps) => {
|
||||||
|
// Get agents that belong to this pipeline
|
||||||
|
const pipelineAgents = pipeline.agents
|
||||||
|
.map(agentName => agents.find(agent => agent.name === agentName))
|
||||||
|
.filter(Boolean) as z.infer<typeof WorkflowAgent>[];
|
||||||
|
|
||||||
|
// Check if any agent in this pipeline is currently selected
|
||||||
|
const hasSelectedAgent = selectedEntity?.type === "agent" &&
|
||||||
|
pipeline.agents.includes(selectedEntity.name);
|
||||||
|
|
||||||
|
// Track expansion state - allow manual override even when agent is selected
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
const [lastSelectedAgent, setLastSelectedAgent] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Auto-expand when a new agent in this pipeline is selected
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasSelectedAgent && selectedEntity?.name !== lastSelectedAgent) {
|
||||||
|
setIsExpanded(true);
|
||||||
|
setLastSelectedAgent(selectedEntity?.name || null);
|
||||||
|
} else if (!hasSelectedAgent) {
|
||||||
|
setLastSelectedAgent(null);
|
||||||
|
}
|
||||||
|
}, [hasSelectedAgent, selectedEntity?.name, lastSelectedAgent]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-1 group">
|
||||||
|
<div className="flex items-center gap-2 px-2 py-1 hover:bg-zinc-50 dark:hover:bg-zinc-800 rounded-md transition-colors">
|
||||||
|
{dragHandle}
|
||||||
|
{/* Chevron button for expand/collapse - only show when has agents and on hover */}
|
||||||
|
<button
|
||||||
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
|
className={`w-4 h-4 flex items-center justify-center transition-opacity rounded ${
|
||||||
|
pipelineAgents.length > 0 ? 'group-hover:opacity-100 opacity-60 hover:bg-gray-200 dark:hover:bg-gray-700' : 'opacity-0 pointer-events-none'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{pipelineAgents.length > 0 && (isExpanded ? (
|
||||||
|
<ChevronDown className="w-3 h-3 text-gray-500" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="w-3 h-3 text-gray-500" />
|
||||||
|
))}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Pipeline name button for configuration */}
|
||||||
|
<button
|
||||||
|
onClick={() => onSelectPipeline(pipeline.name)}
|
||||||
|
className="flex-1 flex items-center gap-2 text-sm text-left min-h-[28px]"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs">{pipeline.name}</span>
|
||||||
|
<span className="text-xs text-gray-500">({pipelineAgents.length} steps)</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Pipeline menu */}
|
||||||
|
<div className="opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownTrigger>
|
||||||
|
<button className="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md transition-colors">
|
||||||
|
<MoreVertical className="w-4 h-4 text-gray-500" />
|
||||||
|
</button>
|
||||||
|
</DropdownTrigger>
|
||||||
|
<DropdownMenu
|
||||||
|
onAction={(key) => {
|
||||||
|
if (key === 'delete') {
|
||||||
|
onDeletePipeline(pipeline.name);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownItem key="delete" className="text-danger">Delete Pipeline</DropdownItem>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="ml-6 mt-0.5 space-y-0.5 border-l border-gray-200 dark:border-gray-700 pl-3">
|
||||||
|
{pipelineAgents.map((agent, index) => (
|
||||||
|
<div key={`pipeline-agent-${index}`} className="group/agent">
|
||||||
|
<div className={clsx(
|
||||||
|
"flex items-center gap-2 px-3 py-2 rounded-md min-h-[24px] cursor-pointer",
|
||||||
|
{
|
||||||
|
"bg-indigo-50 dark:bg-indigo-950/30": selectedEntity?.type === "agent" && selectedEntity.name === agent.name,
|
||||||
|
"hover:bg-zinc-50 dark:hover:bg-zinc-800": !(selectedEntity?.type === "agent" && selectedEntity.name === agent.name)
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
onClick={() => onSelectAgent(agent.name)}>
|
||||||
|
<div className="shrink-0 flex items-center justify-center w-3 h-3">
|
||||||
|
<span className="text-xs font-semibold text-indigo-600 dark:text-indigo-400">
|
||||||
|
{index + 1}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs flex-1">{agent.name}</span>
|
||||||
|
{startAgentName === agent.name && (
|
||||||
|
<div className="text-xs text-indigo-500 dark:text-indigo-400 bg-indigo-50/50 dark:bg-indigo-950/30 px-1.5 py-0.5 rounded">
|
||||||
|
Start
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="opacity-0 group-hover/agent:opacity-100 transition-opacity">
|
||||||
|
<button
|
||||||
|
className="p-1 hover:bg-red-100 dark:hover:bg-red-900 rounded-md transition-colors"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDeleteAgent(agent.name);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3 h-3 text-red-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{/* Add Agent option */}
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-2 px-3 py-2 mt-1 text-xs text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-950/30 rounded transition-colors"
|
||||||
|
onClick={() => {
|
||||||
|
// Create a new pipeline agent and add it to this pipeline
|
||||||
|
onAddAgentToPipeline(pipeline.name); // This will select the pipeline for editing later
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusIcon className="w-4 h-4" />
|
||||||
|
<span>Add Agent to Pipeline</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const EntityList = forwardRef<
|
export const EntityList = forwardRef<
|
||||||
{ openDataSourcesModal: () => void },
|
{ openDataSourcesModal: () => void },
|
||||||
EntityListProps & {
|
EntityListProps & {
|
||||||
|
|
@ -268,6 +431,7 @@ export const EntityList = forwardRef<
|
||||||
agents,
|
agents,
|
||||||
tools,
|
tools,
|
||||||
prompts,
|
prompts,
|
||||||
|
pipelines,
|
||||||
dataSources,
|
dataSources,
|
||||||
workflow,
|
workflow,
|
||||||
selectedEntity,
|
selectedEntity,
|
||||||
|
|
@ -275,27 +439,33 @@ export const EntityList = forwardRef<
|
||||||
onSelectAgent,
|
onSelectAgent,
|
||||||
onSelectTool,
|
onSelectTool,
|
||||||
onSelectPrompt,
|
onSelectPrompt,
|
||||||
|
onSelectPipeline,
|
||||||
onSelectDataSource,
|
onSelectDataSource,
|
||||||
onAddAgent,
|
onAddAgent,
|
||||||
onAddTool,
|
onAddTool,
|
||||||
onAddPrompt,
|
onAddPrompt,
|
||||||
|
onAddPipeline,
|
||||||
|
onAddAgentToPipeline,
|
||||||
onToggleAgent,
|
onToggleAgent,
|
||||||
onSetMainAgent,
|
onSetMainAgent,
|
||||||
onDeleteAgent,
|
onDeleteAgent,
|
||||||
onDeleteTool,
|
onDeleteTool,
|
||||||
onDeletePrompt,
|
onDeletePrompt,
|
||||||
|
onDeletePipeline,
|
||||||
onProjectToolsUpdated,
|
onProjectToolsUpdated,
|
||||||
onDataSourcesUpdated,
|
onDataSourcesUpdated,
|
||||||
projectId,
|
projectId,
|
||||||
projectConfig,
|
projectConfig,
|
||||||
onReorderAgents,
|
onReorderAgents,
|
||||||
|
onReorderPipelines,
|
||||||
onShowVisualise,
|
onShowVisualise,
|
||||||
useRagUploads,
|
useRagUploads,
|
||||||
useRagS3Uploads,
|
useRagS3Uploads,
|
||||||
useRagScraping,
|
useRagScraping,
|
||||||
}: EntityListProps & {
|
}: EntityListProps & {
|
||||||
projectId: string,
|
projectId: string,
|
||||||
onReorderAgents: (agents: z.infer<typeof WorkflowAgent>[]) => void
|
onReorderAgents: (agents: z.infer<typeof WorkflowAgent>[]) => void,
|
||||||
|
onReorderPipelines: (pipelines: z.infer<typeof WorkflowPipeline>[]) => void
|
||||||
}, ref) {
|
}, ref) {
|
||||||
const [showAgentTypeModal, setShowAgentTypeModal] = useState(false);
|
const [showAgentTypeModal, setShowAgentTypeModal] = useState(false);
|
||||||
const [showToolsModal, setShowToolsModal] = useState(false);
|
const [showToolsModal, setShowToolsModal] = useState(false);
|
||||||
|
|
@ -417,20 +587,44 @@ export const EntityList = forwardRef<
|
||||||
const { active, over } = event;
|
const { active, over } = event;
|
||||||
|
|
||||||
if (over && active.id !== over.id) {
|
if (over && active.id !== over.id) {
|
||||||
const oldIndex = agents.findIndex(agent => agent.name === active.id);
|
// Determine if we're dragging a pipeline or an agent
|
||||||
const newIndex = agents.findIndex(agent => agent.name === over.id);
|
const isPipelineDrag = pipelines.some(pipeline => pipeline.name === active.id);
|
||||||
|
const isPipelineTarget = pipelines.some(pipeline => pipeline.name === over.id);
|
||||||
|
|
||||||
const newAgents = [...agents];
|
if (isPipelineDrag && isPipelineTarget) {
|
||||||
const [movedAgent] = newAgents.splice(oldIndex, 1);
|
// Reordering pipelines
|
||||||
newAgents.splice(newIndex, 0, movedAgent);
|
const oldIndex = pipelines.findIndex(pipeline => pipeline.name === active.id);
|
||||||
|
const newIndex = pipelines.findIndex(pipeline => pipeline.name === over.id);
|
||||||
// Update order numbers
|
|
||||||
const updatedAgents = newAgents.map((agent, index) => ({
|
const newPipelines = [...pipelines];
|
||||||
...agent,
|
const [movedPipeline] = newPipelines.splice(oldIndex, 1);
|
||||||
order: index * 100
|
newPipelines.splice(newIndex, 0, movedPipeline);
|
||||||
}));
|
|
||||||
|
// Update order numbers
|
||||||
onReorderAgents(updatedAgents);
|
const updatedPipelines = newPipelines.map((pipeline, index) => ({
|
||||||
|
...pipeline,
|
||||||
|
order: index * 100
|
||||||
|
}));
|
||||||
|
|
||||||
|
onReorderPipelines(updatedPipelines);
|
||||||
|
} else if (!isPipelineDrag && !isPipelineTarget) {
|
||||||
|
// Reordering individual agents (not in pipelines)
|
||||||
|
const oldIndex = agents.findIndex(agent => agent.name === active.id);
|
||||||
|
const newIndex = agents.findIndex(agent => agent.name === over.id);
|
||||||
|
|
||||||
|
const newAgents = [...agents];
|
||||||
|
const [movedAgent] = newAgents.splice(oldIndex, 1);
|
||||||
|
newAgents.splice(newIndex, 0, movedAgent);
|
||||||
|
|
||||||
|
// Update order numbers
|
||||||
|
const updatedAgents = newAgents.map((agent, index) => ({
|
||||||
|
...agent,
|
||||||
|
order: index * 100
|
||||||
|
}));
|
||||||
|
|
||||||
|
onReorderAgents(updatedAgents);
|
||||||
|
}
|
||||||
|
// Note: We don't allow dragging between pipelines and agents
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -509,36 +703,83 @@ export const EntityList = forwardRef<
|
||||||
{expandedPanels.agents && (
|
{expandedPanels.agents && (
|
||||||
<div className="h-[calc(100%-53px)] overflow-y-auto">
|
<div className="h-[calc(100%-53px)] overflow-y-auto">
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
{agents.length > 0 ? (
|
{pipelines.length > 0 || agents.length > 0 ? (
|
||||||
<DndContext
|
<div className="space-y-1">
|
||||||
sensors={sensors}
|
{/* Show pipelines first with drag-and-drop */}
|
||||||
collisionDetection={closestCenter}
|
{pipelines.length > 0 && (
|
||||||
onDragEnd={handleDragEnd}
|
<DndContext
|
||||||
>
|
sensors={sensors}
|
||||||
<SortableContext
|
collisionDetection={closestCenter}
|
||||||
items={agents.map(a => a.name)}
|
onDragEnd={handleDragEnd}
|
||||||
strategy={verticalListSortingStrategy}
|
>
|
||||||
>
|
<SortableContext
|
||||||
<div className="space-y-1">
|
items={pipelines.map(p => p.name)}
|
||||||
{agents.map((agent) => (
|
strategy={verticalListSortingStrategy}
|
||||||
<SortableAgentItem
|
>
|
||||||
key={agent.name}
|
{pipelines.map((pipeline) => (
|
||||||
agent={agent}
|
<SortablePipelineItem
|
||||||
isSelected={selectedEntity?.type === "agent" && selectedEntity.name === agent.name}
|
key={pipeline.name}
|
||||||
onClick={() => onSelectAgent(agent.name)}
|
pipeline={pipeline}
|
||||||
selectedRef={selectedEntity?.type === "agent" && selectedEntity.name === agent.name ? selectedRef : undefined}
|
agents={agents}
|
||||||
statusLabel={startAgentName === agent.name ? <StartLabel /> : null}
|
selectedEntity={selectedEntity}
|
||||||
onToggle={onToggleAgent}
|
onSelectPipeline={onSelectPipeline}
|
||||||
onSetMainAgent={onSetMainAgent}
|
onSelectAgent={onSelectAgent}
|
||||||
onDelete={onDeleteAgent}
|
onDeletePipeline={onDeletePipeline}
|
||||||
isStartAgent={startAgentName === agent.name}
|
onDeleteAgent={onDeleteAgent}
|
||||||
/>
|
onAddAgentToPipeline={onAddAgentToPipeline}
|
||||||
))}
|
selectedRef={selectedRef}
|
||||||
</div>
|
startAgentName={startAgentName}
|
||||||
</SortableContext>
|
/>
|
||||||
</DndContext>
|
))}
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Show individual agents that are NOT part of any pipeline */}
|
||||||
|
{(() => {
|
||||||
|
// Get all agent names that are part of pipelines
|
||||||
|
const pipelineAgentNames = new Set(
|
||||||
|
pipelines.flatMap(pipeline => pipeline.agents)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter agents that are not in any pipeline and are not pipeline agents
|
||||||
|
const individualAgents = agents.filter(
|
||||||
|
agent => !pipelineAgentNames.has(agent.name) && agent.type !== 'pipeline'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (individualAgents.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
>
|
||||||
|
<SortableContext
|
||||||
|
items={individualAgents.map(a => a.name)}
|
||||||
|
strategy={verticalListSortingStrategy}
|
||||||
|
>
|
||||||
|
{individualAgents.map((agent) => (
|
||||||
|
<SortableAgentItem
|
||||||
|
key={agent.name}
|
||||||
|
agent={agent}
|
||||||
|
isSelected={selectedEntity?.type === "agent" && selectedEntity.name === agent.name}
|
||||||
|
onClick={() => onSelectAgent(agent.name)}
|
||||||
|
selectedRef={selectedEntity?.type === "agent" && selectedEntity.name === agent.name ? selectedRef : undefined}
|
||||||
|
statusLabel={startAgentName === agent.name ? <StartLabel /> : null}
|
||||||
|
onToggle={onToggleAgent}
|
||||||
|
onSetMainAgent={onSetMainAgent}
|
||||||
|
onDelete={onDeleteAgent}
|
||||||
|
isStartAgent={startAgentName === agent.name}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<EmptyState entity="agents" hasFilteredItems={false} />
|
<EmptyState entity="agents and pipelines" hasFilteredItems={false} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -925,6 +1166,10 @@ export const EntityList = forwardRef<
|
||||||
isOpen={showAgentTypeModal}
|
isOpen={showAgentTypeModal}
|
||||||
onClose={() => setShowAgentTypeModal(false)}
|
onClose={() => setShowAgentTypeModal(false)}
|
||||||
onConfirm={handleAddAgentWithType}
|
onConfirm={handleAddAgentWithType}
|
||||||
|
onCreatePipeline={() => {
|
||||||
|
onAddPipeline({ name: `Pipeline ${pipelines.length + 1}` });
|
||||||
|
setShowAgentTypeModal(false);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<ToolsModal
|
<ToolsModal
|
||||||
isOpen={showToolsModal}
|
isOpen={showToolsModal}
|
||||||
|
|
@ -1027,7 +1272,7 @@ function EntityDropdown({
|
||||||
interface ComposioCardProps {
|
interface ComposioCardProps {
|
||||||
card: ComposioToolkit;
|
card: ComposioToolkit;
|
||||||
selectedEntity: {
|
selectedEntity: {
|
||||||
type: "agent" | "tool" | "prompt" | "datasource" | "visualise";
|
type: "agent" | "tool" | "prompt" | "datasource" | "pipeline" | "visualise";
|
||||||
name: string;
|
name: string;
|
||||||
} | null;
|
} | null;
|
||||||
onSelectTool: (name: string) => void;
|
onSelectTool: (name: string) => void;
|
||||||
|
|
@ -1210,7 +1455,7 @@ const ComposioCard = ({
|
||||||
<div className="flex items-center gap-2 px-2 py-1 hover:bg-zinc-50 dark:hover:bg-zinc-800 rounded-md transition-colors">
|
<div className="flex items-center gap-2 px-2 py-1 hover:bg-zinc-50 dark:hover:bg-zinc-800 rounded-md transition-colors">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsExpanded(!isExpanded)}
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
className="flex-1 flex items-center gap-2 text-sm text-left min-h-[28px] py-1"
|
className="flex-1 flex items-center gap-2 text-left min-h-[28px]"
|
||||||
>
|
>
|
||||||
{/* Chevron - only show on hover or when has tools */}
|
{/* Chevron - only show on hover or when has tools */}
|
||||||
<div className={`w-4 h-4 flex items-center justify-center transition-opacity ${
|
<div className={`w-4 h-4 flex items-center justify-center transition-opacity ${
|
||||||
|
|
@ -1391,62 +1636,132 @@ const SortableAgentItem = ({ agent, isSelected, onClick, selectedRef, statusLabe
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add SortableItem component for pipelines
|
||||||
|
const SortablePipelineItem = ({
|
||||||
|
pipeline,
|
||||||
|
agents,
|
||||||
|
selectedEntity,
|
||||||
|
onSelectPipeline,
|
||||||
|
onSelectAgent,
|
||||||
|
onDeletePipeline,
|
||||||
|
onDeleteAgent,
|
||||||
|
onAddAgentToPipeline,
|
||||||
|
selectedRef,
|
||||||
|
startAgentName
|
||||||
|
}: {
|
||||||
|
pipeline: z.infer<typeof WorkflowPipeline>;
|
||||||
|
agents: z.infer<typeof WorkflowAgent>[];
|
||||||
|
selectedEntity: {
|
||||||
|
type: "agent" | "tool" | "prompt" | "datasource" | "pipeline" | "visualise";
|
||||||
|
name: string;
|
||||||
|
} | null;
|
||||||
|
onSelectPipeline: (name: string) => void;
|
||||||
|
onSelectAgent: (name: string) => void;
|
||||||
|
onDeletePipeline: (name: string) => void;
|
||||||
|
onDeleteAgent: (name: string) => void;
|
||||||
|
onAddAgentToPipeline: (pipelineName: string) => void;
|
||||||
|
selectedRef: React.RefObject<HTMLButtonElement | null>;
|
||||||
|
startAgentName: string | null;
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging
|
||||||
|
} = useSortable({ id: pipeline.name });
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
opacity: isDragging ? 0.5 : 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={setNodeRef} style={style} {...attributes}>
|
||||||
|
<PipelineCard
|
||||||
|
pipeline={pipeline}
|
||||||
|
agents={agents}
|
||||||
|
selectedEntity={selectedEntity}
|
||||||
|
onSelectPipeline={onSelectPipeline}
|
||||||
|
onSelectAgent={onSelectAgent}
|
||||||
|
onDeletePipeline={onDeletePipeline}
|
||||||
|
onDeleteAgent={onDeleteAgent}
|
||||||
|
onAddAgentToPipeline={onAddAgentToPipeline}
|
||||||
|
selectedRef={selectedRef}
|
||||||
|
startAgentName={startAgentName}
|
||||||
|
dragHandle={
|
||||||
|
<button className="cursor-grab" {...listeners}>
|
||||||
|
<GripVertical className="w-4 h-4 text-gray-400" />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface AgentTypeModalProps {
|
interface AgentTypeModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onConfirm: (agentType: 'internal' | 'user_facing') => void;
|
onConfirm: (agentType: 'internal' | 'user_facing') => void;
|
||||||
|
onCreatePipeline: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AgentTypeModal({ isOpen, onClose, onConfirm }: AgentTypeModalProps) {
|
function AgentTypeModal({ isOpen, onClose, onConfirm, onCreatePipeline }: AgentTypeModalProps) {
|
||||||
const [selectedType, setSelectedType] = useState<'internal' | 'user_facing'>('internal');
|
const [selectedType, setSelectedType] = useState<'internal' | 'user_facing' | 'pipeline'>('internal');
|
||||||
|
|
||||||
const handleConfirm = () => {
|
const handleConfirm = () => {
|
||||||
onConfirm(selectedType);
|
if (selectedType === 'pipeline') {
|
||||||
|
onCreatePipeline();
|
||||||
|
} else {
|
||||||
|
onConfirm(selectedType);
|
||||||
|
}
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={onClose} size="lg" className="max-w-3xl w-full">
|
<Modal isOpen={isOpen} onClose={onClose} size="lg" className="max-w-5xl w-full">
|
||||||
<ModalContent className="max-w-3xl w-full">
|
<ModalContent className="max-w-5xl w-full">
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Brain className="w-5 h-5 text-indigo-600" />
|
<Brain className="w-5 h-5 text-indigo-600" />
|
||||||
<span>Create New Agent</span>
|
<span>Create New Agent or Pipeline</span>
|
||||||
</div>
|
</div>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody className="p-8">
|
||||||
<div className="space-y-6">
|
<div className="space-y-8">
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
Choose the type of agent you want to create:
|
Choose what you want to create:
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||||
{/* Task Agent (Internal) */}
|
{/* Task Agent (Internal) */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSelectedType('internal')}
|
onClick={() => setSelectedType('internal')}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"relative group p-6 rounded-2xl border-2 flex flex-col items-start transition-all duration-200 text-left shadow-sm focus:outline-none",
|
"relative group p-4 rounded-2xl border-2 flex flex-col items-start transition-all duration-200 text-left shadow-sm focus:outline-none",
|
||||||
selectedType === 'internal'
|
selectedType === 'internal'
|
||||||
? "border-indigo-500 bg-indigo-50 dark:bg-indigo-950/40 shadow-lg scale-[1.03]"
|
? "border-indigo-500 bg-indigo-50 dark:bg-indigo-950/40 shadow-lg scale-[1.03]"
|
||||||
: "border-gray-200 dark:border-gray-700 hover:border-indigo-400 hover:shadow-md bg-white dark:bg-gray-900"
|
: "border-gray-200 dark:border-gray-700 hover:border-indigo-400 hover:shadow-md bg-white dark:bg-gray-900"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-4 w-full mb-2">
|
<div className="flex items-center gap-3 w-full mb-1">
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
"flex items-center justify-center w-12 h-12 rounded-lg transition-colors",
|
"flex items-center justify-center w-10 h-10 rounded-lg transition-colors",
|
||||||
selectedType === 'internal'
|
selectedType === 'internal'
|
||||||
? "bg-indigo-100 dark:bg-indigo-900/60"
|
? "bg-indigo-100 dark:bg-indigo-900/60"
|
||||||
: "bg-gray-100 dark:bg-gray-800"
|
: "bg-gray-100 dark:bg-gray-800"
|
||||||
)}>
|
)}>
|
||||||
<Cog className={clsx(
|
<Cog className={clsx(
|
||||||
"w-6 h-6 transition-colors",
|
"w-5 h-5 transition-colors",
|
||||||
selectedType === 'internal'
|
selectedType === 'internal'
|
||||||
? "text-indigo-600 dark:text-indigo-400"
|
? "text-indigo-600 dark:text-indigo-400"
|
||||||
: "text-gray-600 dark:text-gray-400"
|
: "text-gray-600 dark:text-gray-400"
|
||||||
)} />
|
)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-1">
|
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-0.5">
|
||||||
Task Agent
|
Task Agent
|
||||||
</h3>
|
</h3>
|
||||||
<span className="inline-block align-middle">
|
<span className="inline-block align-middle">
|
||||||
|
|
@ -1456,7 +1771,7 @@ function AgentTypeModal({ isOpen, onClose, onConfirm }: AgentTypeModalProps) {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul className="text-sm text-gray-600 dark:text-gray-400 leading-relaxed mt-1 list-disc pl-5 space-y-1">
|
<ul className="text-sm text-gray-600 dark:text-gray-400 leading-snug mt-0 list-disc pl-4 space-y-0.5">
|
||||||
<li>Perform specific internal tasks, such as parts of workflows, pipelines, and data processing</li>
|
<li>Perform specific internal tasks, such as parts of workflows, pipelines, and data processing</li>
|
||||||
<li>Cannot put out user-facing responses directly</li>
|
<li>Cannot put out user-facing responses directly</li>
|
||||||
<li>Can call other agents (both conversation and task agents)</li>
|
<li>Can call other agents (both conversation and task agents)</li>
|
||||||
|
|
@ -1468,28 +1783,28 @@ function AgentTypeModal({ isOpen, onClose, onConfirm }: AgentTypeModalProps) {
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSelectedType('user_facing')}
|
onClick={() => setSelectedType('user_facing')}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"relative group p-6 rounded-2xl border-2 flex flex-col items-start transition-all duration-200 text-left shadow-sm focus:outline-none",
|
"relative group p-4 rounded-2xl border-2 flex flex-col items-start transition-all duration-200 text-left shadow-sm focus:outline-none",
|
||||||
selectedType === 'user_facing'
|
selectedType === 'user_facing'
|
||||||
? "border-indigo-500 bg-indigo-50 dark:bg-indigo-950/40 shadow-lg scale-[1.03]"
|
? "border-indigo-500 bg-indigo-50 dark:bg-indigo-950/40 shadow-lg scale-[1.03]"
|
||||||
: "border-gray-200 dark:border-gray-700 hover:border-indigo-400 hover:shadow-md bg-white dark:bg-gray-900"
|
: "border-gray-200 dark:border-gray-700 hover:border-indigo-400 hover:shadow-md bg-white dark:bg-gray-900"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-4 w-full mb-2">
|
<div className="flex items-center gap-3 w-full mb-1">
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
"flex items-center justify-center w-12 h-12 rounded-lg transition-colors",
|
"flex items-center justify-center w-10 h-10 rounded-lg transition-colors",
|
||||||
selectedType === 'user_facing'
|
selectedType === 'user_facing'
|
||||||
? "bg-indigo-100 dark:bg-indigo-900/60"
|
? "bg-indigo-100 dark:bg-indigo-900/60"
|
||||||
: "bg-gray-100 dark:bg-gray-800"
|
: "bg-gray-100 dark:bg-gray-800"
|
||||||
)}>
|
)}>
|
||||||
<Users className={clsx(
|
<Users className={clsx(
|
||||||
"w-6 h-6 transition-colors",
|
"w-5 h-5 transition-colors",
|
||||||
selectedType === 'user_facing'
|
selectedType === 'user_facing'
|
||||||
? "text-indigo-600 dark:text-indigo-400"
|
? "text-indigo-600 dark:text-indigo-400"
|
||||||
: "text-gray-600 dark:text-gray-400"
|
: "text-gray-600 dark:text-gray-400"
|
||||||
)} />
|
)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-1">
|
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-0.5">
|
||||||
Conversation Agent
|
Conversation Agent
|
||||||
</h3>
|
</h3>
|
||||||
<span className="inline-block align-middle">
|
<span className="inline-block align-middle">
|
||||||
|
|
@ -1499,16 +1814,59 @@ function AgentTypeModal({ isOpen, onClose, onConfirm }: AgentTypeModalProps) {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul className="text-sm text-gray-600 dark:text-gray-400 leading-relaxed mt-1 list-disc pl-5 space-y-1">
|
<ul className="text-sm text-gray-600 dark:text-gray-400 leading-snug mt-0 list-disc pl-4 space-y-0.5">
|
||||||
<li>Interact directly with users</li>
|
<li>Interact directly with users</li>
|
||||||
<li>Ideal for specific roles in customer support, chat interfaces, and other end-user interactions</li>
|
<li>Ideal for specific roles in customer support, chat interfaces, and other end-user interactions</li>
|
||||||
<li>Can call other agents (both conversation and task agents)</li>
|
<li>Can call other agents (both conversation and task agents)</li>
|
||||||
</ul>
|
</ul>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Pipeline */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedType('pipeline')}
|
||||||
|
className={clsx(
|
||||||
|
"relative group p-4 rounded-2xl border-2 flex flex-col items-start transition-all duration-200 text-left shadow-sm focus:outline-none",
|
||||||
|
selectedType === 'pipeline'
|
||||||
|
? "border-indigo-500 bg-indigo-50 dark:bg-indigo-950/40 shadow-lg scale-[1.03]"
|
||||||
|
: "border-gray-200 dark:border-gray-700 hover:border-indigo-400 hover:shadow-md bg-white dark:bg-gray-900"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 w-full mb-1">
|
||||||
|
<div className={clsx(
|
||||||
|
"flex items-center justify-center w-10 h-10 rounded-lg transition-colors",
|
||||||
|
selectedType === 'pipeline'
|
||||||
|
? "bg-indigo-100 dark:bg-indigo-900/60"
|
||||||
|
: "bg-gray-100 dark:bg-gray-800"
|
||||||
|
)}>
|
||||||
|
<Component className={clsx(
|
||||||
|
"w-5 h-5 transition-colors",
|
||||||
|
selectedType === 'pipeline'
|
||||||
|
? "text-indigo-600 dark:text-indigo-400"
|
||||||
|
: "text-gray-600 dark:text-gray-400"
|
||||||
|
)} />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-0.5">
|
||||||
|
Pipeline
|
||||||
|
</h3>
|
||||||
|
<span className="inline-block align-middle">
|
||||||
|
<span className="text-xs font-medium text-purple-700 dark:text-purple-300 bg-purple-100 dark:bg-purple-900/40 px-2 py-0.5 rounded">
|
||||||
|
Sequential
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul className="text-sm text-gray-600 dark:text-gray-400 leading-snug mt-0 list-disc pl-4 space-y-0.5">
|
||||||
|
<li>Create a sequential workflow of agents</li>
|
||||||
|
<li>Agents execute one after another in order</li>
|
||||||
|
<li>Add individual agents to the pipeline after creation</li>
|
||||||
|
</ul>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter className="px-8 pb-8">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
|
|
@ -1519,7 +1877,7 @@ function AgentTypeModal({ isOpen, onClose, onConfirm }: AgentTypeModalProps) {
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={handleConfirm}
|
onClick={handleConfirm}
|
||||||
>
|
>
|
||||||
Create Agent
|
{selectedType === 'pipeline' ? 'Create Pipeline' : 'Create Agent'}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
"use client";
|
"use client";
|
||||||
import React, { useReducer, Reducer, useState, useCallback, useEffect, useRef, createContext, useContext } from "react";
|
import React, { useReducer, Reducer, useState, useCallback, useEffect, useRef, createContext, useContext } from "react";
|
||||||
import { MCPServer, Message, WithStringId } from "../../../lib/types/types";
|
import { MCPServer, Message, WithStringId } from "../../../lib/types/types";
|
||||||
import { Workflow, WorkflowTool, WorkflowPrompt, WorkflowAgent } from "../../../lib/types/workflow_types";
|
import { Workflow, WorkflowTool, WorkflowPrompt, WorkflowAgent, WorkflowPipeline } from "../../../lib/types/workflow_types";
|
||||||
import { DataSource } from "../../../lib/types/datasource_types";
|
import { DataSource } from "../../../lib/types/datasource_types";
|
||||||
import { Project } from "../../../lib/types/project_types";
|
import { Project } from "../../../lib/types/project_types";
|
||||||
import { produce, applyPatches, enablePatches, produceWithPatches, Patch } from 'immer';
|
import { produce, applyPatches, enablePatches, produceWithPatches, Patch } from 'immer';
|
||||||
import { AgentConfig } from "../entities/agent_config";
|
import { AgentConfig } from "../entities/agent_config";
|
||||||
|
import { PipelineConfig } from "../entities/pipeline_config";
|
||||||
import { ToolConfig } from "../entities/tool_config";
|
import { ToolConfig } from "../entities/tool_config";
|
||||||
import { App as ChatApp } from "../playground/app";
|
import { App as ChatApp } from "../playground/app";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
@ -25,7 +26,7 @@ import { publishWorkflow } from "@/app/actions/project_actions";
|
||||||
import { saveWorkflow } from "@/app/actions/project_actions";
|
import { saveWorkflow } from "@/app/actions/project_actions";
|
||||||
import { updateProjectName } from "@/app/actions/project_actions";
|
import { updateProjectName } from "@/app/actions/project_actions";
|
||||||
import { BackIcon, HamburgerIcon, WorkflowIcon } from "../../../lib/components/icons";
|
import { BackIcon, HamburgerIcon, WorkflowIcon } from "../../../lib/components/icons";
|
||||||
import { CopyIcon, ImportIcon, Layers2Icon, RadioIcon, RedoIcon, ServerIcon, Sparkles, UndoIcon, RocketIcon, PenLine, AlertTriangle, DownloadIcon, XIcon, SettingsIcon, ChevronDownIcon, PhoneIcon, MessageCircleIcon } from "lucide-react";
|
import { CopyIcon, ImportIcon, RadioIcon, RedoIcon, ServerIcon, Sparkles, UndoIcon, RocketIcon, PenLine, AlertTriangle, DownloadIcon, XIcon, SettingsIcon, ChevronDownIcon, PhoneIcon, MessageCircleIcon } from "lucide-react";
|
||||||
import { EntityList } from "./entity_list";
|
import { EntityList } from "./entity_list";
|
||||||
import { ProductTour } from "@/components/common/product-tour";
|
import { ProductTour } from "@/components/common/product-tour";
|
||||||
import { ModelsResponse } from "@/app/lib/types/billing_types";
|
import { ModelsResponse } from "@/app/lib/types/billing_types";
|
||||||
|
|
@ -49,7 +50,7 @@ interface StateItem {
|
||||||
workflow: z.infer<typeof Workflow>;
|
workflow: z.infer<typeof Workflow>;
|
||||||
publishing: boolean;
|
publishing: boolean;
|
||||||
selection: {
|
selection: {
|
||||||
type: "agent" | "tool" | "prompt" | "datasource" | "visualise";
|
type: "agent" | "tool" | "prompt" | "datasource" | "pipeline" | "visualise";
|
||||||
name: string;
|
name: string;
|
||||||
} | null;
|
} | null;
|
||||||
saving: boolean;
|
saving: boolean;
|
||||||
|
|
@ -83,18 +84,31 @@ export type Action = {
|
||||||
} | {
|
} | {
|
||||||
type: "add_prompt";
|
type: "add_prompt";
|
||||||
prompt: Partial<z.infer<typeof WorkflowPrompt>>;
|
prompt: Partial<z.infer<typeof WorkflowPrompt>>;
|
||||||
|
} | {
|
||||||
|
type: "add_pipeline";
|
||||||
|
pipeline: Partial<z.infer<typeof WorkflowPipeline>>;
|
||||||
} | {
|
} | {
|
||||||
type: "select_agent";
|
type: "select_agent";
|
||||||
name: string;
|
name: string;
|
||||||
} | {
|
} | {
|
||||||
type: "select_tool";
|
type: "select_tool";
|
||||||
name: string;
|
name: string;
|
||||||
|
} | {
|
||||||
|
type: "select_pipeline";
|
||||||
|
name: string;
|
||||||
} | {
|
} | {
|
||||||
type: "delete_agent";
|
type: "delete_agent";
|
||||||
name: string;
|
name: string;
|
||||||
} | {
|
} | {
|
||||||
type: "delete_tool";
|
type: "delete_tool";
|
||||||
name: string;
|
name: string;
|
||||||
|
} | {
|
||||||
|
type: "delete_pipeline";
|
||||||
|
name: string;
|
||||||
|
} | {
|
||||||
|
type: "update_pipeline";
|
||||||
|
name: string;
|
||||||
|
pipeline: Partial<z.infer<typeof WorkflowPipeline>>;
|
||||||
} | {
|
} | {
|
||||||
type: "update_agent";
|
type: "update_agent";
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -119,6 +133,8 @@ export type Action = {
|
||||||
name: string;
|
name: string;
|
||||||
} | {
|
} | {
|
||||||
type: "unselect_prompt";
|
type: "unselect_prompt";
|
||||||
|
} | {
|
||||||
|
type: "unselect_pipeline";
|
||||||
} | {
|
} | {
|
||||||
type: "delete_prompt";
|
type: "delete_prompt";
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -144,6 +160,9 @@ export type Action = {
|
||||||
} | {
|
} | {
|
||||||
type: "reorder_agents";
|
type: "reorder_agents";
|
||||||
agents: z.infer<typeof WorkflowAgent>[];
|
agents: z.infer<typeof WorkflowAgent>[];
|
||||||
|
} | {
|
||||||
|
type: "reorder_pipelines";
|
||||||
|
pipelines: z.infer<typeof WorkflowPipeline>[];
|
||||||
} | {
|
} | {
|
||||||
type: "select_datasource";
|
type: "select_datasource";
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -235,6 +254,23 @@ function reducer(state: State, action: Action): State {
|
||||||
currentIndex: state.currentIndex + 1,
|
currentIndex: state.currentIndex + 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case "reorder_pipelines": {
|
||||||
|
const newState = produce(state.present, draft => {
|
||||||
|
draft.workflow.pipelines = action.pipelines;
|
||||||
|
draft.lastUpdatedAt = new Date().toISOString();
|
||||||
|
});
|
||||||
|
const [nextState, patches, inversePatches] = produceWithPatches(state.present, draft => {
|
||||||
|
draft.workflow.pipelines = action.pipelines;
|
||||||
|
draft.lastUpdatedAt = new Date().toISOString();
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
present: nextState,
|
||||||
|
patches: [...state.patches.slice(0, state.currentIndex), patches],
|
||||||
|
inversePatches: [...state.inversePatches.slice(0, state.currentIndex), inversePatches],
|
||||||
|
currentIndex: state.currentIndex + 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
case "show_visualise": {
|
case "show_visualise": {
|
||||||
newState = produce(state, draft => {
|
newState = produce(state, draft => {
|
||||||
draft.present.selection = { type: "visualise", name: "visualise" };
|
draft.present.selection = { type: "visualise", name: "visualise" };
|
||||||
|
|
@ -270,6 +306,12 @@ function reducer(state: State, action: Action): State {
|
||||||
name: action.name
|
name: action.name
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
case "select_pipeline":
|
||||||
|
draft.selection = {
|
||||||
|
type: "pipeline",
|
||||||
|
name: action.name
|
||||||
|
};
|
||||||
|
break;
|
||||||
case "select_datasource":
|
case "select_datasource":
|
||||||
draft.selection = {
|
draft.selection = {
|
||||||
type: "datasource",
|
type: "datasource",
|
||||||
|
|
@ -280,6 +322,7 @@ function reducer(state: State, action: Action): State {
|
||||||
case "unselect_tool":
|
case "unselect_tool":
|
||||||
case "unselect_prompt":
|
case "unselect_prompt":
|
||||||
case "unselect_datasource":
|
case "unselect_datasource":
|
||||||
|
case "unselect_pipeline":
|
||||||
draft.selection = null;
|
draft.selection = null;
|
||||||
break;
|
break;
|
||||||
case "add_agent": {
|
case "add_agent": {
|
||||||
|
|
@ -366,13 +409,97 @@ function reducer(state: State, action: Action): State {
|
||||||
draft.chatKey++;
|
draft.chatKey++;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "add_pipeline": {
|
||||||
|
if (isLive) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let newPipelineName = "New pipeline";
|
||||||
|
if (draft.workflow?.pipelines?.some((pipeline) => pipeline.name === newPipelineName)) {
|
||||||
|
newPipelineName = `New pipeline ${(draft.workflow?.pipelines?.filter((pipeline) =>
|
||||||
|
pipeline.name.startsWith("New pipeline")).length || 0) + 1}`;
|
||||||
|
}
|
||||||
|
if (!draft.workflow.pipelines) {
|
||||||
|
draft.workflow.pipelines = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the first agent for this pipeline
|
||||||
|
const firstAgentName = `${action.pipeline.name || newPipelineName} Step 1`;
|
||||||
|
draft.workflow.agents.push({
|
||||||
|
name: firstAgentName,
|
||||||
|
type: "pipeline",
|
||||||
|
description: "",
|
||||||
|
disabled: false,
|
||||||
|
instructions: "",
|
||||||
|
model: "gpt-4o",
|
||||||
|
locked: false,
|
||||||
|
toggleAble: true,
|
||||||
|
ragReturnType: "chunks",
|
||||||
|
ragK: 3,
|
||||||
|
controlType: "relinquish_to_parent",
|
||||||
|
outputVisibility: "internal",
|
||||||
|
maxCallsPerParentAgent: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create the pipeline with the first agent
|
||||||
|
draft.workflow.pipelines.push({
|
||||||
|
name: newPipelineName,
|
||||||
|
description: "",
|
||||||
|
agents: [firstAgentName],
|
||||||
|
...action.pipeline
|
||||||
|
});
|
||||||
|
|
||||||
|
// Select the newly created agent to open it in agent_config
|
||||||
|
draft.selection = {
|
||||||
|
type: "agent",
|
||||||
|
name: firstAgentName
|
||||||
|
};
|
||||||
|
draft.pendingChanges = true;
|
||||||
|
draft.chatKey++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "delete_agent":
|
case "delete_agent":
|
||||||
if (isLive) {
|
if (isLive) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
// Remove the agent
|
||||||
draft.workflow.agents = draft.workflow.agents.filter(
|
draft.workflow.agents = draft.workflow.agents.filter(
|
||||||
(agent) => agent.name !== action.name
|
(agent) => agent.name !== action.name
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Update references to deleted agent in other agents' instructions
|
||||||
|
draft.workflow.agents = draft.workflow.agents.map(agent => ({
|
||||||
|
...agent,
|
||||||
|
instructions: agent.instructions.replace(
|
||||||
|
new RegExp(`\\[@agent:${action.name}\\]\\(#mention\\)`, 'g'),
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Update references in prompts
|
||||||
|
draft.workflow.prompts = draft.workflow.prompts.map(prompt => ({
|
||||||
|
...prompt,
|
||||||
|
prompt: prompt.prompt.replace(
|
||||||
|
new RegExp(`\\[@agent:${action.name}\\]\\(#mention\\)`, 'g'),
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Update references in pipelines
|
||||||
|
if (draft.workflow.pipelines) {
|
||||||
|
draft.workflow.pipelines = draft.workflow.pipelines.map(pipeline => ({
|
||||||
|
...pipeline,
|
||||||
|
agents: pipeline.agents.filter(agentName => agentName !== action.name)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update start agent if it was the deleted agent
|
||||||
|
if (draft.workflow.startAgent === action.name) {
|
||||||
|
// Set to first available agent, or empty string if no agents left
|
||||||
|
draft.workflow.startAgent = draft.workflow.agents.length > 0
|
||||||
|
? draft.workflow.agents[0].name
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
draft.selection = null;
|
draft.selection = null;
|
||||||
draft.pendingChanges = true;
|
draft.pendingChanges = true;
|
||||||
draft.chatKey++;
|
draft.chatKey++;
|
||||||
|
|
@ -399,7 +526,80 @@ function reducer(state: State, action: Action): State {
|
||||||
draft.pendingChanges = true;
|
draft.pendingChanges = true;
|
||||||
draft.chatKey++;
|
draft.chatKey++;
|
||||||
break;
|
break;
|
||||||
case "update_agent":
|
case "delete_pipeline":
|
||||||
|
if (isLive) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (draft.workflow.pipelines) {
|
||||||
|
// Find the pipeline to get its associated agents
|
||||||
|
const pipelineToDelete = draft.workflow.pipelines.find(
|
||||||
|
(pipeline) => pipeline.name === action.name
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pipelineToDelete) {
|
||||||
|
// Remove all agents that belong to this pipeline
|
||||||
|
const agentsToDelete = pipelineToDelete.agents || [];
|
||||||
|
|
||||||
|
// Check if startAgent is one of the agents being deleted
|
||||||
|
const startAgentBeingDeleted = agentsToDelete.includes(draft.workflow.startAgent);
|
||||||
|
|
||||||
|
draft.workflow.agents = draft.workflow.agents.filter(
|
||||||
|
(agent) => !agentsToDelete.includes(agent.name)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update references to deleted agents in other agents' instructions
|
||||||
|
agentsToDelete.forEach(agentName => {
|
||||||
|
draft.workflow.agents = draft.workflow.agents.map(agent => ({
|
||||||
|
...agent,
|
||||||
|
instructions: agent.instructions.replace(
|
||||||
|
new RegExp(`\\[@agent:${agentName}\\]\\(#mention\\)`, 'g'),
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Update references in prompts
|
||||||
|
draft.workflow.prompts = draft.workflow.prompts.map(prompt => ({
|
||||||
|
...prompt,
|
||||||
|
prompt: prompt.prompt.replace(
|
||||||
|
new RegExp(`\\[@agent:${agentName}\\]\\(#mention\\)`, 'g'),
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update start agent if it was one of the deleted agents (same logic as regular agent deletion)
|
||||||
|
if (startAgentBeingDeleted) {
|
||||||
|
// Set to first available agent, or empty string if no agents left
|
||||||
|
draft.workflow.startAgent = draft.workflow.agents.length > 0
|
||||||
|
? draft.workflow.agents[0].name
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the pipeline itself
|
||||||
|
draft.workflow.pipelines = draft.workflow.pipelines.filter(
|
||||||
|
(pipeline) => pipeline.name !== action.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
draft.selection = null;
|
||||||
|
draft.pendingChanges = true;
|
||||||
|
draft.chatKey++;
|
||||||
|
break;
|
||||||
|
case "update_pipeline": {
|
||||||
|
if (isLive) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (draft.workflow.pipelines) {
|
||||||
|
draft.workflow.pipelines = draft.workflow.pipelines.map(pipeline =>
|
||||||
|
pipeline.name === action.name ? { ...pipeline, ...action.pipeline } : pipeline
|
||||||
|
);
|
||||||
|
}
|
||||||
|
draft.selection = null;
|
||||||
|
draft.pendingChanges = true;
|
||||||
|
draft.chatKey++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "update_agent": {
|
||||||
if (isLive) {
|
if (isLive) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -432,6 +632,16 @@ function reducer(state: State, action: Action): State {
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// update pipeline references if this agent is part of any pipeline
|
||||||
|
if (draft.workflow.pipelines) {
|
||||||
|
draft.workflow.pipelines = draft.workflow.pipelines.map(pipeline => ({
|
||||||
|
...pipeline,
|
||||||
|
agents: pipeline.agents.map(agentName =>
|
||||||
|
agentName === action.name ? action.agent.name! : agentName
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
// update the selection pointer if this is the selected agent
|
// update the selection pointer if this is the selected agent
|
||||||
if (draft.selection?.type === "agent" && draft.selection.name === action.name) {
|
if (draft.selection?.type === "agent" && draft.selection.name === action.name) {
|
||||||
draft.selection = {
|
draft.selection = {
|
||||||
|
|
@ -449,6 +659,7 @@ function reducer(state: State, action: Action): State {
|
||||||
draft.pendingChanges = true;
|
draft.pendingChanges = true;
|
||||||
draft.chatKey++;
|
draft.chatKey++;
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case "update_tool":
|
case "update_tool":
|
||||||
if (isLive) {
|
if (isLive) {
|
||||||
break;
|
break;
|
||||||
|
|
@ -785,10 +996,59 @@ export function WorkflowEditor({
|
||||||
dispatch({ type: "add_prompt", prompt });
|
dispatch({ type: "add_prompt", prompt });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleSelectPipeline(name: string) {
|
||||||
|
dispatch({ type: "select_pipeline", name });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAddPipeline(pipeline: Partial<z.infer<typeof WorkflowPipeline>> = {}) {
|
||||||
|
dispatch({ type: "add_pipeline", pipeline });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeletePipeline(name: string) {
|
||||||
|
if (window.confirm(`Are you sure you want to delete the pipeline "${name}"?`)) {
|
||||||
|
dispatch({ type: "delete_pipeline", name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAddAgentToPipeline(pipelineName: string) {
|
||||||
|
// Create a pipeline agent and add it to the specified pipeline
|
||||||
|
const newAgentName = `${pipelineName} Step ${(state.present.workflow.pipelines?.find(p => p.name === pipelineName)?.agents.length || 0) + 1}`;
|
||||||
|
|
||||||
|
const agentWithModel = {
|
||||||
|
name: newAgentName,
|
||||||
|
type: 'pipeline' as const,
|
||||||
|
outputVisibility: 'internal' as const,
|
||||||
|
model: defaultModel || "gpt-4o"
|
||||||
|
};
|
||||||
|
|
||||||
|
// First add the agent
|
||||||
|
dispatch({ type: "add_agent", agent: agentWithModel });
|
||||||
|
|
||||||
|
// Then add it to the pipeline
|
||||||
|
const pipeline = state.present.workflow.pipelines?.find(p => p.name === pipelineName);
|
||||||
|
if (pipeline) {
|
||||||
|
dispatch({
|
||||||
|
type: "update_pipeline",
|
||||||
|
name: pipelineName,
|
||||||
|
pipeline: {
|
||||||
|
...pipeline,
|
||||||
|
agents: [...pipeline.agents, newAgentName]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select the newly created agent to open it in agent_config
|
||||||
|
dispatch({ type: "select_agent", name: newAgentName });
|
||||||
|
}
|
||||||
|
|
||||||
function handleUpdateAgent(name: string, agent: Partial<z.infer<typeof WorkflowAgent>>) {
|
function handleUpdateAgent(name: string, agent: Partial<z.infer<typeof WorkflowAgent>>) {
|
||||||
dispatch({ type: "update_agent", name, agent });
|
dispatch({ type: "update_agent", name, agent });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleUpdatePipeline(name: string, pipeline: Partial<z.infer<typeof WorkflowPipeline>>) {
|
||||||
|
dispatch({ type: "update_pipeline", name, pipeline });
|
||||||
|
}
|
||||||
|
|
||||||
function handleDeleteAgent(name: string) {
|
function handleDeleteAgent(name: string) {
|
||||||
if (window.confirm(`Are you sure you want to delete the agent "${name}"?`)) {
|
if (window.confirm(`Are you sure you want to delete the agent "${name}"?`)) {
|
||||||
dispatch({ type: "delete_agent", name });
|
dispatch({ type: "delete_agent", name });
|
||||||
|
|
@ -835,6 +1095,18 @@ export function WorkflowEditor({
|
||||||
dispatch({ type: "reorder_agents", agents });
|
dispatch({ type: "reorder_agents", agents });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleReorderPipelines(pipelines: z.infer<typeof WorkflowPipeline>[]) {
|
||||||
|
// Save order to localStorage
|
||||||
|
const orderMap = pipelines.reduce((acc, pipeline, index) => {
|
||||||
|
acc[pipeline.name] = index;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, number>);
|
||||||
|
const mode = isLive ? 'live' : 'draft';
|
||||||
|
localStorage.setItem(`${mode}_workflow_${projectId}_pipeline_order`, JSON.stringify(orderMap));
|
||||||
|
|
||||||
|
dispatch({ type: "reorder_pipelines", pipelines });
|
||||||
|
}
|
||||||
|
|
||||||
async function handlePublishWorkflow() {
|
async function handlePublishWorkflow() {
|
||||||
await publishWorkflow(projectId, state.present.workflow);
|
await publishWorkflow(projectId, state.present.workflow);
|
||||||
onChangeMode('live');
|
onChangeMode('live');
|
||||||
|
|
@ -1110,6 +1382,7 @@ export function WorkflowEditor({
|
||||||
agents={state.present.workflow.agents}
|
agents={state.present.workflow.agents}
|
||||||
tools={state.present.workflow.tools}
|
tools={state.present.workflow.tools}
|
||||||
prompts={state.present.workflow.prompts}
|
prompts={state.present.workflow.prompts}
|
||||||
|
pipelines={state.present.workflow.pipelines || []}
|
||||||
dataSources={dataSources}
|
dataSources={dataSources}
|
||||||
workflow={state.present.workflow}
|
workflow={state.present.workflow}
|
||||||
selectedEntity={
|
selectedEntity={
|
||||||
|
|
@ -1117,7 +1390,8 @@ export function WorkflowEditor({
|
||||||
(state.present.selection.type === "agent" ||
|
(state.present.selection.type === "agent" ||
|
||||||
state.present.selection.type === "tool" ||
|
state.present.selection.type === "tool" ||
|
||||||
state.present.selection.type === "prompt" ||
|
state.present.selection.type === "prompt" ||
|
||||||
state.present.selection.type === "datasource")
|
state.present.selection.type === "datasource" ||
|
||||||
|
state.present.selection.type === "pipeline")
|
||||||
? state.present.selection
|
? state.present.selection
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
|
@ -1125,21 +1399,26 @@ export function WorkflowEditor({
|
||||||
onSelectAgent={handleSelectAgent}
|
onSelectAgent={handleSelectAgent}
|
||||||
onSelectTool={handleSelectTool}
|
onSelectTool={handleSelectTool}
|
||||||
onSelectPrompt={handleSelectPrompt}
|
onSelectPrompt={handleSelectPrompt}
|
||||||
|
onSelectPipeline={handleSelectPipeline}
|
||||||
onSelectDataSource={handleSelectDataSource}
|
onSelectDataSource={handleSelectDataSource}
|
||||||
onAddAgent={handleAddAgent}
|
onAddAgent={handleAddAgent}
|
||||||
onAddTool={handleAddTool}
|
onAddTool={handleAddTool}
|
||||||
onAddPrompt={handleAddPrompt}
|
onAddPrompt={handleAddPrompt}
|
||||||
|
onAddPipeline={handleAddPipeline}
|
||||||
|
onAddAgentToPipeline={handleAddAgentToPipeline}
|
||||||
onToggleAgent={handleToggleAgent}
|
onToggleAgent={handleToggleAgent}
|
||||||
onSetMainAgent={handleSetMainAgent}
|
onSetMainAgent={handleSetMainAgent}
|
||||||
onDeleteAgent={handleDeleteAgent}
|
onDeleteAgent={handleDeleteAgent}
|
||||||
onDeleteTool={handleDeleteTool}
|
onDeleteTool={handleDeleteTool}
|
||||||
onDeletePrompt={handleDeletePrompt}
|
onDeletePrompt={handleDeletePrompt}
|
||||||
|
onDeletePipeline={handleDeletePipeline}
|
||||||
onShowVisualise={handleShowVisualise}
|
onShowVisualise={handleShowVisualise}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
onProjectToolsUpdated={onProjectToolsUpdated}
|
onProjectToolsUpdated={onProjectToolsUpdated}
|
||||||
onDataSourcesUpdated={onDataSourcesUpdated}
|
onDataSourcesUpdated={onDataSourcesUpdated}
|
||||||
projectConfig={projectConfig}
|
projectConfig={projectConfig}
|
||||||
onReorderAgents={handleReorderAgents}
|
onReorderAgents={handleReorderAgents}
|
||||||
|
onReorderPipelines={handleReorderPipelines}
|
||||||
useRagUploads={useRagUploads}
|
useRagUploads={useRagUploads}
|
||||||
useRagS3Uploads={useRagS3Uploads}
|
useRagS3Uploads={useRagS3Uploads}
|
||||||
useRagScraping={useRagScraping}
|
useRagScraping={useRagScraping}
|
||||||
|
|
@ -1168,6 +1447,7 @@ export function WorkflowEditor({
|
||||||
workflow={state.present.workflow}
|
workflow={state.present.workflow}
|
||||||
agent={state.present.workflow.agents.find((agent) => agent.name === state.present.selection!.name)!}
|
agent={state.present.workflow.agents.find((agent) => agent.name === state.present.selection!.name)!}
|
||||||
usedAgentNames={new Set(state.present.workflow.agents.filter((agent) => agent.name !== state.present.selection!.name).map((agent) => agent.name))}
|
usedAgentNames={new Set(state.present.workflow.agents.filter((agent) => agent.name !== state.present.selection!.name).map((agent) => agent.name))}
|
||||||
|
usedPipelineNames={new Set((state.present.workflow.pipelines || []).map((pipeline) => pipeline.name))}
|
||||||
agents={state.present.workflow.agents}
|
agents={state.present.workflow.agents}
|
||||||
tools={state.present.workflow.tools}
|
tools={state.present.workflow.tools}
|
||||||
prompts={state.present.workflow.prompts}
|
prompts={state.present.workflow.prompts}
|
||||||
|
|
@ -1209,6 +1489,18 @@ export function WorkflowEditor({
|
||||||
handleClose={() => dispatch({ type: "unselect_datasource" })}
|
handleClose={() => dispatch({ type: "unselect_datasource" })}
|
||||||
onDataSourceUpdate={onDataSourcesUpdated}
|
onDataSourceUpdate={onDataSourcesUpdated}
|
||||||
/>}
|
/>}
|
||||||
|
{state.present.selection?.type === "pipeline" && <PipelineConfig
|
||||||
|
key={state.present.selection.name}
|
||||||
|
projectId={projectId}
|
||||||
|
workflow={state.present.workflow}
|
||||||
|
pipeline={state.present.workflow.pipelines?.find((pipeline) => pipeline.name === state.present.selection!.name)!}
|
||||||
|
usedPipelineNames={new Set((state.present.workflow.pipelines || []).filter((pipeline) => pipeline.name !== state.present.selection!.name).map((pipeline) => pipeline.name))}
|
||||||
|
usedAgentNames={new Set(state.present.workflow.agents.map((agent) => agent.name))}
|
||||||
|
agents={state.present.workflow.agents}
|
||||||
|
pipelines={state.present.workflow.pipelines || []}
|
||||||
|
handleUpdate={handleUpdatePipeline.bind(null, state.present.selection.name)}
|
||||||
|
handleClose={() => dispatch({ type: "unselect_pipeline" })}
|
||||||
|
/>}
|
||||||
{state.present.selection?.type === "visualise" && (
|
{state.present.selection?.type === "visualise" && (
|
||||||
<Panel
|
<Panel
|
||||||
title={
|
title={
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue