mirror of https://github.com/kortix-ai/suna.git
chore(dev): workflow input nodes, xml examples
This commit is contained in:
parent
1ef38a3bb4
commit
d13d6a55e6
|
@ -42,3 +42,5 @@ aiohttp>=3.9.0
|
|||
email-validator>=2.0.0
|
||||
mailtrap>=2.0.1
|
||||
cryptography>=41.0.0
|
||||
apscheduler>=3.10.0
|
||||
croniter>=1.4.0
|
||||
|
|
|
@ -0,0 +1,214 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify that XML tool examples are correctly included in workflow system prompts.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from workflows.converter import WorkflowConverter
|
||||
from workflows.tool_examples import get_tools_xml_examples
|
||||
|
||||
def test_xml_examples_integration():
|
||||
"""Test that XML tool examples are correctly included in workflow system prompts."""
|
||||
|
||||
print("🧪 Testing XML Tool Examples Integration")
|
||||
print("=" * 50)
|
||||
|
||||
# Create a sample workflow with input node and tools
|
||||
nodes = [
|
||||
{
|
||||
"id": "input-1",
|
||||
"type": "inputNode",
|
||||
"position": {"x": 100, "y": 100},
|
||||
"data": {
|
||||
"label": "Workflow Input",
|
||||
"prompt": "Search for information about AI and create a summary file.",
|
||||
"trigger_type": "MANUAL",
|
||||
"variables": {
|
||||
"topic": "artificial intelligence",
|
||||
"output_format": "markdown"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "agent-1",
|
||||
"type": "agentNode",
|
||||
"position": {"x": 400, "y": 100},
|
||||
"data": {
|
||||
"label": "Research Agent",
|
||||
"instructions": "You are a research assistant that searches for information and creates summaries",
|
||||
"model": "anthropic/claude-3-5-sonnet-latest",
|
||||
"connectedTools": [
|
||||
{"id": "tool-1", "name": "Web Search", "type": "web_search_tool"},
|
||||
{"id": "tool-2", "name": "File Operations", "type": "sb_files_tool"}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "tool-1",
|
||||
"type": "toolConnectionNode",
|
||||
"position": {"x": 100, "y": 200},
|
||||
"data": {
|
||||
"label": "Web Search",
|
||||
"nodeId": "web_search_tool",
|
||||
"description": "Search the web for information"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "tool-2",
|
||||
"type": "toolConnectionNode",
|
||||
"position": {"x": 100, "y": 300},
|
||||
"data": {
|
||||
"label": "File Operations",
|
||||
"nodeId": "sb_files_tool",
|
||||
"description": "Create and manage files"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
edges = [
|
||||
{
|
||||
"id": "e-input-1-agent-1",
|
||||
"source": "input-1",
|
||||
"target": "agent-1",
|
||||
"sourceHandle": "output",
|
||||
"targetHandle": "input"
|
||||
},
|
||||
{
|
||||
"id": "e-tool-1-agent-1",
|
||||
"source": "tool-1",
|
||||
"target": "agent-1",
|
||||
"sourceHandle": "tool-connection",
|
||||
"targetHandle": "tools"
|
||||
},
|
||||
{
|
||||
"id": "e-tool-2-agent-1",
|
||||
"source": "tool-2",
|
||||
"target": "agent-1",
|
||||
"sourceHandle": "tool-connection",
|
||||
"targetHandle": "tools"
|
||||
}
|
||||
]
|
||||
|
||||
metadata = {
|
||||
"name": "Research and Summary Workflow",
|
||||
"description": "A workflow that searches for information and creates summaries",
|
||||
"project_id": "test-project-123"
|
||||
}
|
||||
|
||||
# Convert flow to workflow definition
|
||||
converter = WorkflowConverter()
|
||||
workflow_def = converter.convert_flow_to_workflow(nodes, edges, metadata)
|
||||
|
||||
print(f"✅ Workflow Definition Created:")
|
||||
print(f" Name: {workflow_def.name}")
|
||||
print(f" Steps: {len(workflow_def.steps)}")
|
||||
|
||||
# Get the system prompt
|
||||
main_step = workflow_def.steps[0]
|
||||
system_prompt = main_step.config.get("system_prompt", "")
|
||||
|
||||
print(f"\n📝 System Prompt Analysis:")
|
||||
print(f" System Prompt Length: {len(system_prompt)} characters")
|
||||
|
||||
# Check that input prompt is included
|
||||
input_prompt = main_step.config.get("input_prompt", "")
|
||||
if "Search for information about AI" in system_prompt:
|
||||
print("✅ Input prompt correctly included in system prompt")
|
||||
else:
|
||||
print("❌ ERROR: Input prompt not found in system prompt!")
|
||||
return False
|
||||
|
||||
# Check that tool examples section exists
|
||||
if "## Tool Usage Examples" in system_prompt:
|
||||
print("✅ Tool Usage Examples section found in system prompt")
|
||||
else:
|
||||
print("❌ ERROR: Tool Usage Examples section not found!")
|
||||
return False
|
||||
|
||||
# Check for specific XML examples
|
||||
if "<function_calls>" in system_prompt:
|
||||
print("✅ XML function_calls format found in system prompt")
|
||||
else:
|
||||
print("❌ ERROR: XML function_calls format not found!")
|
||||
return False
|
||||
|
||||
# Check for web search tool example
|
||||
if "web_search" in system_prompt and "<invoke name=\"web_search\">" in system_prompt:
|
||||
print("✅ Web search tool XML example found")
|
||||
else:
|
||||
print("❌ ERROR: Web search tool XML example not found!")
|
||||
return False
|
||||
|
||||
# Check for file operations tool example
|
||||
if "create_file" in system_prompt and "<invoke name=\"create_file\">" in system_prompt:
|
||||
print("✅ File operations tool XML example found")
|
||||
else:
|
||||
print("❌ ERROR: File operations tool XML example not found!")
|
||||
return False
|
||||
|
||||
# Test the tool examples function directly
|
||||
print(f"\n🔧 Testing Tool Examples Function:")
|
||||
tool_ids = ["web_search_tool", "sb_files_tool"]
|
||||
xml_examples = get_tools_xml_examples(tool_ids)
|
||||
|
||||
if xml_examples:
|
||||
print(f"✅ Tool examples generated successfully ({len(xml_examples)} characters)")
|
||||
|
||||
# Check that both tools are included
|
||||
if "Web Search Tool" in xml_examples and "Sb Files Tool" in xml_examples:
|
||||
print("✅ Both tool examples included in output")
|
||||
else:
|
||||
print("❌ ERROR: Not all tool examples included!")
|
||||
return False
|
||||
else:
|
||||
print("❌ ERROR: No tool examples generated!")
|
||||
return False
|
||||
|
||||
# Print a sample of the system prompt for verification
|
||||
print(f"\n📄 System Prompt Sample (first 500 chars):")
|
||||
print("-" * 50)
|
||||
print(system_prompt[:500] + "..." if len(system_prompt) > 500 else system_prompt)
|
||||
print("-" * 50)
|
||||
|
||||
# Look for the tool examples section specifically
|
||||
if "## Tool Usage Examples" in system_prompt:
|
||||
start_idx = system_prompt.find("## Tool Usage Examples")
|
||||
end_idx = system_prompt.find("## Workflow Execution", start_idx)
|
||||
if end_idx == -1:
|
||||
end_idx = start_idx + 1000 # Show next 1000 chars if no end found
|
||||
|
||||
tool_examples_section = system_prompt[start_idx:end_idx]
|
||||
print(f"\n🔧 Tool Examples Section:")
|
||||
print("-" * 50)
|
||||
print(tool_examples_section)
|
||||
print("-" * 50)
|
||||
|
||||
print(f"\n🎉 All Tests Passed!")
|
||||
print(f" ✓ Input prompt integration")
|
||||
print(f" ✓ Tool Usage Examples section")
|
||||
print(f" ✓ XML function_calls format")
|
||||
print(f" ✓ Web search tool example")
|
||||
print(f" ✓ File operations tool example")
|
||||
print(f" ✓ Tool examples function")
|
||||
|
||||
return True
|
||||
|
||||
def main():
|
||||
"""Run the test."""
|
||||
try:
|
||||
result = test_xml_examples_integration()
|
||||
if result:
|
||||
print(f"\n🎯 Test Summary: SUCCESS")
|
||||
print(f" XML tool examples are now automatically included in workflow system prompts!")
|
||||
print(f" When workflows are saved, agents will know exactly how to call tools using XML format.")
|
||||
else:
|
||||
print(f"\n❌ Test Summary: FAILED")
|
||||
print(f" There are issues with XML tool examples integration.")
|
||||
except Exception as e:
|
||||
print(f"❌ Test failed with error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -17,6 +17,7 @@ from .models import (
|
|||
)
|
||||
from .converter import WorkflowConverter, validate_workflow_flow
|
||||
from .executor import WorkflowExecutor
|
||||
from .scheduler import WorkflowScheduler
|
||||
from services.supabase import DBConnection
|
||||
from utils.logger import logger
|
||||
from utils.auth_utils import get_current_user_id_from_jwt
|
||||
|
@ -27,12 +28,14 @@ router = APIRouter()
|
|||
db = DBConnection()
|
||||
workflow_converter = WorkflowConverter()
|
||||
workflow_executor = WorkflowExecutor(db)
|
||||
workflow_scheduler = WorkflowScheduler(db, workflow_executor)
|
||||
|
||||
def initialize(database: DBConnection):
|
||||
"""Initialize the workflow API with database connection."""
|
||||
global db, workflow_executor
|
||||
global db, workflow_executor, workflow_scheduler
|
||||
db = database
|
||||
workflow_executor = WorkflowExecutor(db)
|
||||
workflow_scheduler = WorkflowScheduler(db, workflow_executor)
|
||||
|
||||
def _map_db_to_workflow_definition(data: dict) -> WorkflowDefinition:
|
||||
"""Helper function to map database record to WorkflowDefinition."""
|
||||
|
@ -197,7 +200,18 @@ async def update_workflow(
|
|||
raise HTTPException(status_code=500, detail="Failed to update workflow")
|
||||
|
||||
data = result.data[0]
|
||||
return _map_db_to_workflow_definition(data)
|
||||
updated_workflow = _map_db_to_workflow_definition(data)
|
||||
|
||||
# Handle scheduling if workflow is active and has schedule triggers
|
||||
if updated_workflow.state == 'ACTIVE':
|
||||
has_schedule_trigger = any(trigger.type == 'SCHEDULE' for trigger in updated_workflow.triggers)
|
||||
if has_schedule_trigger:
|
||||
await workflow_scheduler.schedule_workflow(updated_workflow)
|
||||
else:
|
||||
# Unschedule if workflow is not active
|
||||
await workflow_scheduler.unschedule_workflow(workflow_id)
|
||||
|
||||
return updated_workflow
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
|
@ -219,6 +233,9 @@ async def delete_workflow(
|
|||
if not existing.data:
|
||||
raise HTTPException(status_code=404, detail="Workflow not found")
|
||||
|
||||
# Unschedule workflow before deleting
|
||||
await workflow_scheduler.unschedule_workflow(workflow_id)
|
||||
|
||||
await client.table('workflows').delete().eq('id', workflow_id).execute()
|
||||
|
||||
return {"message": "Workflow deleted successfully"}
|
||||
|
@ -441,7 +458,18 @@ async def update_workflow_flow(
|
|||
raise HTTPException(status_code=500, detail="Failed to update workflow")
|
||||
|
||||
data = result.data[0]
|
||||
return _map_db_to_workflow_definition(data)
|
||||
updated_workflow = _map_db_to_workflow_definition(data)
|
||||
|
||||
# Handle scheduling if workflow is active and has schedule triggers
|
||||
if updated_workflow.state == 'ACTIVE':
|
||||
has_schedule_trigger = any(trigger.type == 'SCHEDULE' for trigger in updated_workflow.triggers)
|
||||
if has_schedule_trigger:
|
||||
await workflow_scheduler.schedule_workflow(updated_workflow)
|
||||
else:
|
||||
# Unschedule if workflow is not active
|
||||
await workflow_scheduler.unschedule_workflow(workflow_id)
|
||||
|
||||
return updated_workflow
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
|
@ -493,6 +521,114 @@ async def get_builder_nodes():
|
|||
try:
|
||||
# Return the available node types that can be used in workflows
|
||||
nodes = [
|
||||
{
|
||||
"id": "inputNode",
|
||||
"name": "Input",
|
||||
"description": "Workflow input configuration with prompt and trigger settings",
|
||||
"category": "input",
|
||||
"icon": "Play",
|
||||
"inputs": [],
|
||||
"outputs": ["output"],
|
||||
"required": True,
|
||||
"config_schema": {
|
||||
"prompt": {
|
||||
"type": "textarea",
|
||||
"label": "Workflow Prompt",
|
||||
"description": "The main prompt that describes what this workflow should do",
|
||||
"required": True,
|
||||
"placeholder": "Describe what this workflow should accomplish..."
|
||||
},
|
||||
"trigger_type": {
|
||||
"type": "select",
|
||||
"label": "Trigger Type",
|
||||
"description": "How this workflow should be triggered",
|
||||
"required": True,
|
||||
"options": [
|
||||
{"value": "MANUAL", "label": "Manual"},
|
||||
{"value": "WEBHOOK", "label": "Webhook"},
|
||||
{"value": "SCHEDULE", "label": "Schedule"}
|
||||
],
|
||||
"default": "MANUAL"
|
||||
},
|
||||
"schedule_config": {
|
||||
"type": "object",
|
||||
"label": "Schedule Configuration",
|
||||
"description": "Configure when the workflow runs automatically",
|
||||
"conditional": {"field": "trigger_type", "value": "SCHEDULE"},
|
||||
"properties": {
|
||||
"interval_type": {
|
||||
"type": "select",
|
||||
"label": "Interval Type",
|
||||
"options": [
|
||||
{"value": "minutes", "label": "Minutes"},
|
||||
{"value": "hours", "label": "Hours"},
|
||||
{"value": "days", "label": "Days"},
|
||||
{"value": "weeks", "label": "Weeks"}
|
||||
]
|
||||
},
|
||||
"interval_value": {
|
||||
"type": "number",
|
||||
"label": "Interval Value",
|
||||
"min": 1,
|
||||
"placeholder": "e.g., 30 for every 30 minutes"
|
||||
},
|
||||
"cron_expression": {
|
||||
"type": "text",
|
||||
"label": "Cron Expression (Advanced)",
|
||||
"description": "Use cron syntax for complex schedules",
|
||||
"placeholder": "0 9 * * 1-5 (weekdays at 9 AM)"
|
||||
},
|
||||
"timezone": {
|
||||
"type": "select",
|
||||
"label": "Timezone",
|
||||
"default": "UTC",
|
||||
"options": [
|
||||
{"value": "UTC", "label": "UTC"},
|
||||
{"value": "America/New_York", "label": "Eastern Time"},
|
||||
{"value": "America/Chicago", "label": "Central Time"},
|
||||
{"value": "America/Denver", "label": "Mountain Time"},
|
||||
{"value": "America/Los_Angeles", "label": "Pacific Time"},
|
||||
{"value": "Europe/London", "label": "London"},
|
||||
{"value": "Europe/Paris", "label": "Paris"},
|
||||
{"value": "Asia/Tokyo", "label": "Tokyo"}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"webhook_config": {
|
||||
"type": "object",
|
||||
"label": "Webhook Configuration",
|
||||
"description": "Configure webhook trigger settings",
|
||||
"conditional": {"field": "trigger_type", "value": "WEBHOOK"},
|
||||
"properties": {
|
||||
"method": {
|
||||
"type": "select",
|
||||
"label": "HTTP Method",
|
||||
"default": "POST",
|
||||
"options": [
|
||||
{"value": "POST", "label": "POST"},
|
||||
{"value": "GET", "label": "GET"},
|
||||
{"value": "PUT", "label": "PUT"}
|
||||
]
|
||||
},
|
||||
"authentication": {
|
||||
"type": "select",
|
||||
"label": "Authentication",
|
||||
"options": [
|
||||
{"value": "none", "label": "None"},
|
||||
{"value": "api_key", "label": "API Key"},
|
||||
{"value": "bearer", "label": "Bearer Token"}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"variables": {
|
||||
"type": "key_value",
|
||||
"label": "Default Variables",
|
||||
"description": "Set default values for workflow variables"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "agentNode",
|
||||
"name": "AI Agent",
|
||||
|
@ -603,4 +739,51 @@ async def create_workflow_from_template(
|
|||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating workflow from template: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/workflows/scheduler/status")
|
||||
async def get_scheduler_status(
|
||||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
"""Get information about currently scheduled workflows."""
|
||||
try:
|
||||
scheduled_workflows = await workflow_scheduler.get_scheduled_workflows()
|
||||
|
||||
# Filter to only show workflows owned by the current user
|
||||
client = await db.client
|
||||
user_workflows = await client.table('workflows').select('id').eq('created_by', user_id).execute()
|
||||
user_workflow_ids = {w['id'] for w in user_workflows.data}
|
||||
|
||||
filtered_scheduled = [
|
||||
w for w in scheduled_workflows
|
||||
if w['workflow_id'] in user_workflow_ids
|
||||
]
|
||||
|
||||
return {
|
||||
"scheduled_workflows": filtered_scheduled,
|
||||
"total_scheduled": len(filtered_scheduled)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting scheduler status: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post("/workflows/scheduler/start")
|
||||
async def start_scheduler():
|
||||
"""Start the workflow scheduler."""
|
||||
try:
|
||||
await workflow_scheduler.start()
|
||||
return {"message": "Workflow scheduler started"}
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting scheduler: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post("/workflows/scheduler/stop")
|
||||
async def stop_scheduler():
|
||||
"""Stop the workflow scheduler."""
|
||||
try:
|
||||
await workflow_scheduler.stop()
|
||||
return {"message": "Workflow scheduler stopped"}
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping scheduler: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
|
@ -1,5 +1,6 @@
|
|||
from typing import List, Dict, Any, Optional
|
||||
from .models import WorkflowNode, WorkflowEdge, WorkflowDefinition, WorkflowStep, WorkflowTrigger
|
||||
from .models import WorkflowNode, WorkflowEdge, WorkflowDefinition, WorkflowStep, WorkflowTrigger, InputNodeConfig, ScheduleConfig
|
||||
from .tool_examples import get_tools_xml_examples
|
||||
import uuid
|
||||
from utils.logger import logger
|
||||
|
||||
|
@ -23,8 +24,11 @@ class WorkflowConverter:
|
|||
"""
|
||||
logger.info(f"Converting workflow flow with {len(nodes)} nodes and {len(edges)} edges")
|
||||
|
||||
workflow_prompt = self._generate_workflow_prompt(nodes, edges)
|
||||
# Find input node and extract configuration
|
||||
input_config = self._extract_input_configuration(nodes)
|
||||
workflow_prompt = self._generate_workflow_prompt(nodes, edges, input_config)
|
||||
entry_point = self._find_entry_point(nodes, edges)
|
||||
triggers = self._extract_triggers_from_input(input_config)
|
||||
|
||||
agent_step = WorkflowStep(
|
||||
id="main_agent_step",
|
||||
|
@ -36,22 +40,18 @@ class WorkflowConverter:
|
|||
"system_prompt": workflow_prompt,
|
||||
"agent_id": metadata.get("agent_id"),
|
||||
"model": "anthropic/claude-3-5-sonnet-latest",
|
||||
"max_iterations": 10
|
||||
"max_iterations": 10,
|
||||
"input_prompt": input_config.prompt if input_config else ""
|
||||
},
|
||||
next_steps=[]
|
||||
)
|
||||
|
||||
trigger = WorkflowTrigger(
|
||||
type="MANUAL",
|
||||
config={}
|
||||
)
|
||||
|
||||
workflow = WorkflowDefinition(
|
||||
name=metadata.get("name", "Untitled Workflow"),
|
||||
description=metadata.get("description", "Generated from visual workflow"),
|
||||
steps=[agent_step],
|
||||
entry_point="main_agent_step",
|
||||
triggers=[trigger],
|
||||
triggers=triggers,
|
||||
project_id=metadata.get("project_id", ""),
|
||||
agent_id=metadata.get("agent_id"),
|
||||
is_template=metadata.get("is_template", False),
|
||||
|
@ -61,16 +61,105 @@ class WorkflowConverter:
|
|||
|
||||
return workflow
|
||||
|
||||
def _generate_workflow_prompt(self, nodes: List[Dict[str, Any]], edges: List[Dict[str, Any]]) -> str:
|
||||
def _extract_input_configuration(self, nodes: List[Dict[str, Any]]) -> Optional[InputNodeConfig]:
|
||||
"""Extract input node configuration from the workflow nodes."""
|
||||
for node in nodes:
|
||||
if node.get('type') == 'inputNode':
|
||||
data = node.get('data', {})
|
||||
|
||||
# Extract schedule configuration if present
|
||||
schedule_config = None
|
||||
if data.get('trigger_type') == 'SCHEDULE' and data.get('schedule_config'):
|
||||
schedule_data = data.get('schedule_config', {})
|
||||
schedule_config = ScheduleConfig(
|
||||
cron_expression=schedule_data.get('cron_expression'),
|
||||
interval_type=schedule_data.get('interval_type'),
|
||||
interval_value=schedule_data.get('interval_value'),
|
||||
timezone=schedule_data.get('timezone', 'UTC'),
|
||||
start_date=schedule_data.get('start_date'),
|
||||
end_date=schedule_data.get('end_date'),
|
||||
enabled=schedule_data.get('enabled', True)
|
||||
)
|
||||
|
||||
return InputNodeConfig(
|
||||
prompt=data.get('prompt', ''),
|
||||
trigger_type=data.get('trigger_type', 'MANUAL'),
|
||||
webhook_config=data.get('webhook_config'),
|
||||
schedule_config=schedule_config,
|
||||
variables=data.get('variables')
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def _extract_triggers_from_input(self, input_config: Optional[InputNodeConfig]) -> List[WorkflowTrigger]:
|
||||
"""Extract workflow triggers from input node configuration."""
|
||||
if not input_config:
|
||||
return [WorkflowTrigger(type="MANUAL", config={})]
|
||||
|
||||
triggers = []
|
||||
|
||||
if input_config.trigger_type == 'MANUAL':
|
||||
triggers.append(WorkflowTrigger(type="MANUAL", config={}))
|
||||
|
||||
elif input_config.trigger_type == 'WEBHOOK':
|
||||
webhook_config = input_config.webhook_config or {}
|
||||
triggers.append(WorkflowTrigger(
|
||||
type="WEBHOOK",
|
||||
config={
|
||||
"webhook_url": webhook_config.get('webhook_url'),
|
||||
"method": webhook_config.get('method', 'POST'),
|
||||
"headers": webhook_config.get('headers', {}),
|
||||
"authentication": webhook_config.get('authentication')
|
||||
}
|
||||
))
|
||||
|
||||
elif input_config.trigger_type == 'SCHEDULE':
|
||||
if input_config.schedule_config:
|
||||
schedule_config = {
|
||||
"cron_expression": input_config.schedule_config.cron_expression,
|
||||
"interval_type": input_config.schedule_config.interval_type,
|
||||
"interval_value": input_config.schedule_config.interval_value,
|
||||
"timezone": input_config.schedule_config.timezone,
|
||||
"start_date": input_config.schedule_config.start_date.isoformat() if input_config.schedule_config.start_date else None,
|
||||
"end_date": input_config.schedule_config.end_date.isoformat() if input_config.schedule_config.end_date else None,
|
||||
"enabled": input_config.schedule_config.enabled
|
||||
}
|
||||
triggers.append(WorkflowTrigger(type="SCHEDULE", config=schedule_config))
|
||||
else:
|
||||
# Default schedule trigger
|
||||
triggers.append(WorkflowTrigger(
|
||||
type="SCHEDULE",
|
||||
config={
|
||||
"interval_type": "hours",
|
||||
"interval_value": 1,
|
||||
"timezone": "UTC",
|
||||
"enabled": True
|
||||
}
|
||||
))
|
||||
|
||||
return triggers
|
||||
|
||||
def _generate_workflow_prompt(self, nodes: List[Dict[str, Any]], edges: List[Dict[str, Any]], input_config: Optional[InputNodeConfig] = None) -> str:
|
||||
"""Generate a comprehensive system prompt that describes the workflow."""
|
||||
|
||||
prompt_parts = [
|
||||
"You are an AI agent executing a workflow. Follow these instructions carefully:",
|
||||
"",
|
||||
]
|
||||
|
||||
# Add input prompt if available
|
||||
if input_config and input_config.prompt:
|
||||
prompt_parts.extend([
|
||||
"## Workflow Input Prompt",
|
||||
input_config.prompt,
|
||||
"",
|
||||
])
|
||||
|
||||
prompt_parts.extend([
|
||||
"## Workflow Overview",
|
||||
"This workflow was created visually and consists of the following components:",
|
||||
""
|
||||
]
|
||||
])
|
||||
|
||||
node_descriptions = []
|
||||
agent_nodes = []
|
||||
|
@ -85,6 +174,9 @@ class WorkflowConverter:
|
|||
tool_nodes.append(node)
|
||||
desc = self._describe_tool_node(node, edges)
|
||||
node_descriptions.append(desc)
|
||||
elif node.get('type') == 'inputNode':
|
||||
desc = self._describe_input_node(node, edges)
|
||||
node_descriptions.append(desc)
|
||||
else:
|
||||
desc = self._describe_generic_node(node, edges)
|
||||
node_descriptions.append(desc)
|
||||
|
@ -92,6 +184,32 @@ class WorkflowConverter:
|
|||
prompt_parts.extend(node_descriptions)
|
||||
prompt_parts.append("")
|
||||
|
||||
# Add trigger information
|
||||
if input_config:
|
||||
prompt_parts.extend([
|
||||
"## Trigger Configuration",
|
||||
f"**Trigger Type**: {input_config.trigger_type}",
|
||||
])
|
||||
|
||||
if input_config.trigger_type == 'SCHEDULE' and input_config.schedule_config:
|
||||
schedule = input_config.schedule_config
|
||||
if schedule.cron_expression:
|
||||
prompt_parts.append(f"**Schedule**: {schedule.cron_expression} (cron)")
|
||||
elif schedule.interval_type and schedule.interval_value:
|
||||
prompt_parts.append(f"**Schedule**: Every {schedule.interval_value} {schedule.interval_type}")
|
||||
prompt_parts.append(f"**Timezone**: {schedule.timezone}")
|
||||
|
||||
if input_config.variables:
|
||||
prompt_parts.extend([
|
||||
"",
|
||||
"## Default Variables",
|
||||
"The following default variables are configured:"
|
||||
])
|
||||
for key, value in input_config.variables.items():
|
||||
prompt_parts.append(f"- **{key}**: {value}")
|
||||
|
||||
prompt_parts.append("")
|
||||
|
||||
prompt_parts.extend([
|
||||
"## Execution Instructions",
|
||||
"",
|
||||
|
@ -107,18 +225,37 @@ class WorkflowConverter:
|
|||
"You have access to the following tools based on the workflow configuration:"
|
||||
])
|
||||
|
||||
# Extract tool IDs and generate tool descriptions
|
||||
tool_ids = []
|
||||
for tool_node in tool_nodes:
|
||||
tool_data = tool_node.get('data', {})
|
||||
tool_name = tool_data.get('nodeId', tool_data.get('label', 'Unknown Tool'))
|
||||
tool_desc = tool_data.get('description', 'No description available')
|
||||
prompt_parts.append(f"- **{tool_name}**: {tool_desc}")
|
||||
|
||||
# Collect tool ID for XML examples
|
||||
tool_id = tool_data.get('nodeId')
|
||||
if tool_id:
|
||||
tool_ids.append(tool_id)
|
||||
|
||||
# Add XML tool examples if tools are available
|
||||
if tool_ids:
|
||||
xml_examples = get_tools_xml_examples(tool_ids)
|
||||
if xml_examples:
|
||||
prompt_parts.extend([
|
||||
"",
|
||||
"## Tool Usage Examples",
|
||||
"Use the following XML format to call tools. Each tool call must be wrapped in <function_calls> tags:",
|
||||
"",
|
||||
xml_examples
|
||||
])
|
||||
|
||||
prompt_parts.extend([
|
||||
"",
|
||||
"## Workflow Execution",
|
||||
"When executing this workflow:",
|
||||
"- Follow the logical flow defined by the visual connections",
|
||||
"- Use tools in the order and manner specified",
|
||||
"- Use tools in the order and manner specified using the XML format shown above",
|
||||
"- Provide clear, step-by-step output",
|
||||
"- If any step fails, explain what went wrong and suggest alternatives",
|
||||
"- Complete the workflow by providing the expected output",
|
||||
|
@ -128,6 +265,33 @@ class WorkflowConverter:
|
|||
|
||||
return "\n".join(prompt_parts)
|
||||
|
||||
def _describe_input_node(self, node: Dict[str, Any], edges: List[Dict[str, Any]]) -> str:
|
||||
"""Describe an input node and its configuration."""
|
||||
data = node.get('data', {})
|
||||
prompt = data.get('prompt', 'No prompt specified')
|
||||
trigger_type = data.get('trigger_type', 'MANUAL')
|
||||
|
||||
output_connections = self._find_node_outputs(node.get('id'), edges)
|
||||
|
||||
description = [
|
||||
f"### Input Configuration",
|
||||
f"**Prompt**: {prompt}",
|
||||
f"**Trigger Type**: {trigger_type}",
|
||||
]
|
||||
|
||||
if trigger_type == 'SCHEDULE':
|
||||
schedule_config = data.get('schedule_config', {})
|
||||
if schedule_config.get('cron_expression'):
|
||||
description.append(f"**Schedule**: {schedule_config['cron_expression']} (cron)")
|
||||
elif schedule_config.get('interval_type') and schedule_config.get('interval_value'):
|
||||
description.append(f"**Schedule**: Every {schedule_config['interval_value']} {schedule_config['interval_type']}")
|
||||
|
||||
if output_connections:
|
||||
description.append(f"**Connects to**: {', '.join(output_connections)}")
|
||||
|
||||
description.append("")
|
||||
return "\n".join(description)
|
||||
|
||||
def _describe_agent_node(self, node: Dict[str, Any], edges: List[Dict[str, Any]]) -> str:
|
||||
"""Describe an agent node and its role in the workflow."""
|
||||
data = node.get('data', {})
|
||||
|
@ -239,6 +403,28 @@ def validate_workflow_flow(nodes: List[Dict[str, Any]], edges: List[Dict[str, An
|
|||
errors.append("Workflow must have at least one node")
|
||||
return False, errors
|
||||
|
||||
# Check for required input node
|
||||
has_input = any(node.get('type') == 'inputNode' for node in nodes)
|
||||
if not has_input:
|
||||
errors.append("Every workflow must have an input node")
|
||||
|
||||
# Validate input node configuration
|
||||
for node in nodes:
|
||||
if node.get('type') == 'inputNode':
|
||||
data = node.get('data', {})
|
||||
if not data.get('prompt'):
|
||||
errors.append("Input node must have a prompt configured")
|
||||
|
||||
trigger_type = data.get('trigger_type', 'MANUAL')
|
||||
if trigger_type == 'SCHEDULE':
|
||||
schedule_config = data.get('schedule_config', {})
|
||||
if not schedule_config.get('cron_expression') and not (schedule_config.get('interval_type') and schedule_config.get('interval_value')):
|
||||
errors.append("Schedule trigger must have either cron expression or interval configuration")
|
||||
elif trigger_type == 'WEBHOOK':
|
||||
webhook_config = data.get('webhook_config', {})
|
||||
if not webhook_config:
|
||||
errors.append("Webhook trigger must have webhook configuration")
|
||||
|
||||
connected_nodes = set()
|
||||
for edge in edges:
|
||||
connected_nodes.add(edge.get('source'))
|
||||
|
|
|
@ -228,9 +228,20 @@ class WorkflowExecutor:
|
|||
}
|
||||
|
||||
await client.table('threads').insert(thread_data).execute()
|
||||
initial_message = f"Execute the workflow: {workflow.name}"
|
||||
if workflow.description:
|
||||
initial_message += f"\n\nDescription: {workflow.description}"
|
||||
|
||||
# Get the input prompt from the workflow step configuration
|
||||
input_prompt = ""
|
||||
if workflow.steps:
|
||||
main_step = workflow.steps[0]
|
||||
input_prompt = main_step.config.get("input_prompt", "")
|
||||
|
||||
# Use input prompt if available, otherwise fall back to workflow name/description
|
||||
if input_prompt:
|
||||
initial_message = input_prompt
|
||||
else:
|
||||
initial_message = f"Execute the workflow: {workflow.name}"
|
||||
if workflow.description:
|
||||
initial_message += f"\n\nDescription: {workflow.description}"
|
||||
|
||||
if variables:
|
||||
initial_message += f"\n\nVariables: {json.dumps(variables, indent=2)}"
|
||||
|
|
|
@ -2,11 +2,50 @@ from pydantic import BaseModel
|
|||
from typing import List, Dict, Any, Optional, Literal
|
||||
from datetime import datetime
|
||||
|
||||
class ScheduleConfig(BaseModel):
|
||||
"""Configuration for scheduled workflow triggers."""
|
||||
cron_expression: Optional[str] = None
|
||||
interval_type: Optional[Literal['minutes', 'hours', 'days', 'weeks']] = None
|
||||
interval_value: Optional[int] = None
|
||||
timezone: str = "UTC"
|
||||
start_date: Optional[datetime] = None
|
||||
end_date: Optional[datetime] = None
|
||||
enabled: bool = True
|
||||
|
||||
class SlackWebhookConfig(BaseModel):
|
||||
"""Configuration for Slack webhook integration."""
|
||||
webhook_url: str
|
||||
signing_secret: str
|
||||
channel: Optional[str] = None
|
||||
username: Optional[str] = None
|
||||
|
||||
class GenericWebhookConfig(BaseModel):
|
||||
"""Configuration for generic webhook integration."""
|
||||
url: str
|
||||
headers: Optional[Dict[str, str]] = None
|
||||
auth_token: Optional[str] = None
|
||||
|
||||
class WebhookConfig(BaseModel):
|
||||
"""Configuration for webhook triggers."""
|
||||
type: Literal['slack', 'generic'] = 'slack'
|
||||
method: Optional[Literal['POST', 'GET', 'PUT']] = 'POST'
|
||||
authentication: Optional[Literal['none', 'api_key', 'bearer']] = 'none'
|
||||
slack: Optional[SlackWebhookConfig] = None
|
||||
generic: Optional[GenericWebhookConfig] = None
|
||||
|
||||
class InputNodeConfig(BaseModel):
|
||||
"""Configuration for workflow input nodes."""
|
||||
prompt: str = ""
|
||||
trigger_type: Literal['MANUAL', 'WEBHOOK', 'SCHEDULE'] = 'MANUAL'
|
||||
webhook_config: Optional[WebhookConfig] = None
|
||||
schedule_config: Optional[ScheduleConfig] = None
|
||||
variables: Optional[Dict[str, Any]] = None
|
||||
|
||||
class WorkflowStep(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
type: Literal['TOOL', 'MCP_TOOL', 'CONDITION', 'LOOP', 'PARALLEL', 'WAIT', 'WEBHOOK', 'TRANSFORM']
|
||||
type: Literal['TOOL', 'MCP_TOOL', 'CONDITION', 'LOOP', 'PARALLEL', 'WAIT', 'WEBHOOK', 'TRANSFORM', 'INPUT']
|
||||
config: Dict[str, Any]
|
||||
next_steps: List[str]
|
||||
error_handler: Optional[str] = None
|
||||
|
|
|
@ -0,0 +1,220 @@
|
|||
import asyncio
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, Any, Optional, List
|
||||
from croniter import croniter
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
from apscheduler.triggers.interval import IntervalTrigger
|
||||
|
||||
from .models import WorkflowDefinition, WorkflowTrigger, ScheduleConfig
|
||||
from .executor import WorkflowExecutor
|
||||
from services.supabase import DBConnection
|
||||
from utils.logger import logger
|
||||
|
||||
class WorkflowScheduler:
|
||||
"""Manages scheduled workflow executions."""
|
||||
|
||||
def __init__(self, db: DBConnection, workflow_executor: WorkflowExecutor):
|
||||
self.db = db
|
||||
self.workflow_executor = workflow_executor
|
||||
self.scheduler = AsyncIOScheduler()
|
||||
self.scheduled_jobs = {}
|
||||
|
||||
async def start(self):
|
||||
"""Start the scheduler and load existing scheduled workflows."""
|
||||
self.scheduler.start()
|
||||
await self._load_scheduled_workflows()
|
||||
logger.info("Workflow scheduler started")
|
||||
|
||||
async def stop(self):
|
||||
"""Stop the scheduler."""
|
||||
self.scheduler.shutdown()
|
||||
logger.info("Workflow scheduler stopped")
|
||||
|
||||
async def _load_scheduled_workflows(self):
|
||||
"""Load all active scheduled workflows from the database."""
|
||||
try:
|
||||
client = await self.db.client
|
||||
result = await client.table('workflows').select('*').eq('status', 'active').execute()
|
||||
|
||||
for workflow_data in result.data:
|
||||
definition = workflow_data.get('definition', {})
|
||||
triggers = definition.get('triggers', [])
|
||||
|
||||
for trigger in triggers:
|
||||
if trigger.get('type') == 'SCHEDULE':
|
||||
workflow = self._map_db_to_workflow_definition(workflow_data)
|
||||
await self._schedule_workflow(workflow, trigger)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading scheduled workflows: {e}")
|
||||
|
||||
async def schedule_workflow(self, workflow: WorkflowDefinition):
|
||||
"""Schedule a workflow based on its triggers."""
|
||||
for trigger in workflow.triggers:
|
||||
if trigger.type == 'SCHEDULE':
|
||||
await self._schedule_workflow(workflow, trigger.model_dump())
|
||||
|
||||
async def unschedule_workflow(self, workflow_id: str):
|
||||
"""Remove a workflow from the schedule."""
|
||||
if workflow_id in self.scheduled_jobs:
|
||||
job_id = self.scheduled_jobs[workflow_id]
|
||||
try:
|
||||
self.scheduler.remove_job(job_id)
|
||||
del self.scheduled_jobs[workflow_id]
|
||||
logger.info(f"Unscheduled workflow {workflow_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error unscheduling workflow {workflow_id}: {e}")
|
||||
|
||||
async def _schedule_workflow(self, workflow: WorkflowDefinition, trigger_config: Dict[str, Any]):
|
||||
"""Schedule a single workflow with the given trigger configuration."""
|
||||
try:
|
||||
config = trigger_config.get('config', {})
|
||||
|
||||
if not config.get('enabled', True):
|
||||
logger.info(f"Skipping disabled schedule for workflow {workflow.id}")
|
||||
return
|
||||
|
||||
if workflow.id in self.scheduled_jobs:
|
||||
await self.unschedule_workflow(workflow.id)
|
||||
|
||||
trigger = None
|
||||
|
||||
if config.get('cron_expression'):
|
||||
cron_expr = config['cron_expression']
|
||||
timezone_str = config.get('timezone', 'UTC')
|
||||
if croniter.is_valid(cron_expr):
|
||||
trigger = CronTrigger.from_crontab(cron_expr, timezone=timezone_str)
|
||||
else:
|
||||
logger.error(f"Invalid cron expression for workflow {workflow.id}: {cron_expr}")
|
||||
return
|
||||
|
||||
elif config.get('interval_type') and config.get('interval_value'):
|
||||
interval_type = config['interval_type']
|
||||
interval_value = config['interval_value']
|
||||
|
||||
kwargs = {interval_type: interval_value}
|
||||
trigger = IntervalTrigger(**kwargs)
|
||||
|
||||
else:
|
||||
logger.error(f"No valid schedule configuration for workflow {workflow.id}")
|
||||
return
|
||||
|
||||
job_id = f"workflow_{workflow.id}_{uuid.uuid4().hex[:8]}"
|
||||
|
||||
job = self.scheduler.add_job(
|
||||
func=self._execute_scheduled_workflow,
|
||||
trigger=trigger,
|
||||
args=[workflow.id, workflow.project_id],
|
||||
id=job_id,
|
||||
name=f"Scheduled execution of {workflow.name}",
|
||||
max_instances=1,
|
||||
coalesce=True,
|
||||
misfire_grace_time=300
|
||||
)
|
||||
|
||||
self.scheduled_jobs[workflow.id] = job_id
|
||||
|
||||
next_run = job.next_run_time
|
||||
logger.info(f"Scheduled workflow {workflow.name} (ID: {workflow.id}). Next run: {next_run}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error scheduling workflow {workflow.id}: {e}")
|
||||
|
||||
async def _execute_scheduled_workflow(self, workflow_id: str, project_id: str):
|
||||
"""Execute a scheduled workflow."""
|
||||
try:
|
||||
logger.info(f"Executing scheduled workflow {workflow_id}")
|
||||
client = await self.db.client
|
||||
result = await client.table('workflows').select('*').eq('id', workflow_id).execute()
|
||||
|
||||
if not result.data:
|
||||
logger.error(f"Scheduled workflow {workflow_id} not found")
|
||||
return
|
||||
|
||||
workflow_data = result.data[0]
|
||||
if workflow_data.get('status') != 'active':
|
||||
logger.info(f"Skipping execution of inactive workflow {workflow_id}")
|
||||
await self.unschedule_workflow(workflow_id)
|
||||
return
|
||||
|
||||
workflow = self._map_db_to_workflow_definition(workflow_data)
|
||||
execution_id = str(uuid.uuid4())
|
||||
execution_data = {
|
||||
"id": execution_id,
|
||||
"workflow_id": workflow_id,
|
||||
"workflow_version": workflow_data.get('version', 1),
|
||||
"workflow_name": workflow.name,
|
||||
"execution_context": {},
|
||||
"project_id": project_id,
|
||||
"account_id": workflow_data.get('account_id'),
|
||||
"triggered_by": "SCHEDULE",
|
||||
"status": "pending",
|
||||
"started_at": datetime.now(timezone.utc).isoformat()
|
||||
}
|
||||
|
||||
await client.table('workflow_executions').insert(execution_data).execute()
|
||||
thread_id = str(uuid.uuid4())
|
||||
async for update in self.workflow_executor.execute_workflow(
|
||||
workflow=workflow,
|
||||
variables=None,
|
||||
thread_id=thread_id,
|
||||
project_id=project_id
|
||||
):
|
||||
logger.debug(f"Scheduled workflow {workflow_id} update: {update.get('type', 'unknown')}")
|
||||
|
||||
logger.info(f"Completed scheduled execution of workflow {workflow_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error executing scheduled workflow {workflow_id}: {e}")
|
||||
try:
|
||||
client = await self.db.client
|
||||
await client.table('workflow_executions').update({
|
||||
"status": "failed",
|
||||
"completed_at": datetime.now(timezone.utc).isoformat(),
|
||||
"error": str(e)
|
||||
}).eq('id', execution_id).execute()
|
||||
except:
|
||||
pass
|
||||
|
||||
def _map_db_to_workflow_definition(self, data: dict) -> WorkflowDefinition:
|
||||
"""Helper function to map database record to WorkflowDefinition."""
|
||||
definition = data.get('definition', {})
|
||||
return WorkflowDefinition(
|
||||
id=data['id'],
|
||||
name=data['name'],
|
||||
description=data.get('description'),
|
||||
steps=definition.get('steps', []),
|
||||
entry_point=definition.get('entry_point', ''),
|
||||
triggers=definition.get('triggers', []),
|
||||
state=data.get('status', 'draft').upper(),
|
||||
created_at=datetime.fromisoformat(data['created_at']) if data.get('created_at') else None,
|
||||
updated_at=datetime.fromisoformat(data['updated_at']) if data.get('updated_at') else None,
|
||||
created_by=data.get('created_by'),
|
||||
project_id=data['project_id'],
|
||||
agent_id=definition.get('agent_id'),
|
||||
is_template=False,
|
||||
max_execution_time=definition.get('max_execution_time', 3600),
|
||||
max_retries=definition.get('max_retries', 3)
|
||||
)
|
||||
|
||||
async def get_scheduled_workflows(self) -> List[Dict[str, Any]]:
|
||||
"""Get information about currently scheduled workflows."""
|
||||
scheduled_info = []
|
||||
|
||||
for workflow_id, job_id in self.scheduled_jobs.items():
|
||||
try:
|
||||
job = self.scheduler.get_job(job_id)
|
||||
if job:
|
||||
scheduled_info.append({
|
||||
"workflow_id": workflow_id,
|
||||
"job_id": job_id,
|
||||
"name": job.name,
|
||||
"next_run_time": job.next_run_time.isoformat() if job.next_run_time else None,
|
||||
"trigger": str(job.trigger)
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting job info for {job_id}: {e}")
|
||||
|
||||
return scheduled_info
|
|
@ -0,0 +1,69 @@
|
|||
TOOL_XML_EXAMPLES = {
|
||||
"computer_use_tool": '<function_calls>\n <invoke name="move_to">\n <parameter name="x">100</parameter>\n <parameter name="y">200</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="click">\n <parameter name="x">100</parameter>\n <parameter name="y">200</parameter>\n <parameter name="button">left</parameter>\n <parameter name="num_clicks">1</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="scroll">\n <parameter name="amount">-3</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="typing">\n <parameter name="text">Hello World!</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="press">\n <parameter name="key">enter</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="wait">\n <parameter name="duration">1.5</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="mouse_down">\n <parameter name="button">left</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="mouse_up">\n <parameter name="button">left</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="drag_to">\n <parameter name="x">500</parameter>\n <parameter name="y">50</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="hotkey">\n <parameter name="keys">ctrl+a</parameter>\n </invoke>\n </function_calls>',
|
||||
|
||||
"data_providers_tool": '<!-- \nThe get-data-provider-endpoints tool returns available endpoints for a specific data provider.\nUse this tool when you need to discover what endpoints are available.\n-->\n\n<!-- Example to get LinkedIn API endpoints -->\n<function_calls>\n<invoke name="get_data_provider_endpoints">\n<parameter name="service_name">linkedin</parameter>\n</invoke>\n</function_calls>\n\n<!-- \n The execute-data-provider-call tool makes a request to a specific data provider endpoint.\n Use this tool when you need to call an data provider endpoint with specific parameters.\n The route must be a valid endpoint key obtained from get-data-provider-endpoints tool!!\n -->\n \n <!-- Example to call linkedIn service with the specific route person -->\n <function_calls>\n <invoke name="execute_data_provider_call">\n <parameter name="service_name">linkedin</parameter>\n <parameter name="route">person</parameter>\n <parameter name="payload">{"link": "https://www.linkedin.com/in/johndoe/"}</parameter>\n </invoke>\n </function_calls>',
|
||||
|
||||
"expand_msg_tool": '<!-- Example 1: Expand a message that was truncated in the previous conversation -->\n <function_calls>\n <invoke name="expand_message">\n <parameter name="message_id">ecde3a4c-c7dc-4776-ae5c-8209517c5576</parameter>\n </invoke>\n </function_calls>\n\n <!-- Example 2: Expand a message to create reports or analyze truncated data -->\n <function_calls>\n <invoke name="expand_message">\n <parameter name="message_id">f47ac10b-58cc-4372-a567-0e02b2c3d479</parameter>\n </invoke>\n </function_calls>\n\n <!-- Example 3: Expand a message when you need the full content for analysis -->\n <function_calls>\n <invoke name="expand_message">\n <parameter name="message_id">550e8400-e29b-41d4-a716-446655440000</parameter>\n </invoke>\n </function_calls>',
|
||||
|
||||
"mcp_tool_wrapper": '<function_calls>\n <invoke name="call_mcp_tool">\n <parameter name="tool_name">mcp_exa_web_search_exa</parameter>\n <parameter name="arguments">{"query": "latest developments in AI", "num_results": 10}</parameter>\n </invoke>\n </function_calls>',
|
||||
|
||||
"message_tool": '<function_calls>\n <invoke name="ask">\n <parameter name="text">I\'m planning to bake the chocolate cake for your birthday party. The recipe mentions "rich frosting" but doesn\'t specify what type. Could you clarify your preferences? For example:\n1. Would you prefer buttercream or cream cheese frosting?\n2. Do you want any specific flavor added to the frosting (vanilla, coffee, etc.)?\n3. Should I add any decorative toppings like sprinkles or fruit?\n4. Do you have any dietary restrictions I should be aware of?\n\nThis information will help me make sure the cake meets your expectations for the celebration.</parameter>\n <parameter name="attachments">recipes/chocolate_cake.txt,photos/cake_examples.jpg</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="web_browser_takeover">\n <parameter name="text">I\'ve encountered a CAPTCHA verification on the page. Please:\n1. Solve the CAPTCHA puzzle\n2. Let me know once you\'ve completed it\n3. I\'ll then continue with the automated process\n\nIf you encounter any issues or need to take additional steps, please let me know.</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="complete">\n </invoke>\n </function_calls>',
|
||||
|
||||
"sb_browser_tool": '<function_calls>\n <invoke name="browser_navigate_to">\n <parameter name="url">https://example.com</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="browser_go_back">\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="browser_wait">\n <parameter name="seconds">5</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="browser_click_element">\n <parameter name="index">2</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="browser_input_text">\n <parameter name="index">2</parameter>\n <parameter name="text">Hello, world!</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="browser_send_keys">\n <parameter name="keys">Enter</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="browser_switch_tab">\n <parameter name="page_id">1</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="browser_close_tab">\n <parameter name="page_id">1</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="browser_scroll_down">\n <parameter name="amount">500</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="browser_scroll_up">\n <parameter name="amount">500</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="browser_scroll_to_text">\n <parameter name="text">Contact Us</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="browser_get_dropdown_options">\n <parameter name="index">2</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="browser_select_dropdown_option">\n <parameter name="index">2</parameter>\n <parameter name="text">Option 1</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="browser_drag_drop">\n <parameter name="element_source">#draggable</parameter>\n <parameter name="element_target">#droppable</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="browser_click_coordinates">\n <parameter name="x">100</parameter>\n <parameter name="y">200</parameter>\n </invoke>\n </function_calls>',
|
||||
|
||||
"sb_deploy_tool": '<!-- \n IMPORTANT: Only use this tool when:\n 1. The user explicitly requests permanent deployment to production\n 2. You have a complete, ready-to-deploy directory \n \n NOTE: If the same name is used, it will redeploy to the same project as before\n -->\n\n <function_calls>\n <invoke name="deploy">\n <parameter name="name">my-site</parameter>\n <parameter name="directory_path">website</parameter>\n </invoke>\n </function_calls>',
|
||||
|
||||
"sb_expose_tool": '<!-- Example 1: Expose a web server running on port 8000 -->\n <function_calls>\n <invoke name="expose_port">\n <parameter name="port">8000</parameter>\n </invoke>\n </function_calls>\n\n <!-- Example 2: Expose an API service running on port 3000 -->\n <function_calls>\n <invoke name="expose_port">\n <parameter name="port">3000</parameter>\n </invoke>\n </function_calls>\n\n <!-- Example 3: Expose a development server running on port 5173 -->\n <function_calls>\n <invoke name="expose_port">\n <parameter name="port">5173</parameter>\n </invoke>\n </function_calls>',
|
||||
|
||||
"sb_files_tool": '<function_calls>\n <invoke name="create_file">\n <parameter name="file_path">src/main.py</parameter>\n <parameter name="file_contents">\n # This is the file content\n def main():\n print("Hello, World!")\n \n if __name__ == "__main__":\n main()\n </parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="str_replace">\n <parameter name="file_path">src/main.py</parameter>\n <parameter name="old_str">text to replace (must appear exactly once in the file)</parameter>\n <parameter name="new_str">replacement text that will be inserted instead</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="full_file_rewrite">\n <parameter name="file_path">src/main.py</parameter>\n <parameter name="file_contents">\n This completely replaces the entire file content.\n Use when making major changes to a file or when the changes\n are too extensive for str-replace.\n All previous content will be lost and replaced with this text.\n </parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="delete_file">\n <parameter name="file_path">src/main.py</parameter>\n </invoke>\n </function_calls>',
|
||||
|
||||
"sb_shell_tool": '<function_calls>\n <invoke name="execute_command">\n <parameter name="command">npm run dev</parameter>\n <parameter name="session_name">dev_server</parameter>\n </invoke>\n </function_calls>\n\n <!-- Example 2: Running in Specific Directory -->\n <function_calls>\n <invoke name="execute_command">\n <parameter name="command">npm run build</parameter>\n <parameter name="folder">frontend</parameter>\n <parameter name="session_name">build_process</parameter>\n </invoke>\n </function_calls>\n\n <!-- Example 3: Blocking command (wait for completion) -->\n <function_calls>\n <invoke name="execute_command">\n <parameter name="command">npm install</parameter>\n <parameter name="blocking">true</parameter>\n <parameter name="timeout">300</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="check_command_output">\n <parameter name="session_name">dev_server</parameter>\n </invoke>\n </function_calls>\n \n <!-- Example 2: Check final output and kill session -->\n <function_calls>\n <invoke name="check_command_output">\n <parameter name="session_name">build_process</parameter>\n <parameter name="kill_session">true</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="terminate_command">\n <parameter name="session_name">dev_server</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="list_commands">\n </invoke>\n </function_calls>',
|
||||
|
||||
"sb_vision_tool": '<!-- Example: Request to see an image named \'diagram.png\' inside the \'docs\' folder -->\n <function_calls>\n <invoke name="see_image">\n <parameter name="file_path">docs/diagram.png</parameter>\n </invoke>\n </function_calls>',
|
||||
|
||||
"update_agent_tool": '<function_calls>\n <invoke name="update_agent">\n <parameter name="name">Research Assistant</parameter>\n <parameter name="description">An AI assistant specialized in conducting research and providing comprehensive analysis</parameter>\n <parameter name="system_prompt">You are a research assistant with expertise in gathering, analyzing, and synthesizing information. Your approach is thorough and methodical...</parameter>\n <parameter name="agentpress_tools">{"web_search": {"enabled": true, "description": "Search the web for information"}, "sb_files": {"enabled": true, "description": "Read and write files"}}</parameter>\n <parameter name="avatar">🔬</parameter>\n <parameter name="avatar_color">#4F46E5</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="get_current_agent_config">\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="search_mcp_servers">\n <parameter name="query">linear</parameter>\n <parameter name="limit">5</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="get_mcp_server_tools">\n <parameter name="qualified_name">exa</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="configure_mcp_server">\n <parameter name="qualified_name">exa</parameter>\n <parameter name="display_name">Exa Search</parameter>\n <parameter name="enabled_tools">["search", "find_similar"]</parameter>\n <parameter name="config">{"exaApiKey": "user-api-key"}</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="get_popular_mcp_servers">\n <parameter name="category">AI & Search</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="test_mcp_server_connection">\n <parameter name="qualified_name">exa</parameter>\n <parameter name="config">{"exaApiKey": "user-api-key"}</parameter>\n </invoke>\n </function_calls>',
|
||||
|
||||
"web_search_tool": '<function_calls>\n <invoke name="web_search">\n <parameter name="query">what is Kortix AI and what are they building?</parameter>\n <parameter name="num_results">20</parameter>\n </invoke>\n </function_calls>\n \n <!-- Another search example -->\n <function_calls>\n <invoke name="web_search">\n <parameter name="query">latest AI research on transformer models</parameter>\n <parameter name="num_results">20</parameter>\n </invoke>\n </function_calls>\n\n<function_calls>\n <invoke name="scrape_webpage">\n <parameter name="urls">https://www.kortix.ai/,https://github.com/kortix-ai/suna</parameter>\n </invoke>\n </function_calls>',
|
||||
|
||||
}
|
||||
|
||||
def get_tool_xml_example(tool_id: str) -> str:
|
||||
"""
|
||||
Get the XML example for a specific tool.
|
||||
|
||||
Args:
|
||||
tool_id: The tool identifier
|
||||
|
||||
Returns:
|
||||
XML example string for the tool, or empty string if not found
|
||||
"""
|
||||
return TOOL_XML_EXAMPLES.get(tool_id, "")
|
||||
|
||||
def get_tools_xml_examples(tool_ids: list) -> str:
|
||||
"""
|
||||
Get XML examples for multiple tools.
|
||||
|
||||
Args:
|
||||
tool_ids: List of tool identifiers
|
||||
|
||||
Returns:
|
||||
Combined XML examples for all tools
|
||||
"""
|
||||
examples = []
|
||||
for tool_id in tool_ids:
|
||||
example = get_tool_xml_example(tool_id)
|
||||
if example:
|
||||
examples.append(f"## {tool_id.replace('_', ' ').title()}")
|
||||
examples.append(example)
|
||||
examples.append("")
|
||||
|
||||
return "\n".join(examples)
|
||||
|
||||
def get_all_available_tools() -> list:
|
||||
"""
|
||||
Get a list of all available tool IDs.
|
||||
|
||||
Returns:
|
||||
List of tool identifiers
|
||||
"""
|
||||
return list(TOOL_XML_EXAMPLES.keys())
|
|
@ -4,9 +4,21 @@ import { useState, useEffect } from "react";
|
|||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Plus, Play, Edit, Trash2, Clock, CheckCircle, XCircle, AlertCircle } from "lucide-react";
|
||||
import { Plus, Play, Edit, Trash2, Clock, CheckCircle, XCircle, AlertCircle, Check, X } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { getWorkflows, executeWorkflow, deleteWorkflow, getProjects, createWorkflow, type Workflow } from "@/lib/api";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { getWorkflows, executeWorkflow, deleteWorkflow, getProjects, createWorkflow, updateWorkflow, type Workflow } from "@/lib/api";
|
||||
import { toast } from "sonner";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
|
@ -17,6 +29,10 @@ export default function WorkflowsPage() {
|
|||
const [executingWorkflows, setExecutingWorkflows] = useState<Set<string>>(new Set());
|
||||
const [projectId, setProjectId] = useState<string | null>(null);
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [editingWorkflowId, setEditingWorkflowId] = useState<string | null>(null);
|
||||
const [editingName, setEditingName] = useState("");
|
||||
const [updatingWorkflows, setUpdatingWorkflows] = useState<Set<string>>(new Set());
|
||||
const [deletingWorkflows, setDeletingWorkflows] = useState<Set<string>>(new Set());
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -83,18 +99,23 @@ export default function WorkflowsPage() {
|
|||
toast.error("No project selected");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setCreating(true);
|
||||
const existingNames = workflows.map(w => w.name.toLowerCase());
|
||||
let workflowName = "Untitled Workflow";
|
||||
let counter = 1;
|
||||
while (existingNames.includes(workflowName.toLowerCase())) {
|
||||
workflowName = `Untitled Workflow ${counter}`;
|
||||
counter++;
|
||||
}
|
||||
const newWorkflow = await createWorkflow({
|
||||
name: "Untitled Workflow",
|
||||
name: workflowName,
|
||||
description: "A new workflow",
|
||||
project_id: projectId,
|
||||
nodes: [],
|
||||
edges: [],
|
||||
variables: {}
|
||||
});
|
||||
|
||||
toast.success("Workflow created successfully!");
|
||||
router.push(`/workflows/builder/${newWorkflow.id}`);
|
||||
} catch (err) {
|
||||
|
@ -105,18 +126,77 @@ export default function WorkflowsPage() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleDeleteWorkflow = async (workflowId: string) => {
|
||||
if (!confirm('Are you sure you want to delete this workflow?')) {
|
||||
const handleStartEditName = (workflow: Workflow) => {
|
||||
setEditingWorkflowId(workflow.id);
|
||||
setEditingName(workflow.name);
|
||||
};
|
||||
|
||||
const handleCancelEditName = () => {
|
||||
setEditingWorkflowId(null);
|
||||
setEditingName("");
|
||||
};
|
||||
|
||||
const handleSaveEditName = async (workflowId: string) => {
|
||||
if (!editingName.trim()) {
|
||||
toast.error("Workflow name cannot be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for duplicate names
|
||||
const existingNames = workflows
|
||||
.filter(w => w.id !== workflowId)
|
||||
.map(w => w.name.toLowerCase());
|
||||
|
||||
if (existingNames.includes(editingName.toLowerCase())) {
|
||||
toast.error("A workflow with this name already exists");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setUpdatingWorkflows(prev => new Set(prev).add(workflowId));
|
||||
|
||||
await updateWorkflow(workflowId, {
|
||||
name: editingName.trim()
|
||||
});
|
||||
|
||||
// Update local state
|
||||
setWorkflows(prev => prev.map(w =>
|
||||
w.id === workflowId
|
||||
? { ...w, name: editingName.trim(), definition: { ...w.definition, name: editingName.trim() } }
|
||||
: w
|
||||
));
|
||||
|
||||
setEditingWorkflowId(null);
|
||||
setEditingName("");
|
||||
toast.success('Workflow name updated successfully');
|
||||
} catch (err) {
|
||||
console.error('Error updating workflow name:', err);
|
||||
toast.error(err instanceof Error ? err.message : 'Failed to update workflow name');
|
||||
} finally {
|
||||
setUpdatingWorkflows(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(workflowId);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteWorkflow = async (workflowId: string) => {
|
||||
try {
|
||||
setDeletingWorkflows(prev => new Set(prev).add(workflowId));
|
||||
|
||||
await deleteWorkflow(workflowId);
|
||||
setWorkflows(prev => prev.filter(w => w.id !== workflowId));
|
||||
toast.success('Workflow deleted successfully');
|
||||
} catch (err) {
|
||||
console.error('Error deleting workflow:', err);
|
||||
toast.error(err instanceof Error ? err.message : 'Failed to delete workflow');
|
||||
} finally {
|
||||
setDeletingWorkflows(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(workflowId);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -245,8 +325,55 @@ export default function WorkflowsPage() {
|
|||
<Card key={workflow.id}>
|
||||
<CardHeader>
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="space-y-1">
|
||||
<CardTitle className="text-lg">{workflow.definition.name || workflow.name}</CardTitle>
|
||||
<div className="space-y-1 flex-1 mr-2">
|
||||
{editingWorkflowId === workflow.id ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
value={editingName}
|
||||
onChange={(e) => setEditingName(e.target.value)}
|
||||
className="text-lg font-semibold h-8"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSaveEditName(workflow.id);
|
||||
} else if (e.key === 'Escape') {
|
||||
handleCancelEditName();
|
||||
}
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => handleSaveEditName(workflow.id)}
|
||||
disabled={updatingWorkflows.has(workflow.id)}
|
||||
>
|
||||
{updatingWorkflows.has(workflow.id) ? (
|
||||
<div className="animate-spin rounded-full h-3 w-3 border-b border-current" />
|
||||
) : (
|
||||
<Check className="h-3 w-3 text-green-600" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={handleCancelEditName}
|
||||
disabled={updatingWorkflows.has(workflow.id)}
|
||||
>
|
||||
<X className="h-3 w-3 text-red-600" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<CardTitle
|
||||
className="text-lg cursor-pointer hover:text-primary transition-colors"
|
||||
onClick={() => handleStartEditName(workflow)}
|
||||
>
|
||||
{workflow.definition.name || workflow.name}
|
||||
</CardTitle>
|
||||
)}
|
||||
<CardDescription>{workflow.definition.description || workflow.description}</CardDescription>
|
||||
</div>
|
||||
{getStatusBadge(workflow.status)}
|
||||
|
@ -275,13 +402,39 @@ export default function WorkflowsPage() {
|
|||
)}
|
||||
Run
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteWorkflow(workflow.id)}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={deletingWorkflows.has(workflow.id)}
|
||||
>
|
||||
{deletingWorkflows.has(workflow.id) ? (
|
||||
<div className="animate-spin rounded-full h-3 w-3 border-b border-current" />
|
||||
) : (
|
||||
<Trash2 className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Workflow</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete "{workflow.definition.name || workflow.name}"?
|
||||
This action cannot be undone and will permanently remove the workflow and all its data.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => handleDeleteWorkflow(workflow.id)}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
Delete Workflow
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
|
|
@ -0,0 +1,230 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import InputNode from "./nodes/InputNode";
|
||||
import { validateWorkflow } from "./WorkflowValidator";
|
||||
|
||||
const sampleInputNodeData = {
|
||||
label: "Input",
|
||||
prompt: "Generate daily sales report and email to stakeholders",
|
||||
trigger_type: "SCHEDULE" as const,
|
||||
schedule_config: {
|
||||
interval_type: "hours" as const,
|
||||
interval_value: 24,
|
||||
timezone: "UTC",
|
||||
enabled: true
|
||||
},
|
||||
variables: {
|
||||
recipients: "manager@company.com,sales@company.com",
|
||||
report_type: "daily_summary"
|
||||
}
|
||||
};
|
||||
|
||||
const sampleNodes = [
|
||||
{
|
||||
id: "input-1",
|
||||
type: "inputNode",
|
||||
position: { x: 100, y: 100 },
|
||||
data: sampleInputNodeData
|
||||
},
|
||||
{
|
||||
id: "agent-1",
|
||||
type: "agentNode",
|
||||
position: { x: 400, y: 100 },
|
||||
data: {
|
||||
label: "Report Generator",
|
||||
instructions: "Generate and send reports"
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const sampleEdges = [
|
||||
{
|
||||
id: "e1",
|
||||
source: "input-1",
|
||||
target: "agent-1"
|
||||
}
|
||||
];
|
||||
|
||||
export default function InputNodeDemo() {
|
||||
const [nodeData, setNodeData] = useState(sampleInputNodeData);
|
||||
|
||||
const validation = validateWorkflow(sampleNodes, sampleEdges);
|
||||
|
||||
const updateNodeData = (updates: any) => {
|
||||
setNodeData(prev => ({ ...prev, ...updates }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6 max-w-4xl mx-auto">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
Input Node Demo
|
||||
<Badge variant="outline">Frontend Implementation</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Validation Status */}
|
||||
<div className="p-4 rounded-lg bg-muted/50">
|
||||
<h3 className="font-medium mb-2">Validation Status</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={validation.valid ? "default" : "destructive"}>
|
||||
{validation.valid ? "Valid" : "Invalid"}
|
||||
</Badge>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{validation.errors.length} issues found
|
||||
</span>
|
||||
</div>
|
||||
{validation.errors.length > 0 && (
|
||||
<div className="mt-2 space-y-1">
|
||||
{validation.errors.map((error, index) => (
|
||||
<div key={index} className="text-sm text-red-600">
|
||||
• {error.message}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Input Node Preview */}
|
||||
<div className="border rounded-lg p-4 bg-background">
|
||||
<h3 className="font-medium mb-4">Input Node Component</h3>
|
||||
<div className="flex justify-center">
|
||||
<InputNode
|
||||
id="demo-input"
|
||||
data={nodeData}
|
||||
selected={false}
|
||||
type="inputNode"
|
||||
isConnectable={true}
|
||||
dragging={false}
|
||||
zIndex={1}
|
||||
selectable={true}
|
||||
deletable={true}
|
||||
draggable={true}
|
||||
positionAbsoluteX={0}
|
||||
positionAbsoluteY={0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Configuration Summary */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Current Configuration</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-sm">
|
||||
<div>
|
||||
<span className="font-medium">Prompt:</span>
|
||||
<p className="text-muted-foreground mt-1">{nodeData.prompt}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Trigger:</span>
|
||||
<Badge variant="outline" className="ml-2">{nodeData.trigger_type}</Badge>
|
||||
</div>
|
||||
{nodeData.trigger_type === 'SCHEDULE' && nodeData.schedule_config && (
|
||||
<div>
|
||||
<span className="font-medium">Schedule:</span>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Every {nodeData.schedule_config.interval_value} {nodeData.schedule_config.interval_type}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<span className="font-medium">Variables:</span>
|
||||
<div className="mt-1 space-y-1">
|
||||
{Object.entries(nodeData.variables || {}).map(([key, value]) => (
|
||||
<div key={key} className="text-xs">
|
||||
<span className="font-mono bg-muted px-1 rounded">{key}</span>: {value}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Features Implemented</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span>Prompt configuration</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span>Trigger type selection</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span>Schedule configuration</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span>Webhook configuration</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span>Variables editor</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span>Workflow validation</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span>Connection rules</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span>Visual indicators</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Test Actions */}
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => updateNodeData({ trigger_type: "MANUAL" })}
|
||||
>
|
||||
Set Manual Trigger
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => updateNodeData({
|
||||
trigger_type: "SCHEDULE",
|
||||
schedule_config: {
|
||||
interval_type: "days",
|
||||
interval_value: 1,
|
||||
timezone: "UTC",
|
||||
enabled: true
|
||||
}
|
||||
})}
|
||||
>
|
||||
Set Daily Schedule
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => updateNodeData({
|
||||
trigger_type: "WEBHOOK",
|
||||
webhook_config: {
|
||||
method: "POST",
|
||||
authentication: "api_key"
|
||||
}
|
||||
})}
|
||||
>
|
||||
Set Webhook Trigger
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Trash2, Plus } from "lucide-react";
|
||||
|
||||
interface KeyValueEditorProps {
|
||||
values: Record<string, any>;
|
||||
onChange: (values: Record<string, any>) => void;
|
||||
placeholder?: {
|
||||
key?: string;
|
||||
value?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function KeyValueEditor({
|
||||
values,
|
||||
onChange,
|
||||
placeholder = { key: "Variable name", value: "Variable value" }
|
||||
}: KeyValueEditorProps) {
|
||||
const [newKey, setNewKey] = useState("");
|
||||
const [newValue, setNewValue] = useState("");
|
||||
|
||||
const handleAddVariable = () => {
|
||||
if (newKey.trim() && !values.hasOwnProperty(newKey.trim())) {
|
||||
onChange({
|
||||
...values,
|
||||
[newKey.trim()]: newValue.trim() || ""
|
||||
});
|
||||
setNewKey("");
|
||||
setNewValue("");
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateVariable = (key: string, value: string) => {
|
||||
onChange({
|
||||
...values,
|
||||
[key]: value
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteVariable = (key: string) => {
|
||||
const newValues = { ...values };
|
||||
delete newValues[key];
|
||||
onChange(newValues);
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleAddVariable();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{/* Existing Variables */}
|
||||
{Object.entries(values).map(([key, value]) => (
|
||||
<Card key={key} className="p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 grid grid-cols-2 gap-2">
|
||||
<Input
|
||||
value={key}
|
||||
onChange={(e) => {
|
||||
const newKey = e.target.value;
|
||||
if (newKey !== key) {
|
||||
const newValues = { ...values };
|
||||
delete newValues[key];
|
||||
newValues[newKey] = value;
|
||||
onChange(newValues);
|
||||
}
|
||||
}}
|
||||
placeholder={placeholder.key}
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={(e) => handleUpdateVariable(key, e.target.value)}
|
||||
placeholder={placeholder.value}
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteVariable(key)}
|
||||
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
{/* Add New Variable */}
|
||||
<Card className="p-3 border-dashed">
|
||||
<div className="space-y-2">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Input
|
||||
value={newKey}
|
||||
onChange={(e) => setNewKey(e.target.value)}
|
||||
placeholder={placeholder.key}
|
||||
onKeyPress={handleKeyPress}
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
<Input
|
||||
value={newValue}
|
||||
onChange={(e) => setNewValue(e.target.value)}
|
||||
placeholder={placeholder.value}
|
||||
onKeyPress={handleKeyPress}
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleAddVariable}
|
||||
disabled={!newKey.trim()}
|
||||
className="w-full h-8"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Variable
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{Object.keys(values).length === 0 && (
|
||||
<p className="text-xs text-muted-foreground text-center py-2">
|
||||
No variables configured. Add variables to pass data to your workflow.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -21,9 +21,21 @@ import {
|
|||
Send,
|
||||
Settings,
|
||||
Sparkles,
|
||||
Zap
|
||||
Zap,
|
||||
Play
|
||||
} from "lucide-react";
|
||||
|
||||
const inputNodes = [
|
||||
{
|
||||
id: "inputNode",
|
||||
name: "Input",
|
||||
description: "Workflow input configuration with prompt and trigger settings",
|
||||
icon: Play,
|
||||
category: "input",
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
const agentNodes = [
|
||||
{
|
||||
id: "agent",
|
||||
|
@ -140,11 +152,14 @@ function DraggableNode({ type, data, children }: DraggableNodeProps) {
|
|||
|
||||
export default function NodePalette() {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>("agent");
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>("input");
|
||||
|
||||
const filteredNodes = useMemo(() => {
|
||||
let nodes: any[] = [];
|
||||
|
||||
if (!selectedCategory || selectedCategory === "input") {
|
||||
nodes = [...nodes, ...inputNodes];
|
||||
}
|
||||
if (!selectedCategory || selectedCategory === "agent") {
|
||||
nodes = [...nodes, ...agentNodes];
|
||||
}
|
||||
|
@ -163,6 +178,7 @@ export default function NodePalette() {
|
|||
}, [searchQuery, selectedCategory]);
|
||||
|
||||
const getNodesByCategory = (category: string) => {
|
||||
if (category === "input") return inputNodes;
|
||||
if (category === "agent") return agentNodes;
|
||||
if (category === "tools") return toolNodes;
|
||||
return [];
|
||||
|
@ -195,7 +211,11 @@ export default function NodePalette() {
|
|||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
<div className="px-4 pt-2 pb-4">
|
||||
<Tabs value={selectedCategory} onValueChange={setSelectedCategory}>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="input">
|
||||
<Play className="h-4 w-4 mr-2" />
|
||||
Input
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="agent">
|
||||
<Bot className="h-4 w-4 mr-2" />
|
||||
Agents
|
||||
|
@ -209,6 +229,60 @@ export default function NodePalette() {
|
|||
</div>
|
||||
|
||||
<div className="flex-1 overflow-hidden px-4">
|
||||
{selectedCategory === "input" && (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h4 className="text-sm font-medium text-muted-foreground">Workflow Input</h4>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Required
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1 overflow-y-auto">
|
||||
<div className="space-y-3 pr-3 mb-4">
|
||||
{getNodesByCategory("input").filter(node =>
|
||||
!searchQuery ||
|
||||
node.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
node.description.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
).map((node) => {
|
||||
const Icon = node.icon;
|
||||
return (
|
||||
<DraggableNode
|
||||
key={node.id}
|
||||
type="inputNode"
|
||||
data={{
|
||||
label: node.name,
|
||||
prompt: "",
|
||||
trigger_type: "MANUAL",
|
||||
variables: {}
|
||||
}}
|
||||
>
|
||||
<Card className="group transition-all duration-200 border hover:border-primary/50 cursor-move border-primary/30 bg-primary/5">
|
||||
<CardHeader className="p-4 py-0">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="p-2 rounded-lg bg-primary/20 border border-primary/30 group-hover:bg-primary/30 transition-colors">
|
||||
<Icon className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<CardTitle className="text-sm font-semibold leading-tight flex items-center gap-2">
|
||||
{node.name}
|
||||
<Badge variant="outline" className="text-xs">Required</Badge>
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs mt-1 line-clamp-2">
|
||||
{node.description}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</DraggableNode>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedCategory === "agent" && (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
|
|
|
@ -30,17 +30,24 @@ import {
|
|||
Zap,
|
||||
Workflow,
|
||||
Eye,
|
||||
EyeOff
|
||||
EyeOff,
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
XCircle
|
||||
} from "lucide-react";
|
||||
import NodePalette from "./NodePalette";
|
||||
import WorkflowSettings from "./WorkflowSettings";
|
||||
import AgentNode from "./nodes/AgentNode";
|
||||
import ToolConnectionNode from "./nodes/ToolConnectionNode";
|
||||
import InputNode from "./nodes/InputNode";
|
||||
import { getProjects, createWorkflow, updateWorkflow, getWorkflow, executeWorkflow, type WorkflowNode, type WorkflowEdge } from "@/lib/api";
|
||||
import { toast } from "sonner";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { validateWorkflow, canConnect, type ValidationResult } from "./WorkflowValidator";
|
||||
import { WorkflowProvider } from "./WorkflowContext";
|
||||
|
||||
const nodeTypes = {
|
||||
inputNode: InputNode,
|
||||
agentNode: AgentNode,
|
||||
toolConnectionNode: ToolConnectionNode,
|
||||
};
|
||||
|
@ -138,12 +145,12 @@ export default function WorkflowBuilder({ workflowId }: WorkflowBuilderProps = {
|
|||
const [saving, setSaving] = useState(false);
|
||||
const [running, setRunning] = useState(false);
|
||||
const [projectId, setProjectId] = useState<string | null>(null);
|
||||
const [validationResult, setValidationResult] = useState<ValidationResult>({ valid: true, errors: [] });
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
// Get user's first project
|
||||
const projects = await getProjects();
|
||||
if (projects.length === 0) {
|
||||
toast.error("No projects found. Please create a project first.");
|
||||
|
@ -152,12 +159,16 @@ export default function WorkflowBuilder({ workflowId }: WorkflowBuilderProps = {
|
|||
|
||||
const firstProject = projects[0];
|
||||
setProjectId(firstProject.id);
|
||||
|
||||
// If editing existing workflow, load it
|
||||
if (workflowId) {
|
||||
const workflow = await getWorkflow(workflowId);
|
||||
setWorkflowName(workflow.definition.name || workflow.name);
|
||||
setWorkflowDescription(workflow.definition.description || workflow.description);
|
||||
console.log('Loaded workflow:', workflow);
|
||||
|
||||
const displayName = workflow.name || workflow.definition.name || "Untitled Workflow";
|
||||
const displayDescription = workflow.description || workflow.definition.description || "";
|
||||
|
||||
console.log('Setting workflow name to:', displayName);
|
||||
setWorkflowName(displayName);
|
||||
setWorkflowDescription(displayDescription);
|
||||
|
||||
if (workflow.definition.nodes && workflow.definition.edges) {
|
||||
setNodes(workflow.definition.nodes);
|
||||
|
@ -173,15 +184,38 @@ export default function WorkflowBuilder({ workflowId }: WorkflowBuilderProps = {
|
|||
loadData();
|
||||
}, [workflowId, setNodes, setEdges]);
|
||||
|
||||
useEffect(() => {
|
||||
const result = validateWorkflow(nodes, edges);
|
||||
setValidationResult(result);
|
||||
}, [nodes, edges]);
|
||||
|
||||
const updateNodeData = useCallback((nodeId: string, updates: any) => {
|
||||
setNodes((nds) =>
|
||||
nds.map((node) =>
|
||||
node.id === nodeId
|
||||
? { ...node, data: { ...node.data, ...updates } }
|
||||
: node
|
||||
)
|
||||
);
|
||||
}, [setNodes]);
|
||||
|
||||
const handleSaveWorkflow = async () => {
|
||||
if (!projectId) {
|
||||
toast.error("No project selected");
|
||||
return;
|
||||
}
|
||||
const validation = validateWorkflow(nodes, edges);
|
||||
if (!validation.valid) {
|
||||
const errorMessages = validation.errors
|
||||
.filter(e => e.type === 'error')
|
||||
.map(e => e.message)
|
||||
.join(', ');
|
||||
toast.error(`Cannot save workflow: ${errorMessages}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setSaving(true);
|
||||
|
||||
const workflowData = {
|
||||
name: workflowName,
|
||||
description: workflowDescription,
|
||||
|
@ -465,9 +499,16 @@ export default function WorkflowBuilder({ workflowId }: WorkflowBuilderProps = {
|
|||
y: event.clientY - 100,
|
||||
};
|
||||
|
||||
let nodeType = "toolConnectionNode";
|
||||
if (nodeData.nodeId === "agent") {
|
||||
nodeType = "agentNode";
|
||||
} else if (type === "inputNode" || nodeData.nodeId === "inputNode") {
|
||||
nodeType = "inputNode";
|
||||
}
|
||||
|
||||
const newNode: Node = {
|
||||
id: `${type}-${Date.now()}`,
|
||||
type: nodeData.nodeId === "agent" ? "agentNode" : "toolConnectionNode",
|
||||
type: nodeType,
|
||||
position,
|
||||
data: {
|
||||
...nodeData,
|
||||
|
@ -522,117 +563,123 @@ export default function WorkflowBuilder({ workflowId }: WorkflowBuilderProps = {
|
|||
)}
|
||||
|
||||
<div className="flex-1 flex flex-col">
|
||||
<Card className="mb-0 rounded-none border-b shadow-none">
|
||||
<CardHeader className="py-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-gradient-to-br from-blue-500/20 to-purple-500/20 border border-blue-500/20">
|
||||
<Workflow className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-lg font-semibold">{workflowName}</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">{workflowDescription || "No description"}</p>
|
||||
</div>
|
||||
<div className="h-16 px-6 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="flex items-center justify-between h-full">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-gradient-to-br from-blue-500/20 to-purple-500/20 border border-blue-500/20">
|
||||
<Workflow className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowNodePalette(!showNodePalette)}
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{showNodePalette ? <EyeOff className="h-4 w-4 mr-2" /> : <Eye className="h-4 w-4 mr-2" />}
|
||||
{showNodePalette ? "Hide" : "Show"} Palette
|
||||
</Button>
|
||||
|
||||
<Separator orientation="vertical" className="h-6" />
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => setShowSettings(true)}
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
Settings
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleSaveWorkflow}
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b border-current mr-2" />
|
||||
) : (
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
{saving ? "Saving..." : "Save"}
|
||||
</Button>
|
||||
|
||||
<Button variant="outline">
|
||||
<Share className="h-4 w-4 mr-2" />
|
||||
Share
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={handleRunWorkflow}
|
||||
disabled={running || !workflowId}
|
||||
>
|
||||
{running ? (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b border-current mr-2" />
|
||||
) : (
|
||||
<Play className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
{running ? "Running..." : "Run Workflow"}
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold">{workflowName}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<div className="flex-1 border border-border/50 bg-card/30 backdrop-blur-sm overflow-hidden">
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={handleEdgesChange}
|
||||
onConnect={onConnect}
|
||||
onDrop={onDrop}
|
||||
onDragOver={onDragOver}
|
||||
nodeTypes={nodeTypes}
|
||||
defaultEdgeOptions={defaultEdgeOptions}
|
||||
connectionLineType={ConnectionLineType.SmoothStep}
|
||||
connectionLineStyle={{
|
||||
strokeWidth: 2,
|
||||
stroke: '#6366f1',
|
||||
}}
|
||||
fitView
|
||||
className="bg-transparent"
|
||||
>
|
||||
<Controls
|
||||
className="bg-card/80 backdrop-blur-sm border border-border/50 rounded-lg shadow-lg"
|
||||
showZoom={true}
|
||||
showFitView={true}
|
||||
showInteractive={true}
|
||||
/>
|
||||
<MiniMap
|
||||
className="bg-card/80 backdrop-blur-sm border border-border/50 rounded-lg shadow-lg"
|
||||
nodeColor={(node) => {
|
||||
if (node.type === 'agentNode') return '#6366f1';
|
||||
return '#9ca3af';
|
||||
}}
|
||||
/>
|
||||
<Background
|
||||
variant={BackgroundVariant.Dots}
|
||||
gap={20}
|
||||
size={1}
|
||||
className="opacity-30 dark:opacity-20"
|
||||
/>
|
||||
</ReactFlow>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleSaveWorkflow}
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b border-current mr-2" />
|
||||
) : (
|
||||
<Save className="h-4 w-4" />
|
||||
)}
|
||||
{saving ? "Saving..." : "Save"}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleRunWorkflow}
|
||||
disabled={running || !workflowId}
|
||||
>
|
||||
{running ? (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b border-current mr-2" />
|
||||
) : (
|
||||
<Play className="h-4 w-4" />
|
||||
)}
|
||||
{running ? "Running..." : "Run Workflow"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="rounded-none border-border/50 bg-card/80 backdrop-blur-sm">
|
||||
|
||||
{/* {validationResult.errors.length > 0 && (
|
||||
<Card className="mx-4 mt-4 border-l-4 border-l-red-500">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
{validationResult.valid ? (
|
||||
<AlertTriangle className="h-4 w-4 text-yellow-500" />
|
||||
) : (
|
||||
<XCircle className="h-4 w-4 text-red-500" />
|
||||
)}
|
||||
<span className="text-sm font-medium">
|
||||
{validationResult.valid ? 'Warnings' : 'Validation Errors'}
|
||||
</span>
|
||||
<Badge variant={validationResult.valid ? "secondary" : "destructive"} className="text-xs">
|
||||
{validationResult.errors.length}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{validationResult.errors.map((error, index) => (
|
||||
<div key={index} className={`flex items-start gap-2 text-sm p-2 rounded ${
|
||||
error.type === 'error' ? 'bg-red-50 text-red-700 dark:bg-red-950/20 dark:text-red-300' : 'bg-yellow-50 text-yellow-700 dark:bg-yellow-950/20 dark:text-yellow-300'
|
||||
}`}>
|
||||
{error.type === 'error' ? (
|
||||
<XCircle className="h-4 w-4 mt-0.5 flex-shrink-0" />
|
||||
) : (
|
||||
<AlertTriangle className="h-4 w-4 mt-0.5 flex-shrink-0" />
|
||||
)}
|
||||
<span>{error.message}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)} */}
|
||||
|
||||
<div className="flex-1 border border-border/50 bg-card/30 backdrop-blur-sm overflow-hidden">
|
||||
<WorkflowProvider updateNodeData={updateNodeData}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={handleEdgesChange}
|
||||
onConnect={onConnect}
|
||||
onDrop={onDrop}
|
||||
onDragOver={onDragOver}
|
||||
nodeTypes={nodeTypes}
|
||||
defaultEdgeOptions={defaultEdgeOptions}
|
||||
connectionLineType={ConnectionLineType.SmoothStep}
|
||||
connectionLineStyle={{
|
||||
strokeWidth: 2,
|
||||
stroke: '#6366f1',
|
||||
}}
|
||||
fitView
|
||||
className="bg-transparent"
|
||||
>
|
||||
<Controls
|
||||
className="bg-card/80 backdrop-blur-sm border border-border/50 rounded-lg shadow-lg"
|
||||
showZoom={true}
|
||||
showFitView={true}
|
||||
showInteractive={true}
|
||||
/>
|
||||
<MiniMap
|
||||
className="bg-card/80 backdrop-blur-sm border border-border/50 rounded-lg shadow-lg"
|
||||
nodeColor={(node) => {
|
||||
if (node.type === 'agentNode') return '#6366f1';
|
||||
return '#9ca3af';
|
||||
}}
|
||||
/>
|
||||
<Background
|
||||
variant={BackgroundVariant.Dots}
|
||||
gap={20}
|
||||
size={1}
|
||||
className="opacity-30 dark:opacity-20"
|
||||
/>
|
||||
</ReactFlow>
|
||||
</WorkflowProvider>
|
||||
</div>
|
||||
{/* <Card className="h-13.5 rounded-none border-border/50 bg-card/80 backdrop-blur-sm">
|
||||
<CardContent className="py-0">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-4">
|
||||
|
@ -654,7 +701,7 @@ export default function WorkflowBuilder({ workflowId }: WorkflowBuilderProps = {
|
|||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Card> */}
|
||||
</div>
|
||||
|
||||
<WorkflowSettings
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
"use client";
|
||||
|
||||
import { createContext, useContext, ReactNode } from "react";
|
||||
|
||||
interface WorkflowContextType {
|
||||
updateNodeData: (nodeId: string, updates: any) => void;
|
||||
}
|
||||
|
||||
const WorkflowContext = createContext<WorkflowContextType | null>(null);
|
||||
|
||||
export function WorkflowProvider({
|
||||
children,
|
||||
updateNodeData
|
||||
}: {
|
||||
children: ReactNode;
|
||||
updateNodeData: (nodeId: string, updates: any) => void;
|
||||
}) {
|
||||
return (
|
||||
<WorkflowContext.Provider value={{ updateNodeData }}>
|
||||
{children}
|
||||
</WorkflowContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useWorkflow() {
|
||||
const context = useContext(WorkflowContext);
|
||||
if (!context) {
|
||||
throw new Error("useWorkflow must be used within a WorkflowProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
"use client";
|
||||
|
||||
import { Node, Edge } from "@xyflow/react";
|
||||
|
||||
export interface ValidationError {
|
||||
type: 'error' | 'warning';
|
||||
message: string;
|
||||
nodeId?: string;
|
||||
}
|
||||
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
errors: ValidationError[];
|
||||
}
|
||||
|
||||
export function validateWorkflow(nodes: Node[], edges: Edge[]): ValidationResult {
|
||||
const errors: ValidationError[] = [];
|
||||
|
||||
// Check for required input node
|
||||
const hasInputNode = nodes.some(node => node.type === 'inputNode');
|
||||
if (!hasInputNode) {
|
||||
errors.push({
|
||||
type: 'error',
|
||||
message: 'Every workflow must have an input node'
|
||||
});
|
||||
}
|
||||
|
||||
// Validate input node configuration
|
||||
const inputNodes = nodes.filter(node => node.type === 'inputNode');
|
||||
inputNodes.forEach(node => {
|
||||
const data = node.data as any;
|
||||
|
||||
if (!data.prompt || (typeof data.prompt === 'string' && data.prompt.trim() === '')) {
|
||||
errors.push({
|
||||
type: 'error',
|
||||
message: 'Input node must have a prompt configured',
|
||||
nodeId: node.id
|
||||
});
|
||||
}
|
||||
|
||||
if (data.trigger_type === 'SCHEDULE') {
|
||||
const scheduleConfig = data.schedule_config as any;
|
||||
if (!scheduleConfig?.cron_expression &&
|
||||
!(scheduleConfig?.interval_type && scheduleConfig?.interval_value)) {
|
||||
errors.push({
|
||||
type: 'error',
|
||||
message: 'Schedule trigger must have either cron expression or interval configuration',
|
||||
nodeId: node.id
|
||||
});
|
||||
}
|
||||
} else if (data.trigger_type === 'WEBHOOK') {
|
||||
const webhookConfig = data.webhook_config;
|
||||
if (!webhookConfig) {
|
||||
errors.push({
|
||||
type: 'error',
|
||||
message: 'Webhook trigger must have webhook configuration',
|
||||
nodeId: node.id
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Check for multiple input nodes (warning)
|
||||
if (inputNodes.length > 1) {
|
||||
errors.push({
|
||||
type: 'warning',
|
||||
message: 'Multiple input nodes detected. Only one input node is recommended per workflow.'
|
||||
});
|
||||
}
|
||||
|
||||
// Check for disconnected nodes
|
||||
const connectedNodeIds = new Set<string>();
|
||||
edges.forEach(edge => {
|
||||
connectedNodeIds.add(edge.source);
|
||||
connectedNodeIds.add(edge.target);
|
||||
});
|
||||
|
||||
const disconnectedNodes = nodes.filter(node =>
|
||||
!connectedNodeIds.has(node.id) && nodes.length > 1
|
||||
);
|
||||
|
||||
if (disconnectedNodes.length > 0) {
|
||||
disconnectedNodes.forEach(node => {
|
||||
errors.push({
|
||||
type: 'warning',
|
||||
message: `Node "${node.data.label || node.id}" is not connected to the workflow`,
|
||||
nodeId: node.id
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Check for self-referencing edges
|
||||
edges.forEach(edge => {
|
||||
if (edge.source === edge.target) {
|
||||
errors.push({
|
||||
type: 'error',
|
||||
message: 'Self-referencing connections are not allowed',
|
||||
nodeId: edge.source
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Check that input nodes cannot receive connections
|
||||
edges.forEach(edge => {
|
||||
const targetNode = nodes.find(n => n.id === edge.target);
|
||||
if (targetNode?.type === 'inputNode') {
|
||||
errors.push({
|
||||
type: 'error',
|
||||
message: 'Input nodes cannot receive connections from other nodes',
|
||||
nodeId: edge.target
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Check for at least one agent node
|
||||
const hasAgentNode = nodes.some(node => node.type === 'agentNode');
|
||||
if (!hasAgentNode && nodes.length > 1) {
|
||||
errors.push({
|
||||
type: 'warning',
|
||||
message: 'Workflow should have at least one agent node'
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.filter(e => e.type === 'error').length === 0,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
export function canConnect(source: Node, target: Node): boolean {
|
||||
// Input nodes cannot receive connections
|
||||
if (target.type === 'inputNode') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Input nodes can connect to any other node
|
||||
if (source.type === 'inputNode') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Other connection rules can be added here
|
||||
return true;
|
||||
}
|
|
@ -101,38 +101,6 @@ const AgentNode = memo(({ data, selected }: NodeProps) => {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium">Connected Tools</label>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{connectedTools.length} tools
|
||||
</Badge>
|
||||
</div>
|
||||
{connectedTools.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{connectedTools.map((tool) => (
|
||||
<div key={tool.id} className="flex items-center gap-2 p-2 bg-muted/30 rounded-lg border">
|
||||
<div className="p-1 rounded bg-primary/10">
|
||||
<Zap className="h-3 w-3 text-primary" />
|
||||
</div>
|
||||
<span className="text-sm font-medium flex-1">{tool.name}</span>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{tool.type.replace('_tool', '').replace('_', ' ')}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-muted/30 rounded-lg p-3 border border-dashed">
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<Plus className="h-3 w-3" />
|
||||
Connect tools from the palette
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium">Output Connections</label>
|
||||
|
|
|
@ -1,59 +1,551 @@
|
|||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { memo, useState } from "react";
|
||||
import { Handle, Position, NodeProps } from "@xyflow/react";
|
||||
import { MessageSquare, Type, Upload, Webhook, Clock } from "lucide-react";
|
||||
import { Play, Settings, Clock, Webhook, User, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import KeyValueEditor from "../KeyValueEditor";
|
||||
import { useWorkflow } from "../WorkflowContext";
|
||||
|
||||
interface InputNodeData {
|
||||
label: string;
|
||||
nodeId: string;
|
||||
config: any;
|
||||
interface SlackWebhookConfig {
|
||||
webhook_url: string;
|
||||
signing_secret: string;
|
||||
channel?: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
const InputNode = memo(({ data, selected }: NodeProps) => {
|
||||
const nodeData = data as unknown as InputNodeData;
|
||||
|
||||
const getIcon = () => {
|
||||
switch (nodeData.nodeId) {
|
||||
case "chat-input":
|
||||
return <MessageSquare className="h-4 w-4 text-white" />;
|
||||
case "text-input":
|
||||
return <Type className="h-4 w-4 text-white" />;
|
||||
case "file-input":
|
||||
return <Upload className="h-4 w-4 text-white" />;
|
||||
case "webhook-input":
|
||||
return <Webhook className="h-4 w-4 text-white" />;
|
||||
case "schedule-input":
|
||||
return <Clock className="h-4 w-4 text-white" />;
|
||||
interface WebhookConfig {
|
||||
type: 'slack' | 'generic';
|
||||
method?: 'POST' | 'GET' | 'PUT';
|
||||
authentication?: 'none' | 'api_key' | 'bearer';
|
||||
slack?: SlackWebhookConfig;
|
||||
generic?: {
|
||||
url: string;
|
||||
headers?: Record<string, string>;
|
||||
auth_token?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface InputNodeData {
|
||||
label?: string;
|
||||
prompt?: string;
|
||||
trigger_type?: 'MANUAL' | 'WEBHOOK' | 'SCHEDULE';
|
||||
schedule_config?: {
|
||||
interval_type?: 'minutes' | 'hours' | 'days' | 'weeks';
|
||||
interval_value?: number;
|
||||
cron_expression?: string;
|
||||
timezone?: string;
|
||||
enabled?: boolean;
|
||||
};
|
||||
webhook_config?: WebhookConfig;
|
||||
variables?: Record<string, any>;
|
||||
}
|
||||
|
||||
const InputNode = memo(({ data, selected, id }: NodeProps) => {
|
||||
const nodeData = data as InputNodeData;
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [isConfigOpen, setIsConfigOpen] = useState(false);
|
||||
const { updateNodeData } = useWorkflow();
|
||||
|
||||
const getTriggerIcon = () => {
|
||||
switch (nodeData.trigger_type) {
|
||||
case 'SCHEDULE':
|
||||
return <Clock className="h-4 w-4" />;
|
||||
case 'WEBHOOK':
|
||||
return <Webhook className="h-4 w-4" />;
|
||||
case 'MANUAL':
|
||||
default:
|
||||
return <MessageSquare className="h-4 w-4 text-white" />;
|
||||
return <User className="h-4 w-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getTriggerColor = () => {
|
||||
switch (nodeData.trigger_type) {
|
||||
case 'SCHEDULE':
|
||||
return 'bg-orange-500';
|
||||
case 'WEBHOOK':
|
||||
return 'bg-purple-500';
|
||||
case 'MANUAL':
|
||||
default:
|
||||
return 'bg-blue-500';
|
||||
}
|
||||
};
|
||||
|
||||
const getTriggerDescription = () => {
|
||||
switch (nodeData.trigger_type) {
|
||||
case 'SCHEDULE':
|
||||
if (nodeData.schedule_config?.cron_expression) {
|
||||
return `Cron: ${nodeData.schedule_config.cron_expression}`;
|
||||
} else if (nodeData.schedule_config?.interval_type && nodeData.schedule_config?.interval_value) {
|
||||
return `Every ${nodeData.schedule_config.interval_value} ${nodeData.schedule_config.interval_type}`;
|
||||
}
|
||||
return 'Scheduled execution';
|
||||
case 'WEBHOOK':
|
||||
if (nodeData.webhook_config?.type === 'slack') {
|
||||
return 'Slack webhook';
|
||||
}
|
||||
return `${nodeData.webhook_config?.method || 'POST'} webhook`;
|
||||
case 'MANUAL':
|
||||
default:
|
||||
return 'Manual execution';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`relative bg-gray-800 rounded-lg border border-gray-600 min-w-[160px] ${selected ? "ring-2 ring-blue-400" : ""}`}>
|
||||
{/* Icon section */}
|
||||
<div className="flex items-center justify-center p-3 bg-blue-500 rounded-t-lg">
|
||||
{getIcon()}
|
||||
<div className={`relative bg-card rounded-lg border-2 min-w-[280px] max-w-[400px] ${
|
||||
selected ? "border-primary shadow-lg" : "border-border"
|
||||
}`}>
|
||||
{/* Header */}
|
||||
<div className={`flex items-center justify-between p-3 ${getTriggerColor()} rounded-t-lg text-white`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Play className="h-5 w-5" />
|
||||
<span className="font-medium">Input</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{getTriggerIcon()}
|
||||
<Badge variant="secondary" className="text-xs bg-white/20 text-white border-white/30">
|
||||
{nodeData.trigger_type || 'MANUAL'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content section */}
|
||||
<div className="p-3 text-center">
|
||||
<h3 className="text-sm font-medium text-white">{nodeData.label}</h3>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
{nodeData.nodeId === "chat-input" && "Get chat inputs from the Playground."}
|
||||
{nodeData.nodeId === "text-input" && "Static text input"}
|
||||
{nodeData.nodeId === "file-input" && "Upload and read files"}
|
||||
{nodeData.nodeId === "webhook-input" && "Receive webhook data"}
|
||||
{nodeData.nodeId === "schedule-input" && "Scheduled trigger"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Output handle */}
|
||||
|
||||
{/* Content */}
|
||||
<CardContent className="p-4 space-y-3">
|
||||
{/* Prompt Preview */}
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-muted-foreground">Prompt</Label>
|
||||
<p className="text-sm mt-1 line-clamp-2 text-foreground">
|
||||
{nodeData.prompt || "No prompt configured"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Trigger Info */}
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-muted-foreground">Trigger</Label>
|
||||
<p className="text-sm mt-1 text-foreground">
|
||||
{getTriggerDescription()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Variables Preview */}
|
||||
{nodeData.variables && Object.keys(nodeData.variables).length > 0 && (
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-muted-foreground">Variables</Label>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{Object.keys(nodeData.variables).slice(0, 3).map((key) => (
|
||||
<Badge key={key} variant="outline" className="text-xs">
|
||||
{key}
|
||||
</Badge>
|
||||
))}
|
||||
{Object.keys(nodeData.variables).length > 3 && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
+{Object.keys(nodeData.variables).length - 3} more
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Configuration Toggle */}
|
||||
<Collapsible open={isConfigOpen} onOpenChange={setIsConfigOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="w-full justify-between p-2">
|
||||
<span className="flex items-center gap-2">
|
||||
<Settings className="h-4 w-4" />
|
||||
Configure
|
||||
</span>
|
||||
{isConfigOpen ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
|
||||
<CollapsibleContent className="space-y-4 mt-3">
|
||||
{/* Prompt Configuration */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`prompt-${id}`} className="text-sm font-medium">
|
||||
Workflow Prompt *
|
||||
</Label>
|
||||
<Textarea
|
||||
id={`prompt-${id}`}
|
||||
placeholder="Describe what this workflow should accomplish..."
|
||||
value={nodeData.prompt || ''}
|
||||
onChange={(e) => updateNodeData(id, { prompt: e.target.value })}
|
||||
className="min-h-[80px] text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* <div className="space-y-2">
|
||||
<Label className="text-sm font-medium">Trigger Type *</Label>
|
||||
<Select
|
||||
value={nodeData.trigger_type || 'MANUAL'}
|
||||
onValueChange={(value: 'MANUAL' | 'WEBHOOK' | 'SCHEDULE') =>
|
||||
updateNodeData(id, { trigger_type: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="MANUAL">
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="h-4 w-4" />
|
||||
Manual
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="WEBHOOK">
|
||||
<div className="flex items-center gap-2">
|
||||
<Webhook className="h-4 w-4" />
|
||||
Webhook
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="SCHEDULE">
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="h-4 w-4" />
|
||||
Schedule
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div> */}
|
||||
|
||||
{/* {nodeData.trigger_type === 'SCHEDULE' && (
|
||||
<div className="space-y-3 p-3 bg-muted/50 rounded-lg">
|
||||
<Label className="text-sm font-medium">Schedule Configuration</Label>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Interval Type</Label>
|
||||
<Select
|
||||
value={nodeData.schedule_config?.interval_type || 'hours'}
|
||||
onValueChange={(value: 'minutes' | 'hours' | 'days' | 'weeks') =>
|
||||
updateNodeData(id, {
|
||||
schedule_config: {
|
||||
...nodeData.schedule_config,
|
||||
interval_type: value
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="minutes">Minutes</SelectItem>
|
||||
<SelectItem value="hours">Hours</SelectItem>
|
||||
<SelectItem value="days">Days</SelectItem>
|
||||
<SelectItem value="weeks">Weeks</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Interval Value</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="1"
|
||||
value={nodeData.schedule_config?.interval_value || ''}
|
||||
onChange={(e) =>
|
||||
updateNodeData(id, {
|
||||
schedule_config: {
|
||||
...nodeData.schedule_config,
|
||||
interval_value: parseInt(e.target.value) || 1
|
||||
}
|
||||
})
|
||||
}
|
||||
className="h-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Cron Expression (Advanced)</Label>
|
||||
<Input
|
||||
placeholder="0 9 * * 1-5 (weekdays at 9 AM)"
|
||||
value={nodeData.schedule_config?.cron_expression || ''}
|
||||
onChange={(e) =>
|
||||
updateNodeData(id, {
|
||||
schedule_config: {
|
||||
...nodeData.schedule_config,
|
||||
cron_expression: e.target.value
|
||||
}
|
||||
})
|
||||
}
|
||||
className="h-8 text-xs font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Timezone</Label>
|
||||
<Select
|
||||
value={nodeData.schedule_config?.timezone || 'UTC'}
|
||||
onValueChange={(value) =>
|
||||
updateNodeData(id, {
|
||||
schedule_config: {
|
||||
...nodeData.schedule_config,
|
||||
timezone: value
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="UTC">UTC</SelectItem>
|
||||
<SelectItem value="America/New_York">Eastern Time</SelectItem>
|
||||
<SelectItem value="America/Chicago">Central Time</SelectItem>
|
||||
<SelectItem value="America/Denver">Mountain Time</SelectItem>
|
||||
<SelectItem value="America/Los_Angeles">Pacific Time</SelectItem>
|
||||
<SelectItem value="Europe/London">London</SelectItem>
|
||||
<SelectItem value="Europe/Paris">Paris</SelectItem>
|
||||
<SelectItem value="Asia/Tokyo">Tokyo</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
)} */}
|
||||
|
||||
{/* {nodeData.trigger_type === 'WEBHOOK' && (
|
||||
<div className="space-y-3 p-3 bg-muted/50 rounded-lg">
|
||||
<Label className="text-sm font-medium">Webhook Configuration</Label>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Webhook Type</Label>
|
||||
<Select
|
||||
value={nodeData.webhook_config?.type || 'slack'}
|
||||
onValueChange={(value: 'slack' | 'generic') =>
|
||||
updateNodeData(id, {
|
||||
webhook_config: {
|
||||
...nodeData.webhook_config,
|
||||
type: value
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="slack">Slack</SelectItem>
|
||||
<SelectItem value="generic">Generic</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{nodeData.webhook_config?.type === 'slack' && (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Slack Webhook URL *</Label>
|
||||
<Input
|
||||
placeholder="https://hooks.slack.com/services/..."
|
||||
value={nodeData.webhook_config?.slack?.webhook_url || ''}
|
||||
onChange={(e) =>
|
||||
updateNodeData(id, {
|
||||
webhook_config: {
|
||||
...nodeData.webhook_config,
|
||||
slack: {
|
||||
...nodeData.webhook_config?.slack,
|
||||
webhook_url: e.target.value
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Signing Secret *</Label>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Your Slack app signing secret"
|
||||
value={nodeData.webhook_config?.slack?.signing_secret || ''}
|
||||
onChange={(e) =>
|
||||
updateNodeData(id, {
|
||||
webhook_config: {
|
||||
...nodeData.webhook_config,
|
||||
slack: {
|
||||
...nodeData.webhook_config?.slack,
|
||||
signing_secret: e.target.value
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Channel (Optional)</Label>
|
||||
<Input
|
||||
placeholder="#general"
|
||||
value={nodeData.webhook_config?.slack?.channel || ''}
|
||||
onChange={(e) =>
|
||||
updateNodeData(id, {
|
||||
webhook_config: {
|
||||
...nodeData.webhook_config,
|
||||
slack: {
|
||||
...nodeData.webhook_config?.slack,
|
||||
channel: e.target.value
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Username (Optional)</Label>
|
||||
<Input
|
||||
placeholder="WorkflowBot"
|
||||
value={nodeData.webhook_config?.slack?.username || ''}
|
||||
onChange={(e) =>
|
||||
updateNodeData(id, {
|
||||
webhook_config: {
|
||||
...nodeData.webhook_config,
|
||||
slack: {
|
||||
...nodeData.webhook_config?.slack,
|
||||
username: e.target.value
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeData.webhook_config?.type === 'generic' && (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Webhook URL *</Label>
|
||||
<Input
|
||||
placeholder="https://your-webhook-endpoint.com"
|
||||
value={nodeData.webhook_config?.generic?.url || ''}
|
||||
onChange={(e) =>
|
||||
updateNodeData(id, {
|
||||
webhook_config: {
|
||||
...nodeData.webhook_config,
|
||||
generic: {
|
||||
...nodeData.webhook_config?.generic,
|
||||
url: e.target.value
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">HTTP Method</Label>
|
||||
<Select
|
||||
value={nodeData.webhook_config?.method || 'POST'}
|
||||
onValueChange={(value: 'POST' | 'GET' | 'PUT') =>
|
||||
updateNodeData(id, {
|
||||
webhook_config: {
|
||||
...nodeData.webhook_config,
|
||||
method: value
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="POST">POST</SelectItem>
|
||||
<SelectItem value="GET">GET</SelectItem>
|
||||
<SelectItem value="PUT">PUT</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Authentication</Label>
|
||||
<Select
|
||||
value={nodeData.webhook_config?.authentication || 'none'}
|
||||
onValueChange={(value: 'none' | 'api_key' | 'bearer') =>
|
||||
updateNodeData(id, {
|
||||
webhook_config: {
|
||||
...nodeData.webhook_config,
|
||||
authentication: value
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">None</SelectItem>
|
||||
<SelectItem value="api_key">API Key</SelectItem>
|
||||
<SelectItem value="bearer">Bearer Token</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{nodeData.webhook_config?.authentication !== 'none' && (
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Auth Token</Label>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Your authentication token"
|
||||
value={nodeData.webhook_config?.generic?.auth_token || ''}
|
||||
onChange={(e) =>
|
||||
updateNodeData(id, {
|
||||
webhook_config: {
|
||||
...nodeData.webhook_config,
|
||||
generic: {
|
||||
...nodeData.webhook_config?.generic,
|
||||
auth_token: e.target.value
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)} */}
|
||||
|
||||
{/* <div className="space-y-2">
|
||||
<Label className="text-sm font-medium">Default Variables</Label>
|
||||
<KeyValueEditor
|
||||
values={nodeData.variables || {}}
|
||||
onChange={(variables) => updateNodeData(id, { variables })}
|
||||
placeholder={{
|
||||
key: "Variable name",
|
||||
value: "Default value"
|
||||
}}
|
||||
/>
|
||||
</div> */}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</CardContent>
|
||||
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="w-3 h-3 !bg-gray-400 !border-gray-300"
|
||||
className="w-3 h-3 !bg-primary !border-primary-foreground"
|
||||
style={{ right: -6 }}
|
||||
/>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue