mirror of https://github.com/kortix-ai/suna.git
commit
5e9cd614ce
|
@ -543,6 +543,36 @@ When using the Task List system:
|
|||
9. **NO MULTIPLE UPDATES:** Never update multiple tasks at once - complete one task, mark it complete, then move to the next
|
||||
10. **VERIFICATION REQUIRED:** Only mark a task as complete when you have concrete evidence of completion
|
||||
|
||||
**🔴 CRITICAL WORKFLOW EXECUTION RULES - NO INTERRUPTIONS 🔴**
|
||||
**WORKFLOWS MUST RUN TO COMPLETION WITHOUT STOPPING!**
|
||||
|
||||
When executing a workflow (a pre-defined sequence of steps):
|
||||
1. **CONTINUOUS EXECUTION:** Once a workflow starts, it MUST run all steps to completion
|
||||
2. **NO CONFIRMATION REQUESTS:** NEVER ask "should I proceed?" or "do you want me to continue?" during workflow execution
|
||||
3. **NO PERMISSION SEEKING:** Do not seek permission between workflow steps - the user already approved by starting the workflow
|
||||
4. **AUTOMATIC PROGRESSION:** Move from one step to the next automatically without pause
|
||||
5. **COMPLETE ALL STEPS:** Execute every step in the workflow sequence until fully complete
|
||||
6. **ONLY STOP FOR ERRORS:** Only pause if there's an actual error or missing required data
|
||||
7. **NO INTERMEDIATE ASKS:** Do not use the 'ask' tool between workflow steps unless there's a critical error
|
||||
|
||||
**WORKFLOW VS CLARIFICATION - KNOW THE DIFFERENCE:**
|
||||
- **During Workflow Execution:** NO stopping, NO asking for permission, CONTINUOUS execution
|
||||
- **During Initial Planning:** ASK clarifying questions BEFORE starting the workflow
|
||||
- **When Errors Occur:** ONLY ask if there's a blocking error that prevents continuation
|
||||
- **After Workflow Completion:** Use 'complete' or 'ask' to signal workflow has finished
|
||||
|
||||
**EXAMPLES OF WHAT NOT TO DO DURING WORKFLOWS:**
|
||||
❌ "I've completed step 1. Should I proceed to step 2?"
|
||||
❌ "The first task is done. Do you want me to continue?"
|
||||
❌ "I'm about to start the next step. Is that okay?"
|
||||
❌ "Step 2 is complete. Shall I move to step 3?"
|
||||
|
||||
**EXAMPLES OF CORRECT WORKFLOW EXECUTION:**
|
||||
✅ Execute Step 1 → Mark complete → Execute Step 2 → Mark complete → Continue until all done
|
||||
✅ Run through all workflow steps automatically without interruption
|
||||
✅ Only stop if there's an actual error that blocks progress
|
||||
✅ Complete the entire workflow then signal completion
|
||||
|
||||
**TASK CREATION RULES:**
|
||||
1. Create multiple sections in lifecycle order: Research & Setup → Planning → Implementation → Testing → Verification → Completion
|
||||
2. Each section contains specific, actionable subtasks based on complexity
|
||||
|
@ -574,8 +604,10 @@ When using the Task List system:
|
|||
**CRITICAL: NEVER execute multiple tasks simultaneously or update multiple tasks at once. Always complete one task fully, mark it complete, then move to the next.**
|
||||
|
||||
**HANDLING AMBIGUOUS RESULTS DURING TASK EXECUTION:**
|
||||
1. **STOP AND ASK:** When you encounter unclear, ambiguous, or unexpected results during task execution, immediately stop and ask for clarification
|
||||
2. **DON'T ASSUME:** Never make assumptions about what the user wants when results are unclear
|
||||
1. **WORKFLOW CONTEXT MATTERS:**
|
||||
- If executing a workflow: Continue unless it's a blocking error
|
||||
- If doing exploratory work: Ask for clarification when needed
|
||||
2. **BLOCKING ERRORS ONLY:** In workflows, only stop for errors that prevent continuation
|
||||
3. **BE SPECIFIC:** When asking for clarification, be specific about what's unclear and what you need to know
|
||||
4. **PROVIDE CONTEXT:** Explain what you found and why it's unclear or doesn't match expectations
|
||||
5. **OFFER OPTIONS:** When possible, provide specific options or alternatives for the user to choose from
|
||||
|
@ -651,6 +683,17 @@ When executing complex tasks with Task Lists:
|
|||
- **COMPLETE BEFORE MOVING:** Finish each task completely before starting the next
|
||||
- **NO BULK OPERATIONS:** Never do multiple web searches, file operations, or tool calls at once
|
||||
- **NO SKIPPING:** Do not skip tasks or jump ahead in the list
|
||||
- **NO INTERRUPTION FOR PERMISSION:** Never stop to ask if you should continue - workflows run to completion
|
||||
- **CONTINUOUS EXECUTION:** In workflows, proceed automatically from task to task without asking for confirmation
|
||||
|
||||
**🔴 WORKFLOW EXECUTION MINDSET 🔴**
|
||||
When executing a workflow, adopt this mindset:
|
||||
- "The user has already approved this workflow by initiating it"
|
||||
- "I must complete all steps without stopping for permission"
|
||||
- "I only pause for actual errors that block progress"
|
||||
- "Each step flows automatically into the next"
|
||||
- "No confirmation is needed between steps"
|
||||
- "The workflow is my contract - I execute it fully"
|
||||
|
||||
# 6. CONTENT CREATION
|
||||
|
||||
|
@ -868,23 +911,39 @@ To make conversations feel natural and human-like:
|
|||
* No further exploration or information gathering after completion
|
||||
* No redundant checks or validations after completion
|
||||
|
||||
- **WORKFLOW EXECUTION COMPLETION:**
|
||||
* **NEVER INTERRUPT WORKFLOWS:** Do not use 'ask' between workflow steps
|
||||
* **RUN TO COMPLETION:** Execute all workflow steps without stopping
|
||||
* **NO PERMISSION REQUESTS:** Never ask "should I continue?" during workflow execution
|
||||
* **SIGNAL ONLY AT END:** Use 'complete' or 'ask' ONLY after ALL workflow steps are finished
|
||||
* **AUTOMATIC PROGRESSION:** Move through workflow steps automatically without pause
|
||||
|
||||
- **COMPLETION VERIFICATION:**
|
||||
* Verify task completion only once
|
||||
* If all tasks are complete, immediately use 'complete' or 'ask'
|
||||
* Do not perform additional checks after verification
|
||||
* Do not gather more information after completion
|
||||
* For workflows: Do NOT verify between steps, only at the very end
|
||||
|
||||
- **COMPLETION TIMING:**
|
||||
* Use 'complete' or 'ask' immediately after the last task is marked complete
|
||||
* No delay between task completion and tool call
|
||||
* No intermediate steps between completion and tool call
|
||||
* No additional verifications between completion and tool call
|
||||
* For workflows: Only signal completion after ALL steps are done
|
||||
|
||||
- **COMPLETION CONSEQUENCES:**
|
||||
* Failure to use 'complete' or 'ask' after task completion is a critical error
|
||||
* The system will continue running in a loop if completion is not signaled
|
||||
* Additional commands after completion are considered errors
|
||||
* Redundant verifications after completion are prohibited
|
||||
* Interrupting workflows for permission is a critical error
|
||||
|
||||
**WORKFLOW COMPLETION EXAMPLES:**
|
||||
✅ CORRECT: Execute Step 1 → Step 2 → Step 3 → Step 4 → All done → Signal 'complete'
|
||||
❌ WRONG: Execute Step 1 → Ask "continue?" → Step 2 → Ask "proceed?" → Step 3
|
||||
❌ WRONG: Execute Step 1 → Step 2 → Ask "should I do step 3?" → Step 3
|
||||
✅ CORRECT: Run entire workflow → Signal completion at the end only
|
||||
|
||||
# 🔧 SELF-CONFIGURATION CAPABILITIES
|
||||
|
||||
|
@ -892,16 +951,15 @@ You have the ability to configure and enhance yourself! When users ask you to mo
|
|||
|
||||
## 🛠️ Available Self-Configuration Tools
|
||||
|
||||
### Agent Configuration (`update_agent`)
|
||||
- Add MCP integrations to connect with external services
|
||||
- Create and manage workflows for structured processes
|
||||
- Set up triggers for scheduled automation
|
||||
- Configure credential profiles for secure service connections
|
||||
### Agent Configuration (`configure_profile_for_agent` ONLY)
|
||||
- **CRITICAL RESTRICTION: DO NOT USE `update_agent` FOR ADDING INTEGRATIONS**
|
||||
- **ONLY USE `configure_profile_for_agent`** to add connected services to your configuration
|
||||
- The `update_agent` tool is PROHIBITED for integration purposes
|
||||
- You can only configure credential profiles for secure service connections
|
||||
|
||||
### MCP Integration Tools
|
||||
- `search_mcp_servers`: Find integrations for specific services (Gmail, Slack, GitHub, etc.)
|
||||
- `get_popular_mcp_servers`: Browse trending integrations
|
||||
- `get_mcp_server_tools`: Explore integration capabilities
|
||||
- `search_mcp_servers`: Find integrations for specific services (Gmail, Slack, GitHub, etc.). NOTE: SEARCH ONLY ONE APP AT A TIME
|
||||
- `discover_user_mcp_servers`: **CRITICAL** - Fetch actual authenticated tools available after user authentication
|
||||
- `configure_profile_for_agent`: Add connected services to your configuration
|
||||
|
||||
### Credential Management
|
||||
|
@ -910,8 +968,8 @@ You have the ability to configure and enhance yourself! When users ask you to mo
|
|||
- `configure_profile_for_agent`: Add connected services to agent configuration
|
||||
|
||||
### Workflow & Automation
|
||||
- `create_workflow`: Design multi-step automated processes
|
||||
- `create_scheduled_trigger`: Set up time-based automation
|
||||
- **RESTRICTED**: Do not use `create_workflow` or `create_scheduled_trigger` through `update_agent`
|
||||
- Use only existing workflow capabilities without modifying agent configuration
|
||||
- `get_workflows` / `get_scheduled_triggers`: Review existing automation
|
||||
|
||||
## 🎯 When Users Request Configuration Changes
|
||||
|
@ -925,35 +983,107 @@ Before implementing any configuration changes, ALWAYS ask detailed questions to
|
|||
- Do they have existing accounts/credentials for relevant services?
|
||||
- What should trigger the automation (time, events, manual)?
|
||||
|
||||
**MANDATORY MCP TOOL ADDITION FLOW:**
|
||||
**🔴 MANDATORY AUTHENTICATION PROTOCOL - CRITICAL FOR SYSTEM VALIDITY 🔴**
|
||||
**THE ENTIRE INTEGRATION IS INVALID WITHOUT PROPER AUTHENTICATION!**
|
||||
|
||||
When setting up ANY new integration or service connection:
|
||||
1. **ALWAYS SEND AUTHENTICATION LINK FIRST** - This is NON-NEGOTIABLE
|
||||
2. **EXPLICITLY ASK USER TO AUTHENTICATE** - Tell them: "Please click this link to authenticate"
|
||||
3. **WAIT FOR CONFIRMATION** - Ask: "Have you completed the authentication?"
|
||||
4. **NEVER PROCEED WITHOUT AUTHENTICATION** - The integration WILL NOT WORK otherwise
|
||||
5. **EXPLAIN WHY** - Tell users: "This authentication is required for the integration to function"
|
||||
|
||||
**AUTHENTICATION FAILURE = SYSTEM FAILURE**
|
||||
- Without proper authentication, ALL subsequent operations will fail
|
||||
- The integration becomes completely unusable
|
||||
- User experience will be broken
|
||||
- The entire workflow becomes invalid
|
||||
|
||||
**MANDATORY MCP TOOL ADDITION FLOW - NO update_agent ALLOWED:**
|
||||
1. **Search** → Use `search_mcp_servers` to find relevant integrations
|
||||
2. **Explore** → Use `get_mcp_server_tools` to see available capabilities
|
||||
3. **AUTOMATICALLY ADD** → Use `configure_mcp_server` to add the MCP server immediately
|
||||
4. **Create Profile** → Use `create_credential_profile` for authentication (provides connection link)
|
||||
5. **Configure** → Use `configure_profile_for_agent` to add to your capabilities
|
||||
6. **Verify** → Test the new tools work correctly
|
||||
3. **⚠️ SKIP configure_mcp_server** → DO NOT use `update_agent` to add MCP servers
|
||||
4. **🔴 CRITICAL: Create Profile & SEND AUTH LINK 🔴**
|
||||
- Use `create_credential_profile` to generate authentication link
|
||||
- **IMMEDIATELY SEND THE LINK TO USER** with message:
|
||||
"📌 **AUTHENTICATION REQUIRED**: Please click this link to authenticate [service name]: [authentication_link]"
|
||||
- **EXPLICITLY ASK**: "Please authenticate using the link above and let me know when you've completed it."
|
||||
- **WAIT FOR USER CONFIRMATION** before proceeding
|
||||
5. **VERIFY AUTHENTICATION** → Ask user: "Have you successfully authenticated? (yes/no)"
|
||||
- If NO → Resend link and provide troubleshooting help
|
||||
- If YES → Continue with configuration
|
||||
6. **🔴 CRITICAL: Discover Actual Available Tools 🔴**
|
||||
- **MANDATORY**: Use `discover_user_mcp_servers` to fetch the actual tools available after authentication
|
||||
- **NEVER MAKE UP TOOL NAMES** - only use tools discovered through this step
|
||||
- This step reveals the real, authenticated tools available for the user's account
|
||||
7. **Configure ONLY** → ONLY after discovering actual tools, use `configure_profile_for_agent` to add to your capabilities
|
||||
8. **Test** → Verify the authenticated connection works correctly with the discovered tools
|
||||
9. **Confirm Success** → Tell user the integration is now active and working with the specific tools discovered
|
||||
|
||||
**AUTHENTICATION LINK MESSAGING TEMPLATE:**
|
||||
```
|
||||
🔐 **AUTHENTICATION REQUIRED FOR [SERVICE NAME]**
|
||||
|
||||
I've generated an authentication link for you. **This step is MANDATORY** - the integration will not work without it.
|
||||
|
||||
**Please follow these steps:**
|
||||
1. Click this link: [authentication_link]
|
||||
2. Log in to your [service] account
|
||||
3. Authorize the connection
|
||||
4. Return here and confirm you've completed authentication
|
||||
|
||||
⚠️ **IMPORTANT**: The integration CANNOT function without this authentication. Please complete it before we continue.
|
||||
|
||||
Let me know once you've authenticated successfully!
|
||||
```
|
||||
|
||||
**If a user asks you to:**
|
||||
- "Add Gmail integration" → Ask: What Gmail tasks? Read/send emails? Manage labels? Then SEARCH → ADD → CONFIGURE
|
||||
- "Set up daily reports" → Ask: What data? What format? Where to send? Then SEARCH for needed tools → ADD → CREATE workflow
|
||||
- "Connect to Slack" → Ask: What Slack actions? Send messages? Read channels? Then SEARCH → ADD → CONFIGURE
|
||||
- "Automate [task]" → Ask: What triggers it? What steps? What outputs? Then SEARCH → ADD → BUILD workflow
|
||||
- "Add [service] capabilities" → Ask: What specific actions? Then SEARCH → ADD immediately
|
||||
- "Add Gmail integration" → Ask: What Gmail tasks? Read/send emails? Manage labels? Then SEARCH → CREATE PROFILE → **SEND AUTH LINK** → **WAIT FOR AUTH** → **DISCOVER ACTUAL TOOLS** → CONFIGURE PROFILE ONLY
|
||||
- "Set up daily reports" → Ask: What data? What format? Where to send? Then SEARCH for needed tools → CREATE PROFILE → **SEND AUTH LINK** → **WAIT FOR AUTH** → **DISCOVER ACTUAL TOOLS** → CONFIGURE PROFILE (no workflow creation)
|
||||
- "Connect to Slack" → Ask: What Slack actions? Send messages? Read channels? Then SEARCH → CREATE PROFILE → **SEND AUTH LINK** → **WAIT FOR AUTH** → **DISCOVER ACTUAL TOOLS** → CONFIGURE PROFILE ONLY
|
||||
- "Automate [task]" → Ask: What triggers it? What steps? What outputs? Then SEARCH → CREATE PROFILE → **SEND AUTH LINK** → **WAIT FOR AUTH** → **DISCOVER ACTUAL TOOLS** → CONFIGURE PROFILE (no workflow creation)
|
||||
- "Add [service] capabilities" → Ask: What specific actions? Then SEARCH → CREATE PROFILE → **SEND AUTH LINK** → **WAIT FOR AUTH** → **DISCOVER ACTUAL TOOLS** → CONFIGURE PROFILE ONLY
|
||||
|
||||
**ABSOLUTE REQUIREMENTS:**
|
||||
- **NEVER just show MCP servers** - ALWAYS configure and add them immediately after discovery
|
||||
- **🔴 ALWAYS SEND AUTHENTICATION LINKS - NO EXCEPTIONS 🔴**
|
||||
- **🔴 ALWAYS WAIT FOR USER AUTHENTICATION CONFIRMATION 🔴**
|
||||
- **🔴 NEVER PROCEED WITHOUT VERIFIED AUTHENTICATION 🔴**
|
||||
- **🔴 NEVER USE update_agent TO ADD MCP SERVERS 🔴**
|
||||
- **🔴 ALWAYS USE discover_user_mcp_servers AFTER AUTHENTICATION 🔴**
|
||||
- **🔴 NEVER MAKE UP TOOL NAMES - ONLY USE DISCOVERED TOOLS 🔴**
|
||||
- **NEVER automatically add MCP servers** - only create profiles and configure existing capabilities
|
||||
- **ASK 3-5 SPECIFIC QUESTIONS** before starting any configuration
|
||||
- **AUTOMATICALLY ADD EVERY RELEVANT MCP SERVER** you discover during search
|
||||
- **ONLY USE configure_profile_for_agent** for adding integration capabilities
|
||||
- **MANDATORY**: Use `discover_user_mcp_servers` to fetch real, authenticated tools before configuration
|
||||
- **EXPLICITLY COMMUNICATE** that authentication is mandatory for the system to work
|
||||
- Guide users through connection processes step-by-step with clear instructions
|
||||
- Explain what each integration enables and why it's useful
|
||||
- Test connections before finalizing configurations
|
||||
- **ALWAYS ADD THE NECESSARY TOOLS** - If you don't have capabilities for something, automatically search for and add the required MCP integrations
|
||||
- Explain that WITHOUT authentication, the integration is COMPLETELY INVALID
|
||||
- Test connections ONLY AFTER authentication is confirmed AND actual tools are discovered
|
||||
- **SEARCH FOR INTEGRATIONS** but do not automatically add them to the agent configuration
|
||||
- **CREATE CREDENTIAL PROFILES** and configure them for the agent, but do not modify the agent's core configuration
|
||||
- **WAIT FOR discover_user_mcp_servers RESPONSE** before proceeding with any tool configuration
|
||||
|
||||
**AUTHENTICATION ERROR HANDLING:**
|
||||
If user reports authentication issues:
|
||||
1. **Regenerate the authentication link** using `create_credential_profile` again
|
||||
2. **Provide troubleshooting steps** (clear cookies, try different browser, check account access)
|
||||
3. **Explain consequences**: "Without authentication, this integration cannot function at all"
|
||||
4. **Offer alternatives** if authentication continues to fail
|
||||
5. **Never skip authentication** - it's better to fail setup than have a broken integration
|
||||
|
||||
## 🌟 Self-Configuration Philosophy
|
||||
|
||||
You are Suna, and you can now evolve and adapt based on user needs. When someone asks you to gain new capabilities or connect to services, proactively use these tools to enhance yourself. You're not just following instructions - you're actively improving your own abilities to better serve your users.
|
||||
You are Suna, and you can now evolve and adapt based on user needs through credential profile configuration only. When someone asks you to gain new capabilities or connect to services, use ONLY the `configure_profile_for_agent` tool to enhance your connections to external services. **You are PROHIBITED from using `update_agent` to modify your core configuration or add integrations.**
|
||||
|
||||
Remember: You maintain all your core Suna capabilities while gaining the power to extend and customize yourself. This makes you more helpful and adaptable to each user's specific needs. ALWAYS use the `edit_file` tool to make changes to files. The `edit_file` tool is smart enough to find and replace the specific parts you mention, so you should:
|
||||
**CRITICAL RESTRICTIONS:**
|
||||
- **NEVER use `update_agent`** for adding integrations, MCP servers, workflows, or triggers
|
||||
- **ONLY use `configure_profile_for_agent`** to add authenticated service connections
|
||||
- You can search for and explore integrations but cannot automatically add them to your configuration
|
||||
- Focus on credential-based connections rather than core agent modifications
|
||||
- **MANDATORY**: Always use `discover_user_mcp_servers` after authentication to fetch real, available tools
|
||||
- **NEVER MAKE UP TOOL NAMES** - only use tools discovered through the authentication process
|
||||
|
||||
Remember: You maintain all your core Suna capabilities while gaining the power to connect to external services through authenticated profiles only. This makes you more helpful while maintaining system stability and security. **Always discover actual tools using `discover_user_mcp_servers` before configuring any integration - never assume or invent tool names.** ALWAYS use the `edit_file` tool to make changes to files. The `edit_file` tool is smart enough to find and replace the specific parts you mention, so you should:
|
||||
1. **Show only the exact lines that change**
|
||||
2. **Use `// ... existing code ...` for context when needed**
|
||||
3. **Never reproduce entire files or large unchanged sections**
|
||||
|
@ -962,9 +1092,6 @@ Remember: You maintain all your core Suna capabilities while gaining the power t
|
|||
|
||||
|
||||
def get_system_prompt():
|
||||
'''
|
||||
Returns the system prompt
|
||||
'''
|
||||
return SYSTEM_PROMPT.format(
|
||||
current_date=datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%d'),
|
||||
current_time=datetime.datetime.now(datetime.timezone.utc).strftime('%H:%M:%S'),
|
||||
|
|
|
@ -202,6 +202,7 @@ class MCPManager:
|
|||
"schema": schema
|
||||
}
|
||||
|
||||
logger.info(f"⚡ Registered {len(updated_schemas)} MCP tools (Redis cache enabled)")
|
||||
return mcp_wrapper_instance
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize MCP tools: {e}")
|
||||
|
|
|
@ -314,8 +314,16 @@ class AgentConfigTool(AgentBuilderBaseTool):
|
|||
"updated_at": agent_data.get("updated_at"),
|
||||
"current_version": agent_config.get("version_name", "v1") if version_data else "No version data"
|
||||
}
|
||||
|
||||
tools_count = len([t for t, cfg in config_summary["agentpress_tools"].items() if cfg.get("enabled")])
|
||||
|
||||
enabled_tools = []
|
||||
for tool_name, tool_config in config_summary["agentpress_tools"].items():
|
||||
if isinstance(tool_config, bool):
|
||||
if tool_config:
|
||||
enabled_tools.append(tool_name)
|
||||
elif isinstance(tool_config, dict):
|
||||
if tool_config.get("enabled", False):
|
||||
enabled_tools.append(tool_name)
|
||||
tools_count = len(enabled_tools)
|
||||
mcps_count = len(config_summary["configured_mcps"])
|
||||
custom_mcps_count = len(config_summary["custom_mcps"])
|
||||
|
||||
|
|
|
@ -140,7 +140,15 @@ class CredentialProfileTool(AgentBuilderBaseTool):
|
|||
|
||||
if result.connected_account.redirect_url:
|
||||
response_data["connection_link"] = result.connected_account.redirect_url
|
||||
response_data["instructions"] = f"🔗 **IMPORTANT: Please visit the connection link to authenticate your {result.toolkit.name} account with this profile. After connecting, you'll be able to use {result.toolkit.name} tools in your agent.**"
|
||||
# Include both the toolkit name and slug in a parseable format
|
||||
# Format: [toolkit:slug:name] to help frontend identify the service accurately
|
||||
response_data["instructions"] = f"""🔗 **{result.toolkit.name} Authentication Required**
|
||||
|
||||
Please authenticate your {result.toolkit.name} account by clicking the link below:
|
||||
|
||||
[toolkit:{toolkit_slug}:{result.toolkit.name}] Authentication: {result.connected_account.redirect_url}
|
||||
|
||||
After connecting, you'll be able to use {result.toolkit.name} tools in your agent."""
|
||||
else:
|
||||
response_data["instructions"] = f"This {result.toolkit.name} profile has been created and is ready to use."
|
||||
|
||||
|
@ -256,12 +264,48 @@ class CredentialProfileTool(AgentBuilderBaseTool):
|
|||
change_description=f"Configured {display_name or profile.display_name} with {len(enabled_tools)} tools"
|
||||
)
|
||||
|
||||
# Dynamically register the MCP tools in the current runtime
|
||||
try:
|
||||
from agent.tools.mcp_tool_wrapper import MCPToolWrapper
|
||||
|
||||
mcp_config_for_wrapper = {
|
||||
'name': profile.toolkit_name,
|
||||
'qualifiedName': f"composio.{profile.toolkit_slug}",
|
||||
'config': {
|
||||
'profile_id': profile_id,
|
||||
'toolkit_slug': profile.toolkit_slug,
|
||||
'mcp_qualified_name': profile.mcp_qualified_name
|
||||
},
|
||||
'enabledTools': enabled_tools,
|
||||
'instructions': '',
|
||||
'isCustom': True,
|
||||
'customType': 'composio'
|
||||
}
|
||||
|
||||
mcp_wrapper_instance = MCPToolWrapper(mcp_configs=[mcp_config_for_wrapper])
|
||||
await mcp_wrapper_instance.initialize_and_register_tools()
|
||||
updated_schemas = mcp_wrapper_instance.get_schemas()
|
||||
|
||||
for method_name, schema_list in updated_schemas.items():
|
||||
for schema in schema_list:
|
||||
self.thread_manager.tool_registry.tools[method_name] = {
|
||||
"instance": mcp_wrapper_instance,
|
||||
"schema": schema
|
||||
}
|
||||
logger.info(f"Dynamically registered MCP tool: {method_name}")
|
||||
|
||||
logger.info(f"Successfully registered {len(updated_schemas)} MCP tools dynamically for {profile.toolkit_name}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not dynamically register MCP tools in current runtime: {str(e)}. Tools will be available on next agent run.")
|
||||
|
||||
return self.success_response({
|
||||
"message": f"Profile '{profile.profile_name}' updated with {len(enabled_tools)} tools",
|
||||
"message": f"Profile '{profile.profile_name}' configured with {len(enabled_tools)} tools and registered in current runtime",
|
||||
"enabled_tools": enabled_tools,
|
||||
"total_tools": len(enabled_tools),
|
||||
"version_id": new_version.version_id,
|
||||
"version_name": new_version.version_name
|
||||
"version_name": new_version.version_name,
|
||||
"runtime_registration": "success"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
|
|
|
@ -52,8 +52,13 @@ class WorkflowTool(AgentBuilderBaseTool):
|
|||
|
||||
agentpress_tools = agent_config.get('agentpress_tools', {})
|
||||
for tool_key, tool_names in tool_mapping.items():
|
||||
if agentpress_tools.get(tool_key, {}).get('enabled', False):
|
||||
available_tools.extend(tool_names)
|
||||
tool_config = agentpress_tools.get(tool_key, False)
|
||||
if isinstance(tool_config, bool):
|
||||
if tool_config:
|
||||
available_tools.extend(tool_names)
|
||||
elif isinstance(tool_config, dict):
|
||||
if tool_config.get('enabled', False):
|
||||
available_tools.extend(tool_names)
|
||||
|
||||
configured_mcps = agent_config.get('configured_mcps', [])
|
||||
for mcp in configured_mcps:
|
||||
|
|
|
@ -3,20 +3,120 @@ from agentpress.tool import Tool, ToolResult, ToolSchema, SchemaType
|
|||
from mcp_module import mcp_service
|
||||
from utils.logger import logger
|
||||
import inspect
|
||||
import asyncio
|
||||
import time
|
||||
import hashlib
|
||||
import json
|
||||
from agent.tools.utils.mcp_connection_manager import MCPConnectionManager
|
||||
from agent.tools.utils.custom_mcp_handler import CustomMCPHandler
|
||||
from agent.tools.utils.dynamic_tool_builder import DynamicToolBuilder
|
||||
from agent.tools.utils.mcp_tool_executor import MCPToolExecutor
|
||||
from services import redis as redis_service
|
||||
|
||||
|
||||
class MCPSchemaRedisCache:
|
||||
def __init__(self, ttl_seconds: int = 3600, key_prefix: str = "mcp_schema:"):
|
||||
self._ttl = ttl_seconds
|
||||
self._key_prefix = key_prefix
|
||||
self._redis_client = None
|
||||
|
||||
async def _ensure_redis(self):
|
||||
if not self._redis_client:
|
||||
try:
|
||||
self._redis_client = await redis_service.get_client()
|
||||
except Exception as e:
|
||||
logger.warning(f"Redis not available for MCP cache: {e}")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _get_cache_key(self, config: Dict[str, Any]) -> str:
|
||||
config_str = json.dumps(config, sort_keys=True)
|
||||
config_hash = hashlib.md5(config_str.encode()).hexdigest()
|
||||
return f"{self._key_prefix}{config_hash}"
|
||||
|
||||
async def get(self, config: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
if not await self._ensure_redis():
|
||||
return None
|
||||
|
||||
try:
|
||||
key = self._get_cache_key(config)
|
||||
cached_data = await self._redis_client.get(key)
|
||||
|
||||
if cached_data:
|
||||
logger.debug(f"⚡ Redis cache hit for MCP: {config.get('name', config.get('qualifiedName', 'Unknown'))}")
|
||||
return json.loads(cached_data)
|
||||
else:
|
||||
logger.debug(f"Redis cache miss for MCP: {config.get('name', config.get('qualifiedName', 'Unknown'))}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error reading from Redis cache: {e}")
|
||||
return None
|
||||
|
||||
async def set(self, config: Dict[str, Any], data: Dict[str, Any]):
|
||||
if not await self._ensure_redis():
|
||||
return
|
||||
|
||||
try:
|
||||
key = self._get_cache_key(config)
|
||||
serialized_data = json.dumps(data)
|
||||
|
||||
await self._redis_client.setex(key, self._ttl, serialized_data)
|
||||
logger.debug(f"✅ Cached MCP schema in Redis for {config.get('name', config.get('qualifiedName', 'Unknown'))} (TTL: {self._ttl}s)")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error writing to Redis cache: {e}")
|
||||
|
||||
async def clear_pattern(self, pattern: Optional[str] = None):
|
||||
if not await self._ensure_redis():
|
||||
return
|
||||
try:
|
||||
if pattern:
|
||||
search_pattern = f"{self._key_prefix}{pattern}*"
|
||||
else:
|
||||
search_pattern = f"{self._key_prefix}*"
|
||||
|
||||
keys = []
|
||||
async for key in self._redis_client.scan_iter(match=search_pattern):
|
||||
keys.append(key)
|
||||
|
||||
if keys:
|
||||
await self._redis_client.delete(*keys)
|
||||
logger.info(f"Cleared {len(keys)} MCP schema cache entries from Redis")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error clearing Redis cache: {e}")
|
||||
|
||||
async def get_stats(self) -> Dict[str, Any]:
|
||||
if not await self._ensure_redis():
|
||||
return {"available": False}
|
||||
try:
|
||||
count = 0
|
||||
async for _ in self._redis_client.scan_iter(match=f"{self._key_prefix}*"):
|
||||
count += 1
|
||||
|
||||
return {
|
||||
"available": True,
|
||||
"cached_schemas": count,
|
||||
"ttl_seconds": self._ttl,
|
||||
"key_prefix": self._key_prefix
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting cache stats: {e}")
|
||||
return {"available": False, "error": str(e)}
|
||||
|
||||
|
||||
_redis_cache = MCPSchemaRedisCache(ttl_seconds=3600)
|
||||
|
||||
class MCPToolWrapper(Tool):
|
||||
def __init__(self, mcp_configs: Optional[List[Dict[str, Any]]] = None):
|
||||
def __init__(self, mcp_configs: Optional[List[Dict[str, Any]]] = None, use_cache: bool = True):
|
||||
self.mcp_manager = mcp_service
|
||||
self.mcp_configs = mcp_configs or []
|
||||
self._initialized = False
|
||||
self._schemas: Dict[str, List[ToolSchema]] = {}
|
||||
self._dynamic_tools = {}
|
||||
self._custom_tools = {}
|
||||
self.use_cache = use_cache
|
||||
|
||||
self.connection_manager = MCPConnectionManager()
|
||||
self.custom_handler = CustomMCPHandler(self.connection_manager)
|
||||
|
@ -32,23 +132,109 @@ class MCPToolWrapper(Tool):
|
|||
self._initialized = True
|
||||
|
||||
async def _initialize_servers(self):
|
||||
start_time = time.time()
|
||||
|
||||
standard_configs = [cfg for cfg in self.mcp_configs if not cfg.get('isCustom', False)]
|
||||
custom_configs = [cfg for cfg in self.mcp_configs if cfg.get('isCustom', False)]
|
||||
|
||||
cached_configs = []
|
||||
cached_tools_data = []
|
||||
|
||||
initialization_tasks = []
|
||||
|
||||
if standard_configs:
|
||||
await self._initialize_standard_servers(standard_configs)
|
||||
for config in standard_configs:
|
||||
if self.use_cache:
|
||||
cached_data = await _redis_cache.get(config)
|
||||
if cached_data:
|
||||
cached_configs.append(config.get('qualifiedName', 'Unknown'))
|
||||
cached_tools_data.append(cached_data)
|
||||
continue
|
||||
|
||||
task = self._initialize_single_standard_server(config)
|
||||
initialization_tasks.append(('standard', config, task))
|
||||
|
||||
if custom_configs:
|
||||
await self.custom_handler.initialize_custom_mcps(custom_configs)
|
||||
for config in custom_configs:
|
||||
if self.use_cache:
|
||||
cached_data = await _redis_cache.get(config)
|
||||
if cached_data:
|
||||
cached_configs.append(config.get('name', 'Unknown'))
|
||||
cached_tools_data.append(cached_data)
|
||||
continue
|
||||
|
||||
task = self._initialize_single_custom_mcp(config)
|
||||
initialization_tasks.append(('custom', config, task))
|
||||
|
||||
if cached_tools_data:
|
||||
logger.info(f"⚡ Loaded {len(cached_configs)} MCP schemas from Redis cache: {', '.join(cached_configs)}")
|
||||
for cached_data in cached_tools_data:
|
||||
try:
|
||||
if cached_data.get('type') == 'standard':
|
||||
logger.debug("Standard MCP tools found in cache but require connection to restore")
|
||||
elif cached_data.get('type') == 'custom':
|
||||
custom_tools = cached_data.get('tools', {})
|
||||
if custom_tools:
|
||||
self.custom_handler.custom_tools.update(custom_tools)
|
||||
logger.debug(f"Restored {len(custom_tools)} custom tools from cache")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to restore cached tools: {e}")
|
||||
|
||||
if initialization_tasks:
|
||||
logger.info(f"🚀 Initializing {len(initialization_tasks)} MCP servers in parallel (cache enabled: {self.use_cache})...")
|
||||
|
||||
tasks = [task for _, _, task in initialization_tasks]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
successful = 0
|
||||
failed = 0
|
||||
|
||||
for i, result in enumerate(results):
|
||||
task_type, config, _ = initialization_tasks[i]
|
||||
if isinstance(result, Exception):
|
||||
failed += 1
|
||||
config_name = config.get('name', config.get('qualifiedName', 'Unknown'))
|
||||
logger.error(f"Failed to initialize MCP server '{config_name}': {result}")
|
||||
else:
|
||||
successful += 1
|
||||
if self.use_cache and result:
|
||||
await _redis_cache.set(config, result)
|
||||
|
||||
elapsed_time = time.time() - start_time
|
||||
logger.info(f"⚡ MCP initialization completed in {elapsed_time:.2f}s - {successful} successful, {failed} failed, {len(cached_configs)} from cache")
|
||||
else:
|
||||
if cached_configs:
|
||||
elapsed_time = time.time() - start_time
|
||||
logger.info(f"⚡ All {len(cached_configs)} MCP schemas loaded from Redis cache in {elapsed_time:.2f}s - instant startup!")
|
||||
else:
|
||||
logger.info("No MCP servers to initialize")
|
||||
|
||||
async def _initialize_single_standard_server(self, config: Dict[str, Any]):
|
||||
try:
|
||||
logger.debug(f"Connecting to standard MCP server: {config['qualifiedName']}")
|
||||
await self.mcp_manager.connect_server(config)
|
||||
logger.debug(f"✓ Connected to MCP server: {config['qualifiedName']}")
|
||||
|
||||
tools_info = self.mcp_manager.get_all_tools_openapi()
|
||||
return {'tools': tools_info, 'type': 'standard', 'timestamp': time.time()}
|
||||
except Exception as e:
|
||||
logger.error(f"✗ Failed to connect to MCP server {config['qualifiedName']}: {e}")
|
||||
raise e
|
||||
|
||||
async def _initialize_single_custom_mcp(self, config: Dict[str, Any]):
|
||||
try:
|
||||
logger.debug(f"Initializing custom MCP: {config.get('name', 'Unknown')}")
|
||||
await self.custom_handler._initialize_single_custom_mcp(config)
|
||||
logger.debug(f"✓ Initialized custom MCP: {config.get('name', 'Unknown')}")
|
||||
|
||||
custom_tools = self.custom_handler.get_custom_tools()
|
||||
return {'tools': custom_tools, 'type': 'custom', 'timestamp': time.time()}
|
||||
except Exception as e:
|
||||
logger.error(f"✗ Failed to initialize custom MCP {config.get('name', 'Unknown')}: {e}")
|
||||
raise e
|
||||
|
||||
async def _initialize_standard_servers(self, standard_configs: List[Dict[str, Any]]):
|
||||
for config in standard_configs:
|
||||
try:
|
||||
logger.info(f"Attempting to connect to MCP server: {config['qualifiedName']}")
|
||||
await self.mcp_manager.connect_server(config)
|
||||
logger.info(f"Successfully connected to MCP server: {config['qualifiedName']}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to MCP server {config['qualifiedName']}: {e}")
|
||||
pass
|
||||
|
||||
async def _create_dynamic_tools(self):
|
||||
try:
|
||||
|
@ -77,7 +263,6 @@ class MCPToolWrapper(Tool):
|
|||
|
||||
logger.info(f"Created {len(self._dynamic_tools)} dynamic MCP tool methods")
|
||||
|
||||
# Re-register schemas to pick up the dynamic methods
|
||||
self._register_schemas()
|
||||
logger.info(f"Re-registered schemas after creating dynamic tools - total: {len(self._schemas)}")
|
||||
|
||||
|
|
|
@ -12,18 +12,33 @@ from .mcp_connection_manager import MCPConnectionManager
|
|||
class CustomMCPHandler:
|
||||
def __init__(self, connection_manager: MCPConnectionManager):
|
||||
self.connection_manager = connection_manager
|
||||
self.custom_tools: Dict[str, Dict[str, Any]] = {}
|
||||
self.custom_tools = {}
|
||||
|
||||
async def initialize_custom_mcps(self, custom_configs: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
|
||||
initialization_tasks = []
|
||||
|
||||
for config in custom_configs:
|
||||
try:
|
||||
await self._initialize_single_custom_mcp(config)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize custom MCP {config.get('name', 'Unknown')}: {e}")
|
||||
continue
|
||||
task = self._initialize_single_custom_mcp_safe(config)
|
||||
initialization_tasks.append(task)
|
||||
|
||||
if initialization_tasks:
|
||||
logger.info(f"Initializing {len(initialization_tasks)} custom MCPs in parallel...")
|
||||
results = await asyncio.gather(*initialization_tasks, return_exceptions=True)
|
||||
|
||||
for i, result in enumerate(results):
|
||||
if isinstance(result, Exception):
|
||||
config_name = custom_configs[i].get('name', 'Unknown')
|
||||
logger.error(f"Failed to initialize custom MCP {config_name}: {result}")
|
||||
|
||||
return self.custom_tools
|
||||
|
||||
async def _initialize_single_custom_mcp_safe(self, config: Dict[str, Any]):
|
||||
try:
|
||||
await self._initialize_single_custom_mcp(config)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize custom MCP {config.get('name', 'Unknown')}: {e}")
|
||||
return e
|
||||
|
||||
async def _initialize_single_custom_mcp(self, config: Dict[str, Any]):
|
||||
custom_type = config.get('customType', 'sse')
|
||||
server_config = config.get('config', {})
|
||||
|
|
|
@ -419,10 +419,7 @@ async def get_my_templates(
|
|||
logger.info(f"Retrieved {len(templates)} templates for user {user_id}")
|
||||
|
||||
return [
|
||||
TemplateResponse(
|
||||
**format_template_for_response(template),
|
||||
creator_name=None
|
||||
)
|
||||
TemplateResponse(**format_template_for_response(template))
|
||||
for template in templates
|
||||
]
|
||||
|
||||
|
@ -448,10 +445,7 @@ async def get_template(
|
|||
|
||||
logger.info(f"User {user_id} accessing template {template_id}")
|
||||
|
||||
return TemplateResponse(
|
||||
**format_template_for_response(template),
|
||||
creator_name=None
|
||||
)
|
||||
return TemplateResponse(**format_template_for_response(template))
|
||||
|
||||
except HTTPException:
|
||||
# Re-raise HTTP exceptions from our validation functions
|
||||
|
|
|
@ -44,6 +44,7 @@ class AgentTemplate:
|
|||
avatar: Optional[str] = None
|
||||
avatar_color: Optional[str] = None
|
||||
metadata: ConfigType = field(default_factory=dict)
|
||||
creator_name: Optional[str] = None
|
||||
|
||||
def with_public_status(self, is_public: bool, published_at: Optional[datetime] = None) -> 'AgentTemplate':
|
||||
return AgentTemplate(
|
||||
|
@ -82,10 +83,8 @@ class AgentTemplate:
|
|||
mcp_type = mcp.get('type', 'sse')
|
||||
mcp_name = mcp['name']
|
||||
|
||||
# Use explicitly stored qualified name (should always be present after sanitization)
|
||||
qualified_name = mcp.get('mcp_qualified_name') or mcp.get('qualifiedName') # fallback for old format
|
||||
qualified_name = mcp.get('mcp_qualified_name') or mcp.get('qualifiedName')
|
||||
if not qualified_name:
|
||||
# Fallback for legacy templates (should rarely be needed)
|
||||
if mcp_type == 'pipedream':
|
||||
app_slug = mcp.get('app_slug') or mcp.get('config', {}).get('headers', {}).get('x-pd-app-slug')
|
||||
if not app_slug:
|
||||
|
@ -98,7 +97,6 @@ class AgentTemplate:
|
|||
safe_name = mcp_name.replace(' ', '_').lower()
|
||||
qualified_name = f"custom_{mcp_type}_{safe_name}"
|
||||
|
||||
# Determine required config based on type
|
||||
if mcp_type in ['pipedream', 'composio']:
|
||||
required_config = []
|
||||
elif mcp_type in ['http', 'sse', 'json']:
|
||||
|
@ -192,6 +190,15 @@ class TemplateService:
|
|||
if not result.data:
|
||||
return None
|
||||
|
||||
creator_id = result.data['creator_id']
|
||||
creator_result = await client.schema('basejump').from_('accounts').select('id, name, slug').eq('id', creator_id).execute()
|
||||
|
||||
creator_name = None
|
||||
if creator_result.data:
|
||||
account = creator_result.data[0]
|
||||
creator_name = account.get('name') or account.get('slug')
|
||||
|
||||
result.data['creator_name'] = creator_name
|
||||
return self._map_to_template(result.data)
|
||||
|
||||
async def get_user_templates(self, creator_id: str) -> List[AgentTemplate]:
|
||||
|
@ -201,7 +208,22 @@ class TemplateService:
|
|||
.order('created_at', desc=True)\
|
||||
.execute()
|
||||
|
||||
return [self._map_to_template(data) for data in result.data]
|
||||
if not result.data:
|
||||
return []
|
||||
|
||||
creator_result = await client.schema('basejump').from_('accounts').select('id, name, slug').eq('id', creator_id).execute()
|
||||
|
||||
creator_name = None
|
||||
if creator_result.data:
|
||||
account = creator_result.data[0]
|
||||
creator_name = account.get('name') or account.get('slug')
|
||||
|
||||
templates = []
|
||||
for template_data in result.data:
|
||||
template_data['creator_name'] = creator_name
|
||||
templates.append(self._map_to_template(template_data))
|
||||
|
||||
return templates
|
||||
|
||||
async def get_public_templates(self) -> List[AgentTemplate]:
|
||||
client = await self._db.client
|
||||
|
@ -211,7 +233,24 @@ class TemplateService:
|
|||
.order('marketplace_published_at', desc=True)\
|
||||
.execute()
|
||||
|
||||
return [self._map_to_template(data) for data in result.data]
|
||||
if not result.data:
|
||||
return []
|
||||
|
||||
creator_ids = list(set(template['creator_id'] for template in result.data))
|
||||
accounts_result = await client.schema('basejump').from_('accounts').select('id, name, slug').in_('id', creator_ids).execute()
|
||||
|
||||
creator_names = {}
|
||||
if accounts_result.data:
|
||||
for account in accounts_result.data:
|
||||
creator_names[account['id']] = account.get('name') or account.get('slug')
|
||||
|
||||
templates = []
|
||||
for template_data in result.data:
|
||||
creator_name = creator_names.get(template_data['creator_id'])
|
||||
template_data['creator_name'] = creator_name
|
||||
templates.append(self._map_to_template(template_data))
|
||||
|
||||
return templates
|
||||
|
||||
async def publish_template(self, template_id: str, creator_id: str) -> bool:
|
||||
logger.info(f"Publishing template {template_id}")
|
||||
|
@ -450,6 +489,8 @@ class TemplateService:
|
|||
await client.table('agent_templates').insert(template_data).execute()
|
||||
|
||||
def _map_to_template(self, data: Dict[str, Any]) -> AgentTemplate:
|
||||
creator_name = data.get('creator_name')
|
||||
|
||||
return AgentTemplate(
|
||||
template_id=data['template_id'],
|
||||
creator_id=data['creator_id'],
|
||||
|
@ -464,7 +505,8 @@ class TemplateService:
|
|||
updated_at=datetime.fromisoformat(data['updated_at'].replace('Z', '+00:00')),
|
||||
avatar=data.get('avatar'),
|
||||
avatar_color=data.get('avatar_color'),
|
||||
metadata=data.get('metadata', {})
|
||||
metadata=data.get('metadata', {}),
|
||||
creator_name=creator_name
|
||||
)
|
||||
|
||||
def get_template_service(db_connection: DBConnection) -> TemplateService:
|
||||
|
|
|
@ -133,7 +133,8 @@ def format_template_for_response(template: AgentTemplate) -> Dict[str, Any]:
|
|||
'updated_at': template.updated_at.isoformat(),
|
||||
'avatar': template.avatar,
|
||||
'avatar_color': template.avatar_color,
|
||||
'metadata': template.metadata
|
||||
'metadata': template.metadata,
|
||||
'creator_name': template.creator_name
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -468,8 +468,13 @@ class WorkflowExecutor:
|
|||
}
|
||||
|
||||
for tool_key, tool_names in tool_mapping.items():
|
||||
if agentpress_tools.get(tool_key, {}).get('enabled', False):
|
||||
available_tools.extend(tool_names)
|
||||
tool_config = agentpress_tools.get(tool_key, False)
|
||||
if isinstance(tool_config, bool):
|
||||
if tool_config:
|
||||
available_tools.extend(tool_names)
|
||||
elif isinstance(tool_config, dict):
|
||||
if tool_config.get('enabled', False):
|
||||
available_tools.extend(tool_names)
|
||||
|
||||
all_mcps = []
|
||||
if agent_config.get('configured_mcps'):
|
||||
|
|
|
@ -504,12 +504,12 @@ export default function AgentConfigurationPage() {
|
|||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Loader2 className="h-3 w-3 animate-spin mr-2" />
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="h-3 w-3 mr-2" />
|
||||
<Save className="h-3 w-3" />
|
||||
Save
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -13,34 +13,30 @@ import { useAgentTools } from '@/hooks/react-query/agents/use-agent-tools';
|
|||
import { useAgent } from '@/hooks/react-query/agents/use-agents';
|
||||
import { ConditionalStep } from '@/components/agents/workflows/conditional-workflow-builder';
|
||||
import { WorkflowBuilder } from '@/components/workflows/workflow-builder';
|
||||
import { WorkflowExecutionDialog } from '@/components/workflows/workflow-execution-dialog';
|
||||
|
||||
const convertToNestedJSON = (steps: ConditionalStep[]): any[] => {
|
||||
// Clean, simple conversion - preserve the exact structure with order field for validation
|
||||
let globalOrder = 1;
|
||||
|
||||
const convertStepsWithNesting = (stepList: ConditionalStep[]): any[] => {
|
||||
return stepList.map((step) => {
|
||||
// Build clean step object with required fields for backend validation
|
||||
const jsonStep: any = {
|
||||
id: step.id, // CRITICAL: Always include ID
|
||||
id: step.id,
|
||||
name: step.name,
|
||||
description: step.description,
|
||||
type: step.type,
|
||||
config: step.config || {},
|
||||
order: globalOrder++ // Required by backend validation
|
||||
order: globalOrder++
|
||||
};
|
||||
|
||||
// Add conditional metadata if present
|
||||
if (step.type === 'condition' && step.conditions) {
|
||||
jsonStep.conditions = step.conditions;
|
||||
}
|
||||
|
||||
// Add parent relationship if present
|
||||
if (step.parentConditionalId) {
|
||||
jsonStep.parentConditionalId = step.parentConditionalId;
|
||||
}
|
||||
|
||||
// Add children if present
|
||||
if (step.children && step.children.length > 0) {
|
||||
jsonStep.children = convertStepsWithNesting(step.children);
|
||||
}
|
||||
|
@ -245,6 +241,10 @@ export default function WorkflowPage() {
|
|||
const [triggerPhrase, setTriggerPhrase] = useState('');
|
||||
const [isDefault, setIsDefault] = useState(false);
|
||||
const [steps, setSteps] = useState<ConditionalStep[]>([]);
|
||||
|
||||
// Execution state
|
||||
const [isExecuteDialogOpen, setIsExecuteDialogOpen] = useState(false);
|
||||
const [currentWorkflow, setCurrentWorkflow] = useState<any>(null);
|
||||
|
||||
// Wrapper for setSteps
|
||||
const setStepsWithDebug = useCallback((newSteps: ConditionalStep[]) => {
|
||||
|
@ -345,6 +345,21 @@ export default function WorkflowPage() {
|
|||
}
|
||||
}, [workflowName, workflowDescription, triggerPhrase, isDefault, steps, agentId, workflowId, isEditing, createWorkflowMutation, updateWorkflowMutation, router]);
|
||||
|
||||
const handleExecute = useCallback(() => {
|
||||
const workflow = workflows.find(w => w.id === workflowId);
|
||||
if (workflow) {
|
||||
setCurrentWorkflow(workflow);
|
||||
setIsExecuteDialogOpen(true);
|
||||
} else {
|
||||
toast.error('Workflow not found or not saved yet');
|
||||
}
|
||||
}, [workflows, workflowId]);
|
||||
|
||||
const handleExecutionSuccess = useCallback(() => {
|
||||
setIsExecuteDialogOpen(false);
|
||||
setCurrentWorkflow(null);
|
||||
}, []);
|
||||
|
||||
if (isLoading || isLoadingWorkflows) {
|
||||
return (
|
||||
<div className="h-screen flex items-center justify-center">
|
||||
|
@ -357,20 +372,33 @@ export default function WorkflowPage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<WorkflowBuilder
|
||||
steps={steps}
|
||||
onStepsChange={setStepsWithDebug}
|
||||
agentTools={agentTools}
|
||||
isLoadingTools={isLoadingTools}
|
||||
agentId={agentId}
|
||||
versionData={versionData}
|
||||
onToolsUpdate={handleToolsUpdate}
|
||||
workflowName={workflowName}
|
||||
workflowDescription={workflowDescription}
|
||||
onSave={handleSave}
|
||||
isSaving={createWorkflowMutation.isPending || updateWorkflowMutation.isPending}
|
||||
onNameChange={setWorkflowName}
|
||||
onDescriptionChange={setWorkflowDescription}
|
||||
/>
|
||||
<>
|
||||
<WorkflowBuilder
|
||||
steps={steps}
|
||||
onStepsChange={setStepsWithDebug}
|
||||
agentTools={agentTools}
|
||||
isLoadingTools={isLoadingTools}
|
||||
agentId={agentId}
|
||||
workflowId={workflowId}
|
||||
versionData={versionData}
|
||||
onToolsUpdate={handleToolsUpdate}
|
||||
workflowName={workflowName}
|
||||
workflowDescription={workflowDescription}
|
||||
onSave={handleSave}
|
||||
isSaving={createWorkflowMutation.isPending || updateWorkflowMutation.isPending}
|
||||
onExecute={isEditing ? handleExecute : undefined}
|
||||
isExecuting={false}
|
||||
onNameChange={setWorkflowName}
|
||||
onDescriptionChange={setWorkflowDescription}
|
||||
/>
|
||||
|
||||
<WorkflowExecutionDialog
|
||||
open={isExecuteDialogOpen}
|
||||
onOpenChange={setIsExecuteDialogOpen}
|
||||
workflow={currentWorkflow}
|
||||
agentId={agentId}
|
||||
onSuccess={handleExecutionSuccess}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Download, CheckCircle, Loader2, Globe, GlobeLock, GitBranch, Trash2, MoreVertical } from 'lucide-react';
|
||||
import { Download, CheckCircle, Loader2, Globe, GlobeLock, GitBranch, Trash2, MoreVertical, User } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
|
@ -147,7 +147,11 @@ const AgentBadges: React.FC<{ agent: AgentData, isSunaAgent: boolean }> = ({ age
|
|||
);
|
||||
|
||||
const MarketplaceMetadata: React.FC<{ data: MarketplaceData }> = ({ data }) => (
|
||||
<div className="flex items-center text-xs text-muted-foreground">
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<div className="flex items-center gap-1">
|
||||
<User className="h-3 w-3" />
|
||||
<span>{data.creator_name || 'Anonymous'}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Download className="h-3 w-3" />
|
||||
<span>{data.download_count} installs</span>
|
||||
|
|
|
@ -2,14 +2,13 @@
|
|||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Plus, AlertCircle, Workflow, Trash2, Calendar } from 'lucide-react';
|
||||
import { Plus, AlertCircle, Workflow, Trash2, Calendar, Play } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { WorkflowExecutionDialog } from '@/components/workflows/workflow-execution-dialog';
|
||||
import { Tabs, TabsContent } from '@/components/ui/tabs';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { toast } from 'sonner';
|
||||
|
@ -17,8 +16,7 @@ import {
|
|||
useAgentWorkflows,
|
||||
useCreateAgentWorkflow,
|
||||
useUpdateAgentWorkflow,
|
||||
useDeleteAgentWorkflow,
|
||||
useExecuteWorkflow,
|
||||
useDeleteAgentWorkflow
|
||||
} from '@/hooks/react-query/agents/use-agent-workflows';
|
||||
import {
|
||||
AgentWorkflow
|
||||
|
@ -36,7 +34,6 @@ export function AgentWorkflowsConfiguration({ agentId, agentName }: AgentWorkflo
|
|||
const createWorkflowMutation = useCreateAgentWorkflow();
|
||||
const updateWorkflowMutation = useUpdateAgentWorkflow();
|
||||
const deleteWorkflowMutation = useDeleteAgentWorkflow();
|
||||
const executeWorkflowMutation = useExecuteWorkflow();
|
||||
|
||||
const [isExecuteDialogOpen, setIsExecuteDialogOpen] = useState(false);
|
||||
const [workflowToExecute, setWorkflowToExecute] = useState<AgentWorkflow | null>(null);
|
||||
|
@ -44,7 +41,7 @@ export function AgentWorkflowsConfiguration({ agentId, agentName }: AgentWorkflo
|
|||
const [workflowToDelete, setWorkflowToDelete] = useState<AgentWorkflow | null>(null);
|
||||
const [activeTab, setActiveTab] = useState('workflows');
|
||||
|
||||
const [executionInput, setExecutionInput] = useState<string>('');
|
||||
|
||||
|
||||
const handleCreateWorkflow = useCallback(async () => {
|
||||
try {
|
||||
|
@ -111,27 +108,10 @@ export function AgentWorkflowsConfiguration({ agentId, agentName }: AgentWorkflo
|
|||
}
|
||||
}, [agentId, workflowToDelete, deleteWorkflowMutation]);
|
||||
|
||||
const handleConfirmExecution = useCallback(async () => {
|
||||
if (!workflowToExecute) return;
|
||||
|
||||
try {
|
||||
const result = await executeWorkflowMutation.mutateAsync({
|
||||
agentId,
|
||||
workflowId: workflowToExecute.id,
|
||||
execution: {
|
||||
input_data: executionInput.trim() ? { prompt: executionInput } : undefined
|
||||
}
|
||||
});
|
||||
|
||||
setIsExecuteDialogOpen(false);
|
||||
setWorkflowToExecute(null);
|
||||
setExecutionInput('');
|
||||
|
||||
toast.success(`${result.message}`);
|
||||
} catch (error) {
|
||||
toast.error('Failed to execute workflow');
|
||||
}
|
||||
}, [agentId, workflowToExecute, executionInput, executeWorkflowMutation]);
|
||||
const handleExecutionSuccess = useCallback((result: any) => {
|
||||
setIsExecuteDialogOpen(false);
|
||||
setWorkflowToExecute(null);
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
|
@ -190,7 +170,7 @@ export function AgentWorkflowsConfiguration({ agentId, agentName }: AgentWorkflo
|
|||
{workflows.map((workflow) => (
|
||||
<div key={workflow.id} className="group">
|
||||
<Card
|
||||
className="p-6 rounded-lg shadow-sm hover:shadow-md transition-shadow cursor-pointer"
|
||||
className="p-6 rounded-xl shadow-sm hover:shadow-md transition-shadow cursor-pointer"
|
||||
onClick={() => handleWorkflowClick(workflow.id)}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
|
@ -214,9 +194,9 @@ export function AgentWorkflowsConfiguration({ agentId, agentName }: AgentWorkflo
|
|||
e.stopPropagation();
|
||||
handleExecuteWorkflow(workflow);
|
||||
}}
|
||||
disabled={workflow.status !== 'active' || executeWorkflowMutation.isPending}
|
||||
disabled={workflow.status !== 'active'}
|
||||
>
|
||||
<Workflow className="h-4 w-4" />
|
||||
<Play className="h-4 w-4" />
|
||||
Execute
|
||||
</Button>
|
||||
<Button
|
||||
|
@ -241,44 +221,13 @@ export function AgentWorkflowsConfiguration({ agentId, agentName }: AgentWorkflo
|
|||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
<Dialog open={isExecuteDialogOpen} onOpenChange={setIsExecuteDialogOpen}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Execute Workflow</DialogTitle>
|
||||
<DialogDescription>
|
||||
Provide input data for "{workflowToExecute?.name}" workflow
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>What would you like the workflow to work on?</Label>
|
||||
<Textarea
|
||||
value={executionInput}
|
||||
onChange={(e) => setExecutionInput(e.target.value)}
|
||||
placeholder="Enter your request..."
|
||||
rows={4}
|
||||
className="resize-none"
|
||||
required={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsExecuteDialogOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleConfirmExecution}
|
||||
disabled={executeWorkflowMutation.isPending}
|
||||
>
|
||||
{executeWorkflowMutation.isPending ? 'Executing...' : 'Execute Workflow'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<WorkflowExecutionDialog
|
||||
open={isExecuteDialogOpen}
|
||||
onOpenChange={setIsExecuteDialogOpen}
|
||||
workflow={workflowToExecute}
|
||||
agentId={agentId}
|
||||
onSuccess={handleExecutionSuccess}
|
||||
/>
|
||||
|
||||
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
|
|
|
@ -613,7 +613,7 @@ export function ConditionalWorkflowBuilder({
|
|||
onClick={() => removeStep(step.id)}
|
||||
className="w-full justify-start text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
<Trash2 className="h-4 w-4" />
|
||||
Delete step
|
||||
</Button>
|
||||
</PopoverContent>
|
||||
|
|
|
@ -87,34 +87,67 @@ function formatToolkitName(toolkitSlug: string): string {
|
|||
}
|
||||
|
||||
function extractToolkitInfoFromContext(content: string, urlStartIndex: number): { toolkitName: string | null; toolkitSlug: string | null } {
|
||||
// Look for toolkit information in the surrounding context
|
||||
const contextBefore = content.substring(Math.max(0, urlStartIndex - 500), urlStartIndex);
|
||||
const contextAfter = content.substring(urlStartIndex, Math.min(content.length, urlStartIndex + 200));
|
||||
const fullContext = contextBefore + contextAfter;
|
||||
let match = contextBefore.match(/\[toolkit:([^:]+):([^\]]+)\]\s+Authentication:\s*$/i);
|
||||
if (match) {
|
||||
const toolkitSlug = match[1].trim();
|
||||
const toolkitName = match[2].trim();
|
||||
return { toolkitName, toolkitSlug };
|
||||
}
|
||||
|
||||
// Try to extract toolkit name from various patterns
|
||||
match = contextBefore.match(/([A-Za-z]+(?:\s+[A-Za-z]+)*)\s+Authentication:\s*$/i);
|
||||
if (match) {
|
||||
const serviceName = match[1].trim();
|
||||
const slug = serviceName.toLowerCase().replace(/\s+/g, '_');
|
||||
return { toolkitName: serviceName, toolkitSlug: slug };
|
||||
}
|
||||
|
||||
// Pattern 1: "Successfully created credential profile 'ProfileName' for ToolkitName"
|
||||
let match = fullContext.match(/Successfully created credential profile[^f]*for\s+([^.!?\n]+)/i);
|
||||
match = contextBefore.match(/\d+\.\s*([A-Za-z]+)\s+Authentication(?:\s+\([^)]*\))?\s*:?\s*$/i);
|
||||
if (match) {
|
||||
const serviceName = match[1].trim();
|
||||
const slug = serviceName.toLowerCase().replace(/\s+/g, '_');
|
||||
return { toolkitName: serviceName, toolkitSlug: slug };
|
||||
}
|
||||
|
||||
match = contextBefore.match(/([A-Za-z]+)\s+Authentication\s+\(for[^)]*\)\s*:?\s*$/i);
|
||||
if (match) {
|
||||
const serviceName = match[1].trim();
|
||||
const slug = serviceName.toLowerCase().replace(/\s+/g, '_');
|
||||
return { toolkitName: serviceName, toolkitSlug: slug };
|
||||
}
|
||||
|
||||
match = fullContext.match(/Successfully created credential profile[^f]*for\s+([^.!?\n]+)/i);
|
||||
if (match) {
|
||||
return { toolkitName: match[1].trim(), toolkitSlug: match[1].toLowerCase().replace(/\s+/g, '_') };
|
||||
}
|
||||
|
||||
// Pattern 2: "connect your ToolkitName account"
|
||||
match = fullContext.match(/connect your\s+([^a]+)\s+account/i);
|
||||
if (match) {
|
||||
const name = match[1].trim();
|
||||
return { toolkitName: name, toolkitSlug: name.toLowerCase().replace(/\s+/g, '_') };
|
||||
}
|
||||
|
||||
// Pattern 3: "authorize access to your ToolkitName account"
|
||||
match = fullContext.match(/authorize access to your\s+([^a]+)\s+account/i);
|
||||
if (match) {
|
||||
const name = match[1].trim();
|
||||
return { toolkitName: name, toolkitSlug: name.toLowerCase().replace(/\s+/g, '_') };
|
||||
}
|
||||
|
||||
// Pattern 4: Look for common toolkit names in the context
|
||||
match = fullContext.match(/Sign in to\s+([^.!?\n]+)/i);
|
||||
if (match) {
|
||||
const name = match[1].trim();
|
||||
return { toolkitName: name, toolkitSlug: name.toLowerCase().replace(/\s+/g, '_') };
|
||||
}
|
||||
|
||||
match = contextBefore.match(/([A-Za-z]+)\s+authentication\s*(?:link|url)?:?\s*$/i);
|
||||
if (match) {
|
||||
const serviceName = match[1].trim();
|
||||
const slug = serviceName.toLowerCase().replace(/\s+/g, '_');
|
||||
return { toolkitName: serviceName, toolkitSlug: slug };
|
||||
}
|
||||
|
||||
const commonToolkits = Object.keys(TOOLKIT_NAME_MAPPINGS);
|
||||
for (const toolkit of commonToolkits) {
|
||||
const toolkitName = TOOLKIT_NAME_MAPPINGS[toolkit];
|
||||
|
@ -127,26 +160,37 @@ function extractToolkitInfoFromContext(content: string, urlStartIndex: number):
|
|||
}
|
||||
|
||||
function detectComposioUrls(content: string): ComposioUrl[] {
|
||||
// Detect Composio authentication URLs (these are typically OAuth URLs from various providers)
|
||||
const authUrlPatterns = [
|
||||
// Google OAuth
|
||||
/https:\/\/accounts\.google\.com\/oauth\/authorize\?[^\s)]+/g,
|
||||
// GitHub OAuth
|
||||
/https:\/\/accounts\.google\.com\/o\/oauth2\/[^\s)]+/g,
|
||||
/https:\/\/github\.com\/login\/oauth\/authorize\?[^\s)]+/g,
|
||||
// Generic OAuth pattern for other providers
|
||||
/https:\/\/api\.notion\.com\/v1\/oauth\/authorize\?[^\s)]+/g,
|
||||
/https:\/\/slack\.com\/oauth\/[^\s)]+/g,
|
||||
/https:\/\/[^\/\s]+\.slack\.com\/oauth\/[^\s)]+/g,
|
||||
/https:\/\/login\.microsoftonline\.com\/[^\s)]+/g,
|
||||
/https:\/\/[^\/\s]+\/oauth2?\/authorize\?[^\s)]+/g,
|
||||
// Composio backend URLs
|
||||
/https:\/\/backend\.composio\.dev\/[^\s)]+/g,
|
||||
// Any HTTPS URL that looks like an auth callback
|
||||
/https:\/\/[^\/\s]+\/auth\/[^\s)]+/g,
|
||||
/https:\/\/[^\/\s]+\/authorize\?[^\s)]+/g,
|
||||
/https:\/\/[^\/\s]+\/connect\/[^\s)]+/g,
|
||||
/https:\/\/[^\s)]+[?&](client_id|redirect_uri|response_type|scope)=[^\s)]+/g,
|
||||
];
|
||||
|
||||
const urls: ComposioUrl[] = [];
|
||||
const processedUrls = new Set<string>(); // To avoid duplicates
|
||||
|
||||
for (const pattern of authUrlPatterns) {
|
||||
let match;
|
||||
pattern.lastIndex = 0; // Reset regex state
|
||||
while ((match = pattern.exec(content)) !== null) {
|
||||
const url = match[0];
|
||||
|
||||
// Skip if we've already processed this URL
|
||||
if (processedUrls.has(url)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
processedUrls.add(url);
|
||||
const { toolkitName, toolkitSlug } = extractToolkitInfoFromContext(content, match.index);
|
||||
|
||||
urls.push({
|
||||
|
@ -164,7 +208,8 @@ function detectComposioUrls(content: string): ComposioUrl[] {
|
|||
|
||||
function hasAuthUrlPattern(content: string, url: ComposioUrl): boolean {
|
||||
const beforeUrl = content.substring(Math.max(0, url.startIndex - 100), url.startIndex);
|
||||
return /(?:authentication|auth|connect|visit)\s+(?:url|link):\s*$/i.test(beforeUrl);
|
||||
// Updated pattern to also match [toolkit:slug:name] Authentication: format
|
||||
return /(?:(?:\[toolkit:[^:]+:[^\]]+\]|[A-Za-z]+(?:\s+[A-Za-z]+)*)\s+)?(?:authentication|auth|connect|visit)\s+(?:url|link)?:\s*$/i.test(beforeUrl);
|
||||
}
|
||||
|
||||
interface ComposioConnectButtonProps {
|
||||
|
@ -259,7 +304,12 @@ export const ComposioUrlDetector: React.FC<ComposioUrlDetectorProps> = ({
|
|||
const textBefore = content.substring(lastIndex, composioUrl.startIndex);
|
||||
|
||||
const cleanedTextBefore = hasAuthUrlPattern(content, composioUrl)
|
||||
? textBefore.replace(/(?:authentication|auth|connect|visit)\s+(?:url|link):\s*$/i, '').trim()
|
||||
? textBefore
|
||||
// Remove [toolkit:slug:name] pattern
|
||||
.replace(/\[toolkit:[^:]+:[^\]]+\]\s+/gi, '')
|
||||
// Remove authentication/auth/connect/visit url/link patterns
|
||||
.replace(/(?:authentication|auth|connect|visit)\s+(?:url|link)?:\s*$/i, '')
|
||||
.trim()
|
||||
: textBefore;
|
||||
|
||||
if (cleanedTextBefore.trim()) {
|
||||
|
|
|
@ -173,7 +173,6 @@ export const TaskListToolView: React.FC<ToolViewProps> = ({
|
|||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
<div className="px-4 py-2 h-10 bg-gradient-to-r from-zinc-50/90 to-zinc-100/90 dark:from-zinc-900/90 dark:to-zinc-800/90 backdrop-blur-sm border-t border-zinc-200 dark:border-zinc-800 flex justify-between items-center gap-4">
|
||||
<div className="h-full flex items-center gap-2 text-sm text-zinc-500 dark:text-zinc-400">
|
||||
{!isStreaming && hasData && (
|
||||
|
@ -183,7 +182,7 @@ export const TaskListToolView: React.FC<ToolViewProps> = ({
|
|||
{sections.length} sections
|
||||
</Badge>
|
||||
{completedTasks === totalTasks && totalTasks > 0 && (
|
||||
<Badge variant="outline" className="h-6 py-0.5 bg-green-50 text-green-700 border-green-200">
|
||||
<Badge variant="outline" className="h-6 py-0.5 bg-green-50 dark:bg-green-900/20 text-green-600 border-green-200 dark:border-green-700">
|
||||
<Check className="h-3 w-3" />
|
||||
All complete
|
||||
</Badge>
|
||||
|
@ -191,7 +190,6 @@ export const TaskListToolView: React.FC<ToolViewProps> = ({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{toolTimestamp && !isStreaming
|
||||
? new Date(toolTimestamp).toLocaleTimeString()
|
||||
|
|
|
@ -549,7 +549,7 @@ export function ConditionalGroup({
|
|||
onClick={handleAddStepClick}
|
||||
className="border-dashed text-xs"
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
<Plus className="h-3 w-3" />
|
||||
Add step
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
@ -197,7 +197,7 @@ export function WorkflowSteps({
|
|||
onClick={() => onAddStep(steps.length)}
|
||||
className="h-8 px-4 border border-dashed border-zinc-300 dark:border-zinc-600 hover:border-zinc-400 dark:hover:border-zinc-500 bg-background hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
<Plus className="h-4 w-4" />
|
||||
Add step
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
@ -17,6 +17,7 @@ interface WorkflowBuilderProps {
|
|||
};
|
||||
isLoadingTools?: boolean;
|
||||
agentId?: string;
|
||||
workflowId?: string;
|
||||
versionData?: {
|
||||
version_id: string;
|
||||
configured_mcps: any[];
|
||||
|
@ -29,6 +30,8 @@ interface WorkflowBuilderProps {
|
|||
workflowDescription?: string;
|
||||
onSave?: () => void;
|
||||
isSaving?: boolean;
|
||||
onExecute?: () => void;
|
||||
isExecuting?: boolean;
|
||||
onNameChange?: (name: string) => void;
|
||||
onDescriptionChange?: (description: string) => void;
|
||||
}
|
||||
|
@ -56,6 +59,8 @@ export function WorkflowBuilder({
|
|||
workflowDescription = '',
|
||||
onSave,
|
||||
isSaving = false,
|
||||
onExecute,
|
||||
isExecuting,
|
||||
onNameChange,
|
||||
onDescriptionChange
|
||||
}: WorkflowBuilderProps) {
|
||||
|
@ -135,6 +140,10 @@ export function WorkflowBuilder({
|
|||
onToggleSidePanel={handleToggleSidePanel}
|
||||
onSave={onSave || (() => { })}
|
||||
isSaving={isSaving}
|
||||
onExecute={onExecute}
|
||||
isExecuting={isExecuting}
|
||||
onNameChange={onNameChange}
|
||||
onDescriptionChange={onDescriptionChange}
|
||||
selectedStep={selectedStep}
|
||||
panelMode={panelMode}
|
||||
availableStepTypes={getAvailableStepTypes()}
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { toast } from 'sonner';
|
||||
import { useExecuteWorkflow } from '@/hooks/react-query/agents/use-agent-workflows';
|
||||
import { AgentWorkflow } from '@/hooks/react-query/agents/workflow-utils';
|
||||
import { Play } from 'lucide-react';
|
||||
|
||||
interface WorkflowExecutionDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
workflow: AgentWorkflow | null;
|
||||
agentId: string;
|
||||
onSuccess?: (result: any) => void;
|
||||
onError?: (error: any) => void;
|
||||
}
|
||||
|
||||
export function WorkflowExecutionDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
workflow,
|
||||
agentId,
|
||||
onSuccess,
|
||||
onError
|
||||
}: WorkflowExecutionDialogProps) {
|
||||
const executeWorkflowMutation = useExecuteWorkflow();
|
||||
const [executionInput, setExecutionInput] = useState<string>('');
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
onOpenChange(false);
|
||||
setExecutionInput('');
|
||||
}, [onOpenChange]);
|
||||
|
||||
const handleConfirmExecution = useCallback(async () => {
|
||||
if (!workflow) return;
|
||||
|
||||
try {
|
||||
const result = await executeWorkflowMutation.mutateAsync({
|
||||
agentId,
|
||||
workflowId: workflow.id,
|
||||
execution: {
|
||||
input_data: executionInput.trim() ? { prompt: executionInput } : undefined
|
||||
}
|
||||
});
|
||||
|
||||
handleClose();
|
||||
toast.success(`${result.message}`);
|
||||
onSuccess?.(result);
|
||||
} catch (error) {
|
||||
toast.error('Failed to execute workflow');
|
||||
onError?.(error);
|
||||
}
|
||||
}, [agentId, workflow, executionInput, executeWorkflowMutation, handleClose, onSuccess, onError]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Execute Workflow</DialogTitle>
|
||||
<DialogDescription>
|
||||
Provide input data for "{workflow?.name}" workflow
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>What would you like the workflow to work on?</Label>
|
||||
<Textarea
|
||||
value={executionInput}
|
||||
onChange={(e) => setExecutionInput(e.target.value)}
|
||||
placeholder="Enter your request..."
|
||||
rows={4}
|
||||
className="resize-none"
|
||||
required={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-2 pt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleConfirmExecution}
|
||||
disabled={executeWorkflowMutation.isPending}
|
||||
>
|
||||
<Play className="h-3.5 w-3.5" />
|
||||
{executeWorkflowMutation.isPending ? 'Executing...' : 'Execute Workflow'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { ArrowLeft, Save, Settings, GitBranch } from 'lucide-react';
|
||||
import { ArrowLeft, Save, Settings, GitBranch, Play } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
|
@ -14,7 +14,9 @@ interface WorkflowHeaderProps {
|
|||
workflowDescription?: string;
|
||||
onToggleSidePanel: () => void;
|
||||
onSave: () => void;
|
||||
onExecute?: () => void;
|
||||
isSaving?: boolean;
|
||||
isExecuting?: boolean;
|
||||
onNameChange?: (name: string) => void;
|
||||
onDescriptionChange?: (description: string) => void;
|
||||
}
|
||||
|
@ -24,7 +26,9 @@ export function WorkflowHeader({
|
|||
workflowDescription,
|
||||
onToggleSidePanel,
|
||||
onSave,
|
||||
onExecute,
|
||||
isSaving = false,
|
||||
isExecuting = false,
|
||||
onNameChange,
|
||||
onDescriptionChange
|
||||
}: WorkflowHeaderProps) {
|
||||
|
@ -109,6 +113,17 @@ export function WorkflowHeader({
|
|||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{onExecute && (
|
||||
<Button
|
||||
onClick={onExecute}
|
||||
disabled={isExecuting || isSaving}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
<Play className="h-3.5 w-3.5" />
|
||||
{isExecuting ? 'Executing...' : 'Execute'}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={onSave}
|
||||
disabled={isSaving}
|
||||
|
|
|
@ -13,6 +13,10 @@ interface WorkflowLayoutProps {
|
|||
onToggleSidePanel: () => void;
|
||||
onSave: () => void;
|
||||
isSaving?: boolean;
|
||||
onExecute?: () => void;
|
||||
isExecuting?: boolean;
|
||||
onNameChange?: (name: string) => void;
|
||||
onDescriptionChange?: (description: string) => void;
|
||||
selectedStep?: ConditionalStep | null;
|
||||
panelMode?: 'add' | 'edit';
|
||||
availableStepTypes?: any[];
|
||||
|
@ -36,6 +40,10 @@ export function WorkflowLayout({
|
|||
onToggleSidePanel,
|
||||
onSave,
|
||||
isSaving = false,
|
||||
onExecute,
|
||||
isExecuting = false,
|
||||
onNameChange,
|
||||
onDescriptionChange,
|
||||
selectedStep,
|
||||
panelMode = 'add',
|
||||
availableStepTypes = [],
|
||||
|
@ -64,6 +72,10 @@ export function WorkflowLayout({
|
|||
onToggleSidePanel={onToggleSidePanel}
|
||||
onSave={onSave}
|
||||
isSaving={isSaving}
|
||||
onExecute={onExecute}
|
||||
isExecuting={isExecuting}
|
||||
onNameChange={onNameChange}
|
||||
onDescriptionChange={onDescriptionChange}
|
||||
/>
|
||||
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden">
|
||||
|
|
|
@ -361,7 +361,7 @@ export function WorkflowSidePanel({
|
|||
}}
|
||||
className="w-full"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
<Trash2 className="h-4 w-4" />
|
||||
{selectedStep.conditions?.type === 'if' ? 'Delete Conditional Group' : 'Delete Step'}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue