mirror of
https://github.com/microsoft/autogen.git
synced 2026-01-14 17:37:55 -05:00
ui and bacend improvements
This commit is contained in:
@@ -90,7 +90,7 @@ async def create_workflow(request: CreateWorkflowRequest, db=Depends(get_db)) ->
|
||||
try:
|
||||
workflow = db.upsert(
|
||||
WorkflowDB(
|
||||
config=request.config,
|
||||
config=request.config.model_dump(),
|
||||
user_id=request.user_id,
|
||||
),
|
||||
return_json=False,
|
||||
@@ -136,7 +136,7 @@ async def update_workflow(
|
||||
if request.description is not None:
|
||||
workflow.description = request.description
|
||||
if request.config is not None:
|
||||
workflow.config = request.config
|
||||
workflow.config = request.config.model_dump()
|
||||
updated = db.upsert(workflow, return_json=False)
|
||||
return {"status": updated.status, "data": updated.data}
|
||||
except HTTPException:
|
||||
|
||||
@@ -459,17 +459,28 @@ class WorkflowRunner:
|
||||
# No dependencies, use initial input
|
||||
return initial_input.copy()
|
||||
|
||||
# For sequential workflows: use the most recent dependency's output directly
|
||||
# For parallel/fan-in: this logic would need to be more sophisticated
|
||||
latest_dependency = dependencies[-1] # Most recent dependency
|
||||
dep_execution = execution.step_executions.get(latest_dependency)
|
||||
# For conditional workflows: find the dependency that actually executed and has output
|
||||
# Check all dependencies and find the one that completed successfully
|
||||
completed_dependency = None
|
||||
for dep_id in dependencies:
|
||||
dep_execution = execution.step_executions.get(dep_id)
|
||||
if dep_execution and dep_execution.output_data and dep_execution.status.value == "completed":
|
||||
# Also verify that the edge condition for this dependency is satisfied
|
||||
for edge in workflow.edges:
|
||||
if edge.from_step == dep_id and edge.to_step == step_id:
|
||||
if workflow._evaluate_edge_condition(edge, execution):
|
||||
completed_dependency = dep_id
|
||||
break
|
||||
if completed_dependency:
|
||||
break
|
||||
|
||||
if dep_execution and dep_execution.output_data:
|
||||
# Direct forwarding: previous step's output becomes this step's input
|
||||
if completed_dependency:
|
||||
dep_execution = execution.step_executions.get(completed_dependency)
|
||||
logger.info(f"Using output from completed dependency {completed_dependency} for step {step_id}")
|
||||
return dep_execution.output_data.copy()
|
||||
else:
|
||||
# Fallback to initial input if dependency output not available
|
||||
logger.warning(f"No output available from dependency {latest_dependency} for step {step_id}, using initial input")
|
||||
# Fallback to initial input if no valid dependency output is available
|
||||
logger.warning(f"No valid completed dependency found for step {step_id}, using initial input")
|
||||
return initial_input.copy()
|
||||
|
||||
async def run_step(
|
||||
|
||||
@@ -170,21 +170,37 @@ class BaseWorkflow(ComponentBase[BaseModel]):
|
||||
if step_id == self.start_step_id:
|
||||
ready_steps.append(step_id)
|
||||
elif dependencies:
|
||||
# Check if all dependencies are completed
|
||||
all_deps_complete = True
|
||||
for dep_id in dependencies:
|
||||
dep_exec = execution.step_executions.get(dep_id)
|
||||
if not dep_exec or dep_exec.status.value != "completed":
|
||||
all_deps_complete = False
|
||||
break
|
||||
# Determine if this is a fan-in (AND) or conditional (OR) pattern
|
||||
incoming_edges = [edge for edge in self.edges if edge.to_step == step_id]
|
||||
|
||||
if all_deps_complete:
|
||||
# Also check edge conditions
|
||||
for edge in self.edges:
|
||||
if edge.to_step == step_id:
|
||||
# Fan-in pattern: all edges have "always" conditions
|
||||
is_fan_in = all(edge.condition.type == "always" for edge in incoming_edges)
|
||||
|
||||
if is_fan_in:
|
||||
# Fan-in (AND logic): all dependencies must be complete
|
||||
all_deps_complete = True
|
||||
for dep_id in dependencies:
|
||||
dep_exec = execution.step_executions.get(dep_id)
|
||||
if not dep_exec or dep_exec.status.value != "completed":
|
||||
all_deps_complete = False
|
||||
break
|
||||
|
||||
if all_deps_complete:
|
||||
ready_steps.append(step_id)
|
||||
else:
|
||||
# Conditional (OR logic): any valid path makes step ready
|
||||
step_ready = False
|
||||
for edge in incoming_edges:
|
||||
# Check if this specific dependency is complete
|
||||
dep_exec = execution.step_executions.get(edge.from_step)
|
||||
if dep_exec and dep_exec.status.value == "completed":
|
||||
# Check if the edge condition passes
|
||||
if self._evaluate_edge_condition(edge, execution):
|
||||
ready_steps.append(step_id)
|
||||
step_ready = True
|
||||
break
|
||||
|
||||
if step_ready:
|
||||
ready_steps.append(step_id)
|
||||
|
||||
return ready_steps
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ Provides the echo chain workflow and its steps from the original example.
|
||||
|
||||
from typing import List
|
||||
from autogen_core import ComponentModel
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .core import Workflow, StepMetadata, WorkflowMetadata
|
||||
from .core import Workflow, StepMetadata, WorkflowMetadata, EdgeCondition
|
||||
from .steps import EchoStep, HttpStep, AgentStep, TransformStep
|
||||
from .steps._http import HttpRequestInput, HttpResponseOutput
|
||||
from .steps._agent import AgentInput, AgentOutput
|
||||
@@ -22,65 +22,179 @@ class MessageOutput(BaseModel):
|
||||
result: str
|
||||
|
||||
|
||||
def _create_echo_steps() -> List[EchoStep]:
|
||||
"""Create the echo chain steps that are reused in both workflow and step library."""
|
||||
class WebpageInput(BaseModel):
|
||||
"""UI-friendly input for webpage summarization workflow."""
|
||||
url: str = Field(
|
||||
default="https://httpbin.org/html",
|
||||
description="URL of the webpage to summarize",
|
||||
examples=["https://example.com", "https://news.ycombinator.com"]
|
||||
)
|
||||
message: str = Field(
|
||||
default="Starting workflow execution",
|
||||
description="Optional message (ignored, for UI compatibility)"
|
||||
)
|
||||
|
||||
|
||||
class CollectedOutput(BaseModel):
|
||||
"""Output model for collected results from multiple processing streams."""
|
||||
collected_results: list[str] = Field(description="List of processed results from parallel streams")
|
||||
total_processed: int = Field(description="Total number of items processed")
|
||||
processing_summary: str = Field(description="Summary of the processing workflow")
|
||||
|
||||
|
||||
def _create_echo_steps():
|
||||
"""Create complex echo chain steps demonstrating parallel processing, validation, and fan-out/fan-in."""
|
||||
|
||||
# Step 1: Receive and format message
|
||||
# Step 1: Receive and broadcast message
|
||||
receive_step = EchoStep(
|
||||
step_id="receive",
|
||||
metadata=StepMetadata(name="Receive Message",description="Initial step to receive a message", tags=["input"]),
|
||||
metadata=StepMetadata(
|
||||
name="Receive Message",
|
||||
description="Initial step to receive and broadcast a message to parallel processing streams",
|
||||
tags=["input", "broadcast"]
|
||||
),
|
||||
input_type=MessageInput,
|
||||
output_type=MessageOutput,
|
||||
prefix="📥 RECEIVED: ",
|
||||
suffix=" [INBOX]",
|
||||
delay_seconds=10
|
||||
suffix=" [BROADCASTING TO PARALLEL STREAMS]",
|
||||
delay_seconds=2
|
||||
)
|
||||
|
||||
# Step 2: Process the message
|
||||
process_step = EchoStep(
|
||||
step_id="process",
|
||||
metadata=StepMetadata(name="Process Message", description="Step to process the received message", tags=["processing"]),
|
||||
# Parallel Processing Steps (Fan-out)
|
||||
process_urgent_step = EchoStep(
|
||||
step_id="process_urgent",
|
||||
metadata=StepMetadata(
|
||||
name="Process Urgent",
|
||||
description="Fast processing stream for urgent messages",
|
||||
tags=["processing", "urgent", "fast"]
|
||||
),
|
||||
input_type=MessageOutput,
|
||||
output_type=MessageOutput,
|
||||
prefix="⚙️ PROCESSING: ",
|
||||
suffix=" [ANALYZED]",
|
||||
delay_seconds=10
|
||||
prefix="🚨 URGENT: ",
|
||||
suffix=" [FAST-TRACKED]",
|
||||
delay_seconds=3 # Fast processing
|
||||
)
|
||||
|
||||
# Step 3: Validate the message
|
||||
validate_step = EchoStep(
|
||||
step_id="validate",
|
||||
metadata=StepMetadata(name="Validate Message", description="Step to validate the processed message", tags=["validation"]),
|
||||
process_standard_step = EchoStep(
|
||||
step_id="process_standard",
|
||||
metadata=StepMetadata(
|
||||
name="Process Standard",
|
||||
description="Standard processing stream for regular messages",
|
||||
tags=["processing", "standard", "medium"]
|
||||
),
|
||||
input_type=MessageOutput,
|
||||
output_type=MessageOutput,
|
||||
prefix="✅ VALIDATED: ",
|
||||
suffix=" [APPROVED]",
|
||||
delay_seconds=15
|
||||
prefix="📋 STANDARD: ",
|
||||
suffix=" [PROCESSED]",
|
||||
delay_seconds=7 # Medium processing
|
||||
)
|
||||
|
||||
# Step 4: Send final message
|
||||
process_detailed_step = EchoStep(
|
||||
step_id="process_detailed",
|
||||
metadata=StepMetadata(
|
||||
name="Process Detailed",
|
||||
description="Detailed processing stream for complex analysis",
|
||||
tags=["processing", "detailed", "slow"]
|
||||
),
|
||||
input_type=MessageOutput,
|
||||
output_type=MessageOutput,
|
||||
prefix="🔍 DETAILED: ",
|
||||
suffix=" [DEEP-ANALYZED]",
|
||||
delay_seconds=12 # Slow, thorough processing
|
||||
)
|
||||
|
||||
# Validation Steps for each processing stream
|
||||
validate_urgent_step = EchoStep(
|
||||
step_id="validate_urgent",
|
||||
metadata=StepMetadata(
|
||||
name="Validate Urgent",
|
||||
description="Quick validation for urgent processing results",
|
||||
tags=["validation", "urgent"]
|
||||
),
|
||||
input_type=MessageOutput,
|
||||
output_type=MessageOutput,
|
||||
prefix="✅ URGENT-VALIDATED: ",
|
||||
suffix=" [APPROVED-FAST]",
|
||||
delay_seconds=1
|
||||
)
|
||||
|
||||
validate_standard_step = EchoStep(
|
||||
step_id="validate_standard",
|
||||
metadata=StepMetadata(
|
||||
name="Validate Standard",
|
||||
description="Standard validation for regular processing results",
|
||||
tags=["validation", "standard"]
|
||||
),
|
||||
input_type=MessageOutput,
|
||||
output_type=MessageOutput,
|
||||
prefix="✅ STANDARD-VALIDATED: ",
|
||||
suffix=" [APPROVED-NORMAL]",
|
||||
delay_seconds=4
|
||||
)
|
||||
|
||||
validate_detailed_step = EchoStep(
|
||||
step_id="validate_detailed",
|
||||
metadata=StepMetadata(
|
||||
name="Validate Detailed",
|
||||
description="Thorough validation for detailed processing results",
|
||||
tags=["validation", "detailed"]
|
||||
),
|
||||
input_type=MessageOutput,
|
||||
output_type=MessageOutput,
|
||||
prefix="✅ DETAILED-VALIDATED: ",
|
||||
suffix=" [APPROVED-THOROUGH]",
|
||||
delay_seconds=6
|
||||
)
|
||||
|
||||
# Collector Step (Fan-in) using TransformStep for serializable aggregation
|
||||
collect_step = TransformStep(
|
||||
step_id="collect",
|
||||
metadata=StepMetadata(
|
||||
name="Collect Results",
|
||||
description="Collect and aggregate results from all parallel processing streams",
|
||||
tags=["collection", "aggregation", "fan-in"]
|
||||
),
|
||||
input_type=MessageOutput, # Will receive from any validation step
|
||||
output_type=CollectedOutput,
|
||||
mappings={
|
||||
"collected_results": ["static:Results from all parallel processing streams"],
|
||||
"total_processed": 3, # Number of parallel streams
|
||||
"processing_summary": "result" # Use the result field from the triggering validation step
|
||||
}
|
||||
)
|
||||
|
||||
# Final Send Step
|
||||
send_step = EchoStep(
|
||||
step_id="send",
|
||||
metadata=StepMetadata(name="Send Message", description="Final step to send the message", tags=["output"]),
|
||||
input_type=MessageOutput,
|
||||
metadata=StepMetadata(
|
||||
name="Send Final Results",
|
||||
description="Send aggregated results from all processing streams",
|
||||
tags=["output", "final"]
|
||||
),
|
||||
input_type=CollectedOutput,
|
||||
output_type=MessageOutput,
|
||||
prefix="📤 SENT: ",
|
||||
suffix=" [DELIVERED]",
|
||||
delay_seconds=15
|
||||
prefix="📤 FINAL RESULTS: ",
|
||||
suffix=" [DELIVERED TO ALL STAKEHOLDERS]",
|
||||
delay_seconds=3
|
||||
)
|
||||
|
||||
return [receive_step, process_step, validate_step, send_step]
|
||||
return [
|
||||
receive_step,
|
||||
process_urgent_step, process_standard_step, process_detailed_step,
|
||||
validate_urgent_step, validate_standard_step, validate_detailed_step,
|
||||
collect_step, send_step
|
||||
]
|
||||
|
||||
|
||||
def create_echo_chain_workflow() -> ComponentModel:
|
||||
"""Create the default echo chain workflow."""
|
||||
"""Create a complex echo chain workflow demonstrating parallel processing, validation, and fan-out/fan-in patterns."""
|
||||
|
||||
workflow = Workflow(
|
||||
metadata=WorkflowMetadata(
|
||||
name="Echo Chain Workflow",
|
||||
description="Chain of echo steps that process and transform a message",
|
||||
version="1.0.0",
|
||||
tags=["demo", "echo", "chain"]
|
||||
name="Complex Echo Processing Workflow",
|
||||
description="Parallel message processing with urgent/standard/detailed streams, validation, and result aggregation",
|
||||
version="2.0.0",
|
||||
tags=["demo", "echo", "parallel", "fan-out", "fan-in", "validation"]
|
||||
)
|
||||
)
|
||||
|
||||
@@ -91,13 +205,38 @@ def create_echo_chain_workflow() -> ComponentModel:
|
||||
for step in steps:
|
||||
workflow.add_step(step)
|
||||
|
||||
# Create linear chain: receive -> process -> validate -> send
|
||||
workflow.add_edge("receive", "process")
|
||||
workflow.add_edge("process", "validate")
|
||||
workflow.add_edge("validate", "send")
|
||||
# Create complex parallel processing pattern:
|
||||
#
|
||||
# receive → [process_urgent, process_standard, process_detailed] (FAN-OUT)
|
||||
# ↓ ↓ ↓
|
||||
# validate_urgent validate_standard validate_detailed
|
||||
# ↓ ↓ ↓
|
||||
# └────────── collect ─────────────┘ (FAN-IN)
|
||||
# ↓
|
||||
# send
|
||||
|
||||
# Set start and end
|
||||
# Set start step
|
||||
workflow.set_start_step("receive")
|
||||
|
||||
# Fan-out: receive broadcasts to all three processing streams
|
||||
workflow.add_edge("receive", "process_urgent")
|
||||
workflow.add_edge("receive", "process_standard")
|
||||
workflow.add_edge("receive", "process_detailed")
|
||||
|
||||
# Each processing stream flows to its validation step
|
||||
workflow.add_edge("process_urgent", "validate_urgent")
|
||||
workflow.add_edge("process_standard", "validate_standard")
|
||||
workflow.add_edge("process_detailed", "validate_detailed")
|
||||
|
||||
# Fan-in: all validation steps feed into the collector
|
||||
workflow.add_edge("validate_urgent", "collect")
|
||||
workflow.add_edge("validate_standard", "collect")
|
||||
workflow.add_edge("validate_detailed", "collect")
|
||||
|
||||
# Final step: collector feeds into send
|
||||
workflow.add_edge("collect", "send")
|
||||
|
||||
# Set end step
|
||||
workflow.add_end_step("send")
|
||||
|
||||
return workflow.dump_component()
|
||||
@@ -118,6 +257,26 @@ def create_simple_agent_workflow() -> ComponentModel:
|
||||
)
|
||||
)
|
||||
|
||||
# Transform UI input to HTTP input
|
||||
input_transform = TransformStep(
|
||||
step_id="input_transform",
|
||||
metadata=StepMetadata(
|
||||
name="Input Transform",
|
||||
description="Transform UI input to HTTP request",
|
||||
tags=["transform", "input"]
|
||||
),
|
||||
input_type=WebpageInput,
|
||||
output_type=HttpRequestInput,
|
||||
mappings={
|
||||
"url": "url", # Extract URL from UI input
|
||||
"method": "static:GET",
|
||||
"timeout": 30,
|
||||
"verify_ssl": True,
|
||||
"headers": {}, # Empty dict for headers
|
||||
"data": {} # Empty dict for data
|
||||
}
|
||||
)
|
||||
|
||||
http_step = HttpStep(
|
||||
step_id="http_fetch",
|
||||
metadata=StepMetadata(
|
||||
@@ -142,7 +301,7 @@ def create_simple_agent_workflow() -> ComponentModel:
|
||||
"model": "static:gpt-4.1-nano",
|
||||
"temperature": 0.3,
|
||||
"max_tokens": 512,
|
||||
"context_data": {"content": "content"}
|
||||
"context_data": {"content": "content"} # Dict with content field mapped to input.content
|
||||
}
|
||||
)
|
||||
|
||||
@@ -155,22 +314,235 @@ def create_simple_agent_workflow() -> ComponentModel:
|
||||
)
|
||||
)
|
||||
|
||||
workflow.add_step(input_transform)
|
||||
workflow.add_step(http_step)
|
||||
workflow.add_step(transform_step)
|
||||
workflow.add_step(agent_step)
|
||||
workflow.add_edge("input_transform", "http_fetch")
|
||||
workflow.add_edge("http_fetch", "transform_to_agent_input")
|
||||
workflow.add_edge("transform_to_agent_input", "agent_summarize")
|
||||
workflow.set_start_step("http_fetch")
|
||||
workflow.set_start_step("input_transform") # Start with UI-friendly input
|
||||
workflow.add_end_step("agent_summarize")
|
||||
|
||||
return workflow.dump_component()
|
||||
|
||||
|
||||
class ConditionalInput(BaseModel):
|
||||
"""Input for conditional workflow."""
|
||||
message: str = Field(description="Message to process")
|
||||
priority: str = Field(
|
||||
default="normal",
|
||||
description="Priority level: urgent, normal, or low",
|
||||
examples=["urgent", "normal", "low"]
|
||||
)
|
||||
enable_validation: bool = Field(
|
||||
default=True,
|
||||
description="Whether to enable validation step"
|
||||
)
|
||||
|
||||
|
||||
def create_conditional_workflow() -> ComponentModel:
|
||||
"""Create a simple conditional workflow demonstrating conditional edges."""
|
||||
|
||||
workflow = Workflow(
|
||||
metadata=WorkflowMetadata(
|
||||
name="Conditional Processing Workflow",
|
||||
description="Demonstrates conditional routing based on message priority and validation settings",
|
||||
version="1.0.0",
|
||||
tags=["demo", "conditional", "routing"]
|
||||
)
|
||||
)
|
||||
|
||||
# Input step for conditional workflow
|
||||
receive_step = EchoStep(
|
||||
step_id="receive_conditional",
|
||||
metadata=StepMetadata(
|
||||
name="Receive Conditional",
|
||||
description="Receive message and prepare for conditional routing",
|
||||
tags=["input"]
|
||||
),
|
||||
input_type=ConditionalInput, # Proper input schema for UI introspection
|
||||
output_type=MessageOutput,
|
||||
prefix="📨 CONDITIONAL INPUT: ",
|
||||
suffix=" [ROUTING BASED ON CONDITIONS]",
|
||||
delay_seconds=1
|
||||
)
|
||||
|
||||
# Fast track for urgent messages
|
||||
urgent_process_step = EchoStep(
|
||||
step_id="urgent_process",
|
||||
metadata=StepMetadata(
|
||||
name="Urgent Process",
|
||||
description="Fast processing for urgent messages",
|
||||
tags=["processing", "urgent"]
|
||||
),
|
||||
input_type=MessageOutput,
|
||||
output_type=MessageOutput,
|
||||
prefix="🚨 URGENT FAST-TRACK: ",
|
||||
suffix=" [EXPEDITED]",
|
||||
delay_seconds=2
|
||||
)
|
||||
|
||||
# Normal processing
|
||||
normal_process_step = EchoStep(
|
||||
step_id="normal_process",
|
||||
metadata=StepMetadata(
|
||||
name="Normal Process",
|
||||
description="Standard processing for normal messages",
|
||||
tags=["processing", "normal"]
|
||||
),
|
||||
input_type=MessageOutput,
|
||||
output_type=MessageOutput,
|
||||
prefix="📋 NORMAL PROCESSING: ",
|
||||
suffix=" [STANDARD]",
|
||||
delay_seconds=5
|
||||
)
|
||||
|
||||
# Low priority processing
|
||||
low_process_step = EchoStep(
|
||||
step_id="low_process",
|
||||
metadata=StepMetadata(
|
||||
name="Low Priority Process",
|
||||
description="Slow processing for low priority messages",
|
||||
tags=["processing", "low"]
|
||||
),
|
||||
input_type=MessageOutput,
|
||||
output_type=MessageOutput,
|
||||
prefix="🐌 LOW PRIORITY: ",
|
||||
suffix=" [BATCH-PROCESSED]",
|
||||
delay_seconds=8
|
||||
)
|
||||
|
||||
# Optional validation step
|
||||
validation_step = EchoStep(
|
||||
step_id="validation",
|
||||
metadata=StepMetadata(
|
||||
name="Validation",
|
||||
description="Optional validation step",
|
||||
tags=["validation"]
|
||||
),
|
||||
input_type=MessageOutput,
|
||||
output_type=MessageOutput,
|
||||
prefix="✅ VALIDATED: ",
|
||||
suffix=" [APPROVED]",
|
||||
delay_seconds=3
|
||||
)
|
||||
|
||||
# Final delivery
|
||||
deliver_step = EchoStep(
|
||||
step_id="deliver",
|
||||
metadata=StepMetadata(
|
||||
name="Deliver",
|
||||
description="Final delivery step",
|
||||
tags=["output"]
|
||||
),
|
||||
input_type=MessageOutput,
|
||||
output_type=MessageOutput,
|
||||
prefix="📦 DELIVERED: ",
|
||||
suffix=" [COMPLETE]",
|
||||
delay_seconds=1
|
||||
)
|
||||
|
||||
# Add steps
|
||||
workflow.add_step(receive_step)
|
||||
workflow.add_step(urgent_process_step)
|
||||
workflow.add_step(normal_process_step)
|
||||
workflow.add_step(low_process_step)
|
||||
workflow.add_step(validation_step)
|
||||
workflow.add_step(deliver_step)
|
||||
|
||||
# Set start step
|
||||
workflow.set_start_step("receive_conditional")
|
||||
|
||||
# Conditional edges based on priority from initial input (state-based)
|
||||
urgent_condition = EdgeCondition(
|
||||
type="state_based",
|
||||
field="priority",
|
||||
operator="==",
|
||||
value="urgent"
|
||||
)
|
||||
workflow.add_edge(
|
||||
"receive_conditional", "urgent_process",
|
||||
condition=urgent_condition.model_dump()
|
||||
)
|
||||
|
||||
normal_condition = EdgeCondition(
|
||||
type="state_based",
|
||||
field="priority",
|
||||
operator="==",
|
||||
value="normal"
|
||||
)
|
||||
workflow.add_edge(
|
||||
"receive_conditional", "normal_process",
|
||||
condition=normal_condition.model_dump()
|
||||
)
|
||||
|
||||
low_condition = EdgeCondition(
|
||||
type="state_based",
|
||||
field="priority",
|
||||
operator="==",
|
||||
value="low"
|
||||
)
|
||||
workflow.add_edge(
|
||||
"receive_conditional", "low_process",
|
||||
condition=low_condition.model_dump()
|
||||
)
|
||||
|
||||
# Conditional validation (only if enable_validation is True)
|
||||
validation_enabled_condition = EdgeCondition(
|
||||
type="state_based",
|
||||
field="enable_validation",
|
||||
operator="==",
|
||||
value=True
|
||||
)
|
||||
workflow.add_edge(
|
||||
"urgent_process", "validation",
|
||||
condition=validation_enabled_condition.model_dump()
|
||||
)
|
||||
workflow.add_edge(
|
||||
"normal_process", "validation",
|
||||
condition=validation_enabled_condition.model_dump()
|
||||
)
|
||||
workflow.add_edge(
|
||||
"low_process", "validation",
|
||||
condition=validation_enabled_condition.model_dump()
|
||||
)
|
||||
|
||||
# Skip validation if disabled
|
||||
validation_disabled_condition = EdgeCondition(
|
||||
type="state_based",
|
||||
field="enable_validation",
|
||||
operator="==",
|
||||
value=False
|
||||
)
|
||||
workflow.add_edge(
|
||||
"urgent_process", "deliver",
|
||||
condition=validation_disabled_condition.model_dump()
|
||||
)
|
||||
workflow.add_edge(
|
||||
"normal_process", "deliver",
|
||||
condition=validation_disabled_condition.model_dump()
|
||||
)
|
||||
workflow.add_edge(
|
||||
"low_process", "deliver",
|
||||
condition=validation_disabled_condition.model_dump()
|
||||
)
|
||||
|
||||
# Validation to delivery
|
||||
workflow.add_edge("validation", "deliver")
|
||||
|
||||
# Set end step
|
||||
workflow.add_end_step("deliver")
|
||||
|
||||
return workflow.dump_component()
|
||||
|
||||
|
||||
def get_default_workflows() -> List[ComponentModel]:
|
||||
"""Get the default workflows."""
|
||||
return [
|
||||
create_echo_chain_workflow(),
|
||||
create_simple_agent_workflow(),
|
||||
create_conditional_workflow(),
|
||||
]
|
||||
|
||||
|
||||
@@ -3,11 +3,10 @@ Webpage summarization workflow example: HTTP fetch → Agent summarize
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from typing import Any, Dict
|
||||
from pydantic import BaseModel
|
||||
|
||||
from autogenstudio.workflow import Workflow, WorkflowRunner, WorkflowMetadata, StepMetadata
|
||||
from autogenstudio.workflow.steps import HttpStep, AgentStep, FunctionStep, TransformStep
|
||||
from autogenstudio.workflow.steps import HttpStep, AgentStep, TransformStep
|
||||
from autogenstudio.workflow.steps._http import HttpRequestInput, HttpResponseOutput
|
||||
from autogenstudio.workflow.steps._agent import AgentInput, AgentOutput
|
||||
from autogenstudio.workflow.core._models import Context
|
||||
|
||||
@@ -54,15 +54,36 @@ class TransformStep(Component[TransformStepConfig], BaseStep[Any, Any]):
|
||||
"""
|
||||
Execute the transform: map fields from input to output according to config.
|
||||
"""
|
||||
from ..schema_utils import coerce_value_to_schema_type
|
||||
|
||||
output_kwargs = {}
|
||||
output_schema = self.output_type.model_json_schema()
|
||||
|
||||
for out_field, in_field in self.mappings.items():
|
||||
# Get the raw value
|
||||
if isinstance(in_field, str):
|
||||
if in_field.startswith("static:"):
|
||||
output_kwargs[out_field] = in_field[len("static:"):]
|
||||
raw_value = in_field[len("static:"):]
|
||||
else:
|
||||
output_kwargs[out_field] = getattr(input_data, in_field, None)
|
||||
raw_value = getattr(input_data, in_field, None)
|
||||
elif isinstance(in_field, dict):
|
||||
# Handle dict mappings with nested field resolution
|
||||
raw_value = {}
|
||||
for dict_key, dict_field in in_field.items():
|
||||
if isinstance(dict_field, str):
|
||||
if dict_field.startswith("static:"):
|
||||
raw_value[dict_key] = dict_field[len("static:"):]
|
||||
else:
|
||||
raw_value[dict_key] = getattr(input_data, dict_field, None)
|
||||
else:
|
||||
raw_value[dict_key] = dict_field
|
||||
else:
|
||||
output_kwargs[out_field] = in_field
|
||||
raw_value = in_field
|
||||
|
||||
# Apply defensive type coercion using shared utility
|
||||
coerced_value = coerce_value_to_schema_type(raw_value, out_field, output_schema)
|
||||
output_kwargs[out_field] = coerced_value
|
||||
|
||||
return self.output_type(**output_kwargs)
|
||||
|
||||
def _to_config(self) -> TransformStepConfig:
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
Beaker,
|
||||
LucideBeaker,
|
||||
FlaskConical,
|
||||
GitBranch,
|
||||
} from "lucide-react";
|
||||
import Icon from "./icons";
|
||||
import { BeakerIcon } from "@heroicons/react/24/outline";
|
||||
@@ -36,6 +37,14 @@ const navigation: INavItem[] = [
|
||||
icon: Bot,
|
||||
breadcrumbs: [{ name: "Team Builder", href: "/build", current: true }],
|
||||
},
|
||||
{
|
||||
name: "Workflows",
|
||||
href: "/workflow",
|
||||
icon: GitBranch,
|
||||
breadcrumbs: [
|
||||
{ name: "Workflows (Experimental)", href: "/workflow", current: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Playground",
|
||||
href: "/",
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Form, Input, Select, Checkbox, InputNumber, Button, Alert } from 'antd';
|
||||
import { PlayCircleOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
extractWorkflowInputSchema,
|
||||
generateDefaultInputValues,
|
||||
validateInputValues,
|
||||
WorkflowInputField
|
||||
} from './workflowInputUtils';
|
||||
|
||||
interface WorkflowInputFormProps {
|
||||
workflow: any;
|
||||
visible: boolean;
|
||||
onSubmit: (input: Record<string, any>) => void;
|
||||
onCancel: () => void;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export const WorkflowInputForm: React.FC<WorkflowInputFormProps> = ({
|
||||
workflow,
|
||||
visible,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
loading = false
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [inputFields, setInputFields] = useState<WorkflowInputField[]>([]);
|
||||
const [validationErrors, setValidationErrors] = useState<string[]>([]);
|
||||
|
||||
// Extract input schema when workflow changes
|
||||
useEffect(() => {
|
||||
if (workflow) {
|
||||
const fields = extractWorkflowInputSchema(workflow);
|
||||
setInputFields(fields);
|
||||
|
||||
// Set default values
|
||||
const defaultValues = generateDefaultInputValues(fields);
|
||||
form.setFieldsValue(defaultValues);
|
||||
}
|
||||
}, [workflow, form]);
|
||||
|
||||
const handleSubmit = () => {
|
||||
form.validateFields().then(values => {
|
||||
// Additional validation using our schema
|
||||
const validation = validateInputValues(inputFields, values);
|
||||
|
||||
if (!validation.isValid) {
|
||||
setValidationErrors(validation.errors);
|
||||
return;
|
||||
}
|
||||
|
||||
setValidationErrors([]);
|
||||
onSubmit(values);
|
||||
}).catch(error => {
|
||||
console.error('Form validation failed:', error);
|
||||
});
|
||||
};
|
||||
|
||||
const renderFormField = (field: WorkflowInputField) => {
|
||||
const { name, type, enum: enumValues, description, examples, required, title } = field;
|
||||
|
||||
const rules = [
|
||||
{ required, message: `${title || name} is required` }
|
||||
];
|
||||
|
||||
if (enumValues && enumValues.length > 0) {
|
||||
// Dropdown for enum fields (like priority)
|
||||
return (
|
||||
<Form.Item
|
||||
key={name}
|
||||
name={name}
|
||||
label={title || name.charAt(0).toUpperCase() + name.slice(1)}
|
||||
rules={rules}
|
||||
tooltip={description}
|
||||
>
|
||||
<Select
|
||||
placeholder={`Select ${title || name}`}
|
||||
options={enumValues.map(option => ({
|
||||
label: option.charAt(0).toUpperCase() + option.slice(1),
|
||||
value: option
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'boolean') {
|
||||
// Checkbox for boolean fields (like enable_validation)
|
||||
return (
|
||||
<Form.Item
|
||||
key={name}
|
||||
name={name}
|
||||
valuePropName="checked"
|
||||
label={title || name.charAt(0).toUpperCase() + name.slice(1)}
|
||||
tooltip={description}
|
||||
>
|
||||
<Checkbox>
|
||||
{description || `Enable ${title || name}`}
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'number') {
|
||||
// Number input
|
||||
return (
|
||||
<Form.Item
|
||||
key={name}
|
||||
name={name}
|
||||
label={title || name.charAt(0).toUpperCase() + name.slice(1)}
|
||||
rules={[
|
||||
...rules,
|
||||
{ type: 'number', message: `${title || name} must be a number` }
|
||||
]}
|
||||
tooltip={description}
|
||||
>
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
placeholder={examples?.[0] || `Enter ${title || name}`}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
// Default to text input
|
||||
return (
|
||||
<Form.Item
|
||||
key={name}
|
||||
name={name}
|
||||
label={title || name.charAt(0).toUpperCase() + name.slice(1)}
|
||||
rules={rules}
|
||||
tooltip={description}
|
||||
>
|
||||
<Input
|
||||
placeholder={examples?.[0] || `Enter ${title || name}`}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
};
|
||||
|
||||
const workflowName = workflow?.config?.config?.name || 'Workflow';
|
||||
const workflowDescription = workflow?.config?.config?.description;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<PlayCircleOutlined />
|
||||
<span>Run {workflowName}</span>
|
||||
</div>
|
||||
}
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button
|
||||
key="submit"
|
||||
type="primary"
|
||||
loading={loading}
|
||||
onClick={handleSubmit}
|
||||
icon={<PlayCircleOutlined />}
|
||||
>
|
||||
{loading ? 'Running...' : 'Run Workflow'}
|
||||
</Button>
|
||||
]}
|
||||
width={600}
|
||||
destroyOnClose
|
||||
>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
{workflowDescription && (
|
||||
<p style={{ color: '#666', marginBottom: 16 }}>
|
||||
{workflowDescription}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{validationErrors.length > 0 && (
|
||||
<Alert
|
||||
message="Validation Error"
|
||||
description={
|
||||
<ul style={{ margin: 0, paddingLeft: 16 }}>
|
||||
{validationErrors.map((error, index) => (
|
||||
<li key={index}>{error}</li>
|
||||
))}
|
||||
</ul>
|
||||
}
|
||||
type="error"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
>
|
||||
{inputFields.map(renderFormField)}
|
||||
</Form>
|
||||
|
||||
{inputFields.length === 0 && (
|
||||
<div style={{ textAlign: 'center', padding: '20px 0', color: '#999' }}>
|
||||
No input parameters required for this workflow.
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkflowInputForm;
|
||||
@@ -4,6 +4,8 @@ import React, {
|
||||
useState,
|
||||
useContext,
|
||||
useRef,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
} from "react";
|
||||
import {
|
||||
ReactFlow,
|
||||
@@ -45,6 +47,7 @@ import { StepNode } from "./nodes";
|
||||
import { Toolbar } from "./toolbar";
|
||||
import { StepDetails } from "./step-details";
|
||||
import { useWorkflowWebSocket } from "./useWorkflowWebSocket";
|
||||
import WorkflowInputForm from "./WorkflowInputForm";
|
||||
import { workflowAPI } from "./api";
|
||||
import { appContext } from "../../../hooks/provider";
|
||||
import {
|
||||
@@ -54,6 +57,7 @@ import {
|
||||
saveNodePosition,
|
||||
removeNodePosition,
|
||||
calculateNodePosition,
|
||||
getDagreLayoutedNodes, // <-- import dagre layout util
|
||||
} from "./utils";
|
||||
import { Component } from "../../types/datamodel";
|
||||
import { MonacoEditor } from "../monaco";
|
||||
@@ -71,27 +75,33 @@ interface WorkflowBuilderProps {
|
||||
onDirtyStateChange?: (isDirty: boolean) => void;
|
||||
}
|
||||
|
||||
export const WorkflowBuilder: React.FC<WorkflowBuilderProps> = ({
|
||||
workflow,
|
||||
onChange,
|
||||
onSave,
|
||||
onDirtyStateChange,
|
||||
}) => {
|
||||
export interface WorkflowBuilderHandle {
|
||||
resetUIState: () => void;
|
||||
}
|
||||
|
||||
export const WorkflowBuilder = forwardRef<
|
||||
WorkflowBuilderHandle,
|
||||
WorkflowBuilderProps
|
||||
>(({ workflow, onChange, onSave, onDirtyStateChange }, ref) => {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<Node<NodeData>>([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
|
||||
const [isLibraryCompact, setIsLibraryCompact] = useState(false);
|
||||
const [showMiniMap, setShowMiniMap] = useState(false);
|
||||
const [showGrid, setShowGrid] = useState(true);
|
||||
const [isDirty, setIsDirty] = useState(false);
|
||||
// Add state for error details expand/collapse
|
||||
const [showErrorDetails, setShowErrorDetails] = useState(false);
|
||||
const [selectedStep, setSelectedStep] = useState<StepConfig | null>(null);
|
||||
const [selectedStepExecution, setSelectedStepExecution] = useState<
|
||||
StepExecution | undefined
|
||||
>(undefined);
|
||||
const [stepDetailsOpen, setStepDetailsOpen] = useState(false);
|
||||
const [edgeType, setEdgeType] = useState<string>("smoothstep");
|
||||
const [edgeType, setEdgeType] = useState<string>("default");
|
||||
const [isJsonMode, setIsJsonMode] = useState(false);
|
||||
const [workingCopy, setWorkingCopy] = useState<Workflow>(workflow);
|
||||
const editorRef = useRef(null);
|
||||
const [showInputForm, setShowInputForm] = useState(false);
|
||||
const [isStartingWorkflow, setIsStartingWorkflow] = useState(false);
|
||||
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const { user } = useContext(appContext);
|
||||
@@ -321,7 +331,7 @@ export const WorkflowBuilder: React.FC<WorkflowBuilderProps> = ({
|
||||
addEdge(
|
||||
{
|
||||
...params,
|
||||
type: edgeType,
|
||||
type: edgeType, // will be 'bezier' or 'step'
|
||||
},
|
||||
eds
|
||||
)
|
||||
@@ -482,15 +492,67 @@ export const WorkflowBuilder: React.FC<WorkflowBuilderProps> = ({
|
||||
? parseInt(workflow.id, 10)
|
||||
: workflow.id || 0;
|
||||
|
||||
const flowNodes = convertToReactFlowNodes(
|
||||
let flowNodes = convertToReactFlowNodes(
|
||||
workflow.config.config,
|
||||
workflowId,
|
||||
handleDeleteStep,
|
||||
handleStepClick
|
||||
);
|
||||
const flowEdges = convertToReactFlowEdges(workflow.config.config, edgeType);
|
||||
setNodes(flowNodes);
|
||||
setEdges(flowEdges);
|
||||
|
||||
// Only apply dagre to nodes that do NOT have a saved position
|
||||
const positions = JSON.parse(
|
||||
localStorage.getItem(`workflow-${workflowId}-positions`) || "{}"
|
||||
);
|
||||
const nodesWithoutSavedPos = flowNodes.filter((n) => !positions[n.id]);
|
||||
if (nodesWithoutSavedPos.length > 1 && flowEdges.length > 0) {
|
||||
const dagreLayouted = getDagreLayoutedNodes(
|
||||
nodesWithoutSavedPos,
|
||||
flowEdges,
|
||||
"LR"
|
||||
);
|
||||
// Merge dagre positions into flowNodes
|
||||
flowNodes = flowNodes.map((n) => {
|
||||
const dagreNode = dagreLayouted.find((dn) => dn.id === n.id);
|
||||
return dagreNode ? { ...n, position: dagreNode.position } : n;
|
||||
});
|
||||
}
|
||||
|
||||
// Preserve existing execution state when recreating nodes
|
||||
setNodes((currentNodes) => {
|
||||
const nodeMap = new Map(currentNodes.map((node) => [node.id, node]));
|
||||
return flowNodes.map((newNode) => {
|
||||
const existingNode = nodeMap.get(newNode.id);
|
||||
if (existingNode) {
|
||||
// Preserve execution state but update other data
|
||||
return {
|
||||
...newNode,
|
||||
data: {
|
||||
...newNode.data,
|
||||
executionStatus: existingNode.data.executionStatus,
|
||||
executionData: existingNode.data.executionData,
|
||||
},
|
||||
};
|
||||
}
|
||||
return newNode;
|
||||
});
|
||||
});
|
||||
|
||||
setEdges((currentEdges) => {
|
||||
const edgeMap = new Map(currentEdges.map((edge) => [edge.id, edge]));
|
||||
return flowEdges.map((newEdge) => {
|
||||
const existingEdge = edgeMap.get(newEdge.id);
|
||||
if (existingEdge) {
|
||||
// Preserve existing style and animation state
|
||||
return {
|
||||
...newEdge,
|
||||
style: existingEdge.style,
|
||||
animated: existingEdge.animated,
|
||||
};
|
||||
}
|
||||
return newEdge;
|
||||
});
|
||||
});
|
||||
}, [
|
||||
workflow.config,
|
||||
workflow.id,
|
||||
@@ -511,7 +573,7 @@ export const WorkflowBuilder: React.FC<WorkflowBuilderProps> = ({
|
||||
}
|
||||
}, [workflow, onSave, messageApi]);
|
||||
|
||||
const handleRunWorkflow = useCallback(async () => {
|
||||
const handleRunWorkflow = useCallback(() => {
|
||||
if (!user?.id) {
|
||||
messageApi.error("User not authenticated");
|
||||
return;
|
||||
@@ -522,61 +584,66 @@ export const WorkflowBuilder: React.FC<WorkflowBuilderProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
messageApi.loading("Starting workflow execution...", 0);
|
||||
// Show the input form modal
|
||||
setShowInputForm(true);
|
||||
}, [user?.id, workflow.config.config.steps, messageApi]);
|
||||
|
||||
// Reset WebSocket state first
|
||||
resetState();
|
||||
const handleWorkflowInputSubmit = useCallback(
|
||||
async (input: Record<string, any>) => {
|
||||
try {
|
||||
setIsStartingWorkflow(true);
|
||||
messageApi.loading("Starting workflow execution...", 0);
|
||||
|
||||
// Reset all node and edge states to initial state
|
||||
setNodes((currentNodes) =>
|
||||
currentNodes.map((node) => ({
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
executionStatus: undefined, // Reset to no status
|
||||
},
|
||||
}))
|
||||
);
|
||||
// Reset WebSocket state first
|
||||
resetState();
|
||||
|
||||
setEdges((currentEdges) =>
|
||||
currentEdges.map((edge) => ({
|
||||
...edge,
|
||||
style: { stroke: "#6b7280", strokeWidth: 2 }, // Reset to default style
|
||||
animated: false,
|
||||
}))
|
||||
);
|
||||
// Reset all node and edge states to initial state
|
||||
setNodes((currentNodes) =>
|
||||
currentNodes.map((node) => ({
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
executionStatus: undefined, // Reset to no status
|
||||
},
|
||||
}))
|
||||
);
|
||||
|
||||
// Create a workflow run
|
||||
const runResponse = await workflowAPI.createWorkflowRun(
|
||||
undefined, // workflowId - use config instead for real-time execution
|
||||
workflow.config
|
||||
);
|
||||
setEdges((currentEdges) =>
|
||||
currentEdges.map((edge) => ({
|
||||
...edge,
|
||||
style: { stroke: "#6b7280", strokeWidth: 2 }, // Reset to default style
|
||||
animated: false,
|
||||
}))
|
||||
);
|
||||
|
||||
const { run_id, workflow_config } = runResponse.data;
|
||||
// Create a workflow run
|
||||
const runResponse = await workflowAPI.createWorkflowRun(
|
||||
undefined, // workflowId - use config instead for real-time execution
|
||||
workflow.config
|
||||
);
|
||||
|
||||
// Start WebSocket-based execution
|
||||
startWorkflow(run_id, workflow_config, {
|
||||
// Optional initial input - could be made configurable
|
||||
message: "Starting workflow execution",
|
||||
});
|
||||
const { run_id, workflow_config } = runResponse.data;
|
||||
|
||||
messageApi.destroy(); // Clear loading message
|
||||
messageApi.success("Workflow execution started!");
|
||||
} catch (error) {
|
||||
messageApi.destroy();
|
||||
console.error("Error starting workflow:", error);
|
||||
messageApi.error("Failed to start workflow execution");
|
||||
}
|
||||
}, [
|
||||
user?.id,
|
||||
workflow.config,
|
||||
startWorkflow,
|
||||
resetState,
|
||||
messageApi,
|
||||
setNodes,
|
||||
setEdges,
|
||||
]);
|
||||
// Start WebSocket-based execution with user input
|
||||
startWorkflow(run_id, workflow_config, input);
|
||||
|
||||
messageApi.destroy(); // Clear loading message
|
||||
messageApi.success("Workflow execution started!");
|
||||
setShowInputForm(false);
|
||||
} catch (error) {
|
||||
messageApi.destroy();
|
||||
console.error("Error starting workflow:", error);
|
||||
messageApi.error("Failed to start workflow execution");
|
||||
} finally {
|
||||
setIsStartingWorkflow(false);
|
||||
}
|
||||
},
|
||||
[workflow.config, startWorkflow, resetState, messageApi, setNodes, setEdges]
|
||||
);
|
||||
|
||||
const handleWorkflowInputCancel = useCallback(() => {
|
||||
setShowInputForm(false);
|
||||
}, []);
|
||||
|
||||
const handleStopWorkflow = useCallback(() => {
|
||||
stopWorkflow();
|
||||
@@ -592,25 +659,23 @@ export const WorkflowBuilder: React.FC<WorkflowBuilderProps> = ({
|
||||
|
||||
if (isNaN(workflowId)) return;
|
||||
|
||||
workflow.config.config.steps?.forEach((step, index) => {
|
||||
const position = calculateNodePosition(
|
||||
index,
|
||||
workflow.config.config.steps?.length || 0
|
||||
// Use dagre to layout nodes
|
||||
setNodes((currentNodes) => {
|
||||
const flowEdges = convertToReactFlowEdges(
|
||||
workflow.config.config,
|
||||
edgeType
|
||||
);
|
||||
saveNodePosition(workflowId, step.config.step_id, position);
|
||||
const layouted = getDagreLayoutedNodes(currentNodes, flowEdges, "LR");
|
||||
// Persist new positions
|
||||
layouted.forEach((node) => {
|
||||
saveNodePosition(workflowId, node.id, node.position);
|
||||
});
|
||||
return layouted;
|
||||
});
|
||||
|
||||
const flowNodes = convertToReactFlowNodes(
|
||||
workflow.config.config,
|
||||
workflowId,
|
||||
handleDeleteStep,
|
||||
handleStepClick
|
||||
);
|
||||
setNodes(flowNodes);
|
||||
|
||||
messageApi.success("Nodes arranged automatically");
|
||||
}
|
||||
}, [workflow, messageApi, handleDeleteStep, setNodes]);
|
||||
}, [workflow, messageApi, setNodes, edgeType]);
|
||||
|
||||
const handleStepClick = useCallback(
|
||||
(step: StepConfig, executionData?: StepExecution) => {
|
||||
@@ -680,21 +745,47 @@ export const WorkflowBuilder: React.FC<WorkflowBuilderProps> = ({
|
||||
};
|
||||
}, [nodes, handleDeleteStep]);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
resetUIState: () => {
|
||||
setNodes([]);
|
||||
setEdges([]);
|
||||
setIsLibraryCompact(false);
|
||||
setShowMiniMap(false);
|
||||
setShowGrid(true);
|
||||
setIsDirty(false);
|
||||
setSelectedStep(null);
|
||||
setSelectedStepExecution(undefined);
|
||||
setStepDetailsOpen(false);
|
||||
setEdgeType("default");
|
||||
setIsJsonMode(false);
|
||||
setWorkingCopy(workflow);
|
||||
setShowInputForm(false);
|
||||
setIsStartingWorkflow(false);
|
||||
// Reset execution state and disconnect WebSocket
|
||||
disconnect();
|
||||
resetState();
|
||||
},
|
||||
}),
|
||||
[workflow, disconnect, resetState]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-full flex">
|
||||
{contextHolder}
|
||||
|
||||
{/* CSS for edge animations */}
|
||||
<style>{`
|
||||
@keyframes dash {
|
||||
to {
|
||||
stroke-dashoffset: -10;
|
||||
@keyframes dash {
|
||||
to {
|
||||
stroke-dashoffset: -10;
|
||||
}
|
||||
}
|
||||
}
|
||||
.react-flow__edge-path {
|
||||
animation: dash 1s linear infinite;
|
||||
}
|
||||
`}</style>
|
||||
.react-flow__edge-path {
|
||||
animation: dash 1s linear infinite;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
{/* Main Canvas */}
|
||||
<div className="flex-1 relative">
|
||||
@@ -757,6 +848,69 @@ export const WorkflowBuilder: React.FC<WorkflowBuilderProps> = ({
|
||||
{workflow.config.config.edges?.length || 0} connections
|
||||
</div>
|
||||
|
||||
{/* Error Details for Failed Steps (Collapsible) */}
|
||||
{executionState.status === WorkflowStatus.FAILED &&
|
||||
(() => {
|
||||
// Find failed steps with error details
|
||||
const failedSteps = (
|
||||
workflow.config.config.steps || []
|
||||
).filter((step) => {
|
||||
const exec =
|
||||
executionState.execution?.step_executions?.[
|
||||
step.config.step_id
|
||||
];
|
||||
return (
|
||||
exec && exec.status === StepStatus.FAILED && exec.error
|
||||
);
|
||||
});
|
||||
if (!failedSteps.length) return null;
|
||||
return (
|
||||
<div className="mt-2">
|
||||
<button
|
||||
className="text-xs text-red-600 underline hover:text-red-800 focus:outline-none"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
}}
|
||||
onClick={() => setShowErrorDetails((v) => !v)}
|
||||
type="button"
|
||||
>
|
||||
{showErrorDetails ? "Hide" : "Show"} error details (
|
||||
{failedSteps.length} failed step
|
||||
{failedSteps.length > 1 ? "s" : ""})
|
||||
<span style={{ fontSize: 10 }}>
|
||||
{showErrorDetails ? "▲" : "▼"}
|
||||
</span>
|
||||
</button>
|
||||
{showErrorDetails && (
|
||||
<div className="mt-1 bg-red-50 border border-red-200 rounded p-2 max-h-40 overflow-y-auto">
|
||||
{failedSteps.map((step) => {
|
||||
const exec =
|
||||
executionState.execution?.step_executions?.[
|
||||
step.config.step_id
|
||||
];
|
||||
return (
|
||||
<div
|
||||
key={step.config.step_id}
|
||||
className="mb-2 last:mb-0"
|
||||
>
|
||||
<div className="font-semibold text-xs text-red-700">
|
||||
{step.config.metadata.name ||
|
||||
step.config.step_id}
|
||||
</div>
|
||||
<div className="text-xs text-red-800 whitespace-pre-wrap break-all">
|
||||
{exec?.error}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Results Panel: Compact, live-updating step outputs */}
|
||||
{executionState.execution &&
|
||||
workflow.config.config.steps?.length &&
|
||||
@@ -1017,8 +1171,17 @@ export const WorkflowBuilder: React.FC<WorkflowBuilderProps> = ({
|
||||
onClose={() => setStepDetailsOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Workflow Input Form Modal */}
|
||||
<WorkflowInputForm
|
||||
workflow={workflow}
|
||||
visible={showInputForm}
|
||||
onSubmit={handleWorkflowInputSubmit}
|
||||
onCancel={handleWorkflowInputCancel}
|
||||
loading={isStartingWorkflow}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default WorkflowBuilder;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
export { default as FoundryManager } from "./manager";
|
||||
export { default as FoundrySidebar } from "./sidebar";
|
||||
export { default as FoundryBuilder } from "./builder";
|
||||
export { default as NewWorkflowControls } from "./newworkflow";
|
||||
export * from "./types";
|
||||
export * from "./utils";
|
||||
export { foundryAPI } from "./api";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { appContext } from "../../../hooks/provider";
|
||||
import { workflowAPI } from "./api";
|
||||
import { WorkflowSidebar } from "./sidebar";
|
||||
import { Workflow } from "./types";
|
||||
import WorkflowBuilder from "./builder";
|
||||
import WorkflowBuilder, { WorkflowBuilderHandle } from "./builder";
|
||||
|
||||
export const WorkflowManager: React.FC = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -22,6 +22,7 @@ export const WorkflowManager: React.FC = () => {
|
||||
const { user } = useContext(appContext);
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
||||
const workflowBuilderRef = React.useRef<WorkflowBuilderHandle>(null);
|
||||
|
||||
// Persist sidebar state
|
||||
useEffect(() => {
|
||||
@@ -98,6 +99,10 @@ export const WorkflowManager: React.FC = () => {
|
||||
try {
|
||||
const workflow = await workflowAPI.getWorkflow(workflowId, user.id);
|
||||
setCurrentWorkflow(workflow);
|
||||
// Reset all UI state in builder
|
||||
if (workflowBuilderRef.current) {
|
||||
workflowBuilderRef.current.resetUIState();
|
||||
}
|
||||
window.history.pushState({}, "", `?workflowId=${workflowId}`);
|
||||
setHasUnsavedChanges(false);
|
||||
} catch (error) {
|
||||
@@ -134,11 +139,23 @@ export const WorkflowManager: React.FC = () => {
|
||||
name,
|
||||
description: "A new workflow.",
|
||||
config: {
|
||||
id: `config-${Date.now()}`,
|
||||
name,
|
||||
provider: "autogenstudio.workflow.core.Workflow",
|
||||
component_type: "workflow",
|
||||
version: 1,
|
||||
component_version: 1,
|
||||
description: "A new workflow.",
|
||||
steps: [],
|
||||
edges: [],
|
||||
label: "New Workflow",
|
||||
config: {
|
||||
metadata: {
|
||||
name,
|
||||
description: "A new workflow.",
|
||||
version: "1.0.0",
|
||||
tags: [],
|
||||
},
|
||||
steps: [],
|
||||
edges: [],
|
||||
initial_state: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
user.id
|
||||
@@ -146,6 +163,10 @@ export const WorkflowManager: React.FC = () => {
|
||||
|
||||
setWorkflows([newWorkflow, ...workflows]);
|
||||
setCurrentWorkflow(newWorkflow);
|
||||
// Reset all UI state in builder
|
||||
if (workflowBuilderRef.current) {
|
||||
workflowBuilderRef.current.resetUIState();
|
||||
}
|
||||
messageApi.success("Workflow created successfully");
|
||||
} catch (error) {
|
||||
console.error("Error creating workflow:", error);
|
||||
@@ -177,12 +198,11 @@ export const WorkflowManager: React.FC = () => {
|
||||
? parseInt(currentWorkflow.id, 10)
|
||||
: currentWorkflow.id,
|
||||
{
|
||||
id: currentWorkflow.id.toString(), // Convert to string for UpdateWorkflowRequest
|
||||
name: workflowConfig?.name || currentWorkflow.config.config.name,
|
||||
description:
|
||||
workflowConfig?.description ||
|
||||
currentWorkflow.config.config.description,
|
||||
config: workflowData.config?.config || currentWorkflow.config.config,
|
||||
config: workflowData.config || currentWorkflow.config,
|
||||
},
|
||||
user.id
|
||||
);
|
||||
@@ -249,6 +269,7 @@ export const WorkflowManager: React.FC = () => {
|
||||
{currentWorkflow ? (
|
||||
<div className="h-[calc(100vh-120px)]">
|
||||
<WorkflowBuilder
|
||||
ref={workflowBuilderRef}
|
||||
workflow={currentWorkflow}
|
||||
onChange={handleWorkflowChange}
|
||||
onSave={handleSaveWorkflow}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import React from "react";
|
||||
import { Button } from "antd";
|
||||
import { Plus, GitBranch } from "lucide-react";
|
||||
|
||||
interface NewWorkflowControlsProps {
|
||||
isLoading: boolean;
|
||||
onCreateWorkflow: () => void;
|
||||
}
|
||||
|
||||
const NewWorkflowControls = ({
|
||||
isLoading,
|
||||
onCreateWorkflow,
|
||||
}: NewWorkflowControlsProps) => {
|
||||
const handleCreateWorkflow = async () => {
|
||||
await onCreateWorkflow();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2 w-full">
|
||||
<Button
|
||||
type="primary"
|
||||
className="w-full"
|
||||
onClick={handleCreateWorkflow}
|
||||
disabled={isLoading}
|
||||
icon={<Plus className="w-4 h-4" />}
|
||||
>
|
||||
New Workflow
|
||||
</Button>
|
||||
|
||||
<div className="text-xs text-secondary flex items-center justify-center gap-1">
|
||||
<GitBranch className="w-3 h-3" />
|
||||
<span>Graph-based workflow</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewWorkflowControls;
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
import { Workflow } from "./types";
|
||||
import { getRelativeTimeString } from "../atoms";
|
||||
import { getWorkflowTypeColor } from "./utils";
|
||||
import NewWorkflowControls from "./newworkflow";
|
||||
|
||||
interface WorkflowSidebarProps {
|
||||
isOpen: boolean;
|
||||
@@ -93,10 +92,17 @@ export const WorkflowSidebar: React.FC<WorkflowSidebarProps> = ({
|
||||
<div className="my-4 flex text-sm">
|
||||
<div className="mr-2 w-full pr-2">
|
||||
{isOpen && (
|
||||
<NewWorkflowControls
|
||||
isLoading={isLoading}
|
||||
onCreateWorkflow={onCreateWorkflow}
|
||||
/>
|
||||
<div className="space-y-2 w-full">
|
||||
<Button
|
||||
type="primary"
|
||||
className="w-full"
|
||||
onClick={onCreateWorkflow}
|
||||
disabled={isLoading}
|
||||
icon={<Plus className="w-4 h-4" />}
|
||||
>
|
||||
New Workflow
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -79,8 +79,8 @@ export const Toolbar: React.FC<ToolbarProps> = ({
|
||||
<div className="font-semibold text-sm mb-2">Edge Style</div>
|
||||
<Segmented
|
||||
options={[
|
||||
{ label: "Smooth", value: "smoothstep" },
|
||||
{ label: "Straight", value: "straight" },
|
||||
{ label: "Bezier", value: "default" },
|
||||
{ label: "Step", value: "step" },
|
||||
]}
|
||||
value={edgeType}
|
||||
onChange={(value) => onEdgeTypeChange(value as string)}
|
||||
@@ -135,7 +135,7 @@ export const Toolbar: React.FC<ToolbarProps> = ({
|
||||
icon={<Play size={18} />}
|
||||
onClick={onRun}
|
||||
disabled={disabled}
|
||||
className="h-10 w-10 flex items-center justify-center"
|
||||
className="h-10 w-10 flex items-center justify-center"
|
||||
/>
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component } from "../../types/datamodel";
|
||||
import { WorkflowConfig, NodeData, StepConfig, StepExecution } from "./types";
|
||||
import { Node, Edge } from "@xyflow/react";
|
||||
import dagre from "@dagrejs/dagre";
|
||||
|
||||
// Workflow utilities
|
||||
export const createEmptyWorkflow = (
|
||||
@@ -110,3 +111,36 @@ export const addStepToWorkflow = (
|
||||
steps: [...config.steps, { ...step, config: step.config }],
|
||||
};
|
||||
};
|
||||
|
||||
// Layout nodes using dagre (left-to-right)
|
||||
export function getDagreLayoutedNodes(
|
||||
nodes: Node<NodeData>[],
|
||||
edges: Edge[],
|
||||
direction: "LR" | "TB" = "LR"
|
||||
): Node<NodeData>[] {
|
||||
const g = new dagre.graphlib.Graph();
|
||||
g.setDefaultEdgeLabel(() => ({}));
|
||||
g.setGraph({ rankdir: direction });
|
||||
|
||||
// Set nodes with width/height (adjust as needed for your node size)
|
||||
nodes.forEach((node) => {
|
||||
g.setNode(node.id, { width: 220, height: 80 });
|
||||
});
|
||||
|
||||
// Set edges
|
||||
edges.forEach((edge) => {
|
||||
g.setEdge(edge.source, edge.target);
|
||||
});
|
||||
|
||||
dagre.layout(g);
|
||||
|
||||
// Update node positions
|
||||
return nodes.map((node) => {
|
||||
const pos = g.node(node.id);
|
||||
if (!pos) return node;
|
||||
return {
|
||||
...node,
|
||||
position: { x: pos.x - 110, y: pos.y - 40 }, // Center node
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* Utilities for generating dynamic input forms based on workflow schemas
|
||||
*/
|
||||
|
||||
interface WorkflowInputField {
|
||||
name: string;
|
||||
type: 'string' | 'number' | 'boolean' | 'array';
|
||||
required: boolean;
|
||||
default?: any;
|
||||
enum?: string[];
|
||||
description?: string;
|
||||
examples?: any[];
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract input schema from a workflow's start step
|
||||
*/
|
||||
export function extractWorkflowInputSchema(workflow: any): WorkflowInputField[] {
|
||||
try {
|
||||
// Handle both backend API response format and direct workflow config
|
||||
const config = workflow?.config?.config || workflow?.config;
|
||||
if (!config?.steps?.length || !config.start_step_id) {
|
||||
console.warn('No steps or start_step_id found, using message fallback');
|
||||
return [{
|
||||
name: 'message',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Message to process',
|
||||
title: 'Message'
|
||||
}];
|
||||
}
|
||||
|
||||
// Find the start step - handle both component format and direct config format
|
||||
const startStep = config.steps.find((step: any) => {
|
||||
// Handle Component<StepConfig> format from backend
|
||||
const stepId = step.config?.step_id || step.step_id;
|
||||
return stepId === config.start_step_id;
|
||||
});
|
||||
|
||||
if (!startStep) {
|
||||
console.warn(`Start step '${config.start_step_id}' not found, using message fallback`);
|
||||
return [{
|
||||
name: 'message',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Message to process',
|
||||
title: 'Message'
|
||||
}];
|
||||
}
|
||||
|
||||
// Extract input schema - handle both component and direct formats
|
||||
const stepConfig = startStep.config || startStep;
|
||||
const inputSchema = stepConfig.input_schema;
|
||||
|
||||
if (!inputSchema?.properties) {
|
||||
console.warn('No input_schema.properties found, using message fallback');
|
||||
return [{
|
||||
name: 'message',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Message to process',
|
||||
title: 'Message'
|
||||
}];
|
||||
}
|
||||
|
||||
const properties = inputSchema.properties;
|
||||
const required = inputSchema.required || [];
|
||||
|
||||
console.log('Successfully extracted input schema:', { properties, required });
|
||||
|
||||
return Object.entries(properties).map(([fieldName, fieldSchema]: [string, any]) => ({
|
||||
name: fieldName,
|
||||
type: mapJsonTypeToInputType(fieldSchema.type),
|
||||
required: required.includes(fieldName),
|
||||
default: fieldSchema.default,
|
||||
// Use enum if present, otherwise use examples as enum for string fields with limited options
|
||||
enum: fieldSchema.enum || (fieldSchema.type === 'string' && fieldSchema.examples?.length ? fieldSchema.examples : undefined),
|
||||
description: fieldSchema.description,
|
||||
examples: fieldSchema.examples,
|
||||
title: fieldSchema.title || fieldName
|
||||
}));
|
||||
} catch (error) {
|
||||
console.warn('Error extracting workflow input schema:', error);
|
||||
// Fallback to simple message input
|
||||
return [{
|
||||
name: 'message',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Message to process',
|
||||
title: 'Message'
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map JSON schema types to input field types
|
||||
*/
|
||||
function mapJsonTypeToInputType(jsonType: string): 'string' | 'number' | 'boolean' | 'array' {
|
||||
switch (jsonType) {
|
||||
case 'integer':
|
||||
case 'number':
|
||||
return 'number';
|
||||
case 'boolean':
|
||||
return 'boolean';
|
||||
case 'array':
|
||||
return 'array';
|
||||
default:
|
||||
return 'string';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate default input values from schema
|
||||
*/
|
||||
export function generateDefaultInputValues(inputFields: WorkflowInputField[]): Record<string, any> {
|
||||
const defaultValues: Record<string, any> = {};
|
||||
|
||||
inputFields.forEach(field => {
|
||||
if (field.default !== undefined) {
|
||||
defaultValues[field.name] = field.default;
|
||||
} else if (field.required) {
|
||||
// Provide sensible defaults for required fields
|
||||
switch (field.type) {
|
||||
case 'string':
|
||||
defaultValues[field.name] = field.enum?.[0] || '';
|
||||
break;
|
||||
case 'number':
|
||||
defaultValues[field.name] = 0;
|
||||
break;
|
||||
case 'boolean':
|
||||
defaultValues[field.name] = false;
|
||||
break;
|
||||
case 'array':
|
||||
defaultValues[field.name] = [];
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return defaultValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate input values against schema
|
||||
*/
|
||||
export function validateInputValues(
|
||||
inputFields: WorkflowInputField[],
|
||||
values: Record<string, any>
|
||||
): { isValid: boolean; errors: string[] } {
|
||||
const errors: string[] = [];
|
||||
|
||||
inputFields.forEach(field => {
|
||||
const value = values[field.name];
|
||||
|
||||
// Check required fields
|
||||
if (field.required && (value === undefined || value === null || value === '')) {
|
||||
errors.push(`${field.title || field.name} is required`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip validation for empty optional fields
|
||||
if (!field.required && (value === undefined || value === null || value === '')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Type validation
|
||||
switch (field.type) {
|
||||
case 'number':
|
||||
if (isNaN(Number(value))) {
|
||||
errors.push(`${field.title || field.name} must be a number`);
|
||||
}
|
||||
break;
|
||||
case 'boolean':
|
||||
if (typeof value !== 'boolean') {
|
||||
errors.push(`${field.title || field.name} must be true or false`);
|
||||
}
|
||||
break;
|
||||
case 'string':
|
||||
if (field.enum && !field.enum.includes(value)) {
|
||||
errors.push(`${field.title || field.name} must be one of: ${field.enum.join(', ')}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
export type { WorkflowInputField };
|
||||
Reference in New Issue
Block a user