import json
from typing import Optional, Dict, Any, List
from agentpress.tool import ToolResult, openapi_schema, usage_example
from agentpress.thread_manager import ThreadManager
from .base_tool import AgentBuilderBaseTool
from utils.logger import logger
from agent.config_helper import extract_agent_config
class WorkflowTool(AgentBuilderBaseTool):
def __init__(self, thread_manager: ThreadManager, db_connection, agent_id: str):
super().__init__(thread_manager, db_connection, agent_id)
async def _sync_workflows_to_version_config(self) -> None:
try:
client = await self.db.client
agent_result = await client.table('agents').select('current_version_id').eq('agent_id', self.agent_id).single().execute()
if not agent_result.data or not agent_result.data.get('current_version_id'):
logger.warning(f"No current version found for agent {self.agent_id}")
return
current_version_id = agent_result.data['current_version_id']
workflows_result = await client.table('agent_workflows').select('*').eq('agent_id', self.agent_id).execute()
workflows = workflows_result.data if workflows_result.data else []
triggers_result = await client.table('agent_triggers').select('*').eq('agent_id', self.agent_id).execute()
triggers = []
if triggers_result.data:
import json
for trigger in triggers_result.data:
trigger_copy = trigger.copy()
if 'config' in trigger_copy and isinstance(trigger_copy['config'], str):
try:
trigger_copy['config'] = json.loads(trigger_copy['config'])
except json.JSONDecodeError:
logger.warning(f"Failed to parse trigger config for {trigger_copy.get('trigger_id')}")
trigger_copy['config'] = {}
triggers.append(trigger_copy)
version_result = await client.table('agent_versions').select('config').eq('version_id', current_version_id).single().execute()
if not version_result.data:
logger.warning(f"Version {current_version_id} not found")
return
config = version_result.data.get('config', {})
config['workflows'] = workflows
config['triggers'] = triggers
await client.table('agent_versions').update({'config': config}).eq('version_id', current_version_id).execute()
logger.debug(f"Synced {len(workflows)} workflows and {len(triggers)} triggers to version config for agent {self.agent_id}")
except Exception as e:
logger.error(f"Failed to sync workflows to version config: {e}")
async def _get_available_tools_for_agent(self) -> List[str]:
try:
client = await self.db.client
agent_result = await client.table('agents').select('*').eq('agent_id', self.agent_id).execute()
if not agent_result.data:
return []
agent_data = agent_result.data[0]
version_data = None
if agent_data.get('current_version_id'):
try:
from agent.versioning.version_service import get_version_service
account_id = await self._get_current_account_id()
version_service = await get_version_service()
version_obj = await version_service.get_version(
agent_id=self.agent_id,
version_id=agent_data['current_version_id'],
user_id=account_id
)
version_data = version_obj.to_dict()
except Exception as e:
logger.warning(f"Failed to get version data for workflow tool: {e}")
agent_config = extract_agent_config(agent_data, version_data)
available_tools = []
tool_mapping = {
'sb_shell_tool': ['execute_command'],
'sb_files_tool': ['create_file', 'edit_file', 'str_replace', 'full_file_rewrite', 'delete_file'],
'browser_tool': ['browser_navigate_to', 'browser_screenshot'],
'sb_vision_tool': ['see_image'],
'sb_deploy_tool': ['deploy'],
'sb_expose_tool': ['expose_port'],
'web_search_tool': ['web_search'],
'data_providers_tool': ['get_data_provider_endpoints', 'execute_data_provider_call']
}
agentpress_tools = agent_config.get('agentpress_tools', {})
for tool_key, tool_names in tool_mapping.items():
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:
enabled_tools = mcp.get('enabledTools', mcp.get('enabled_tools', []))
available_tools.extend(enabled_tools)
custom_mcps = agent_config.get('custom_mcps', [])
for mcp in custom_mcps:
enabled_tools = mcp.get('enabledTools', mcp.get('enabled_tools', []))
available_tools.extend(enabled_tools)
seen = set()
unique_tools = []
for tool in available_tools:
if tool not in seen:
seen.add(tool)
unique_tools.append(tool)
return unique_tools
except Exception as e:
logger.error(f"Error getting available tools for agent {self.agent_id}: {e}")
return []
def _validate_tool_steps(self, steps: List[Dict[str, Any]], available_tools: List[str]) -> List[str]:
errors = []
def validate_step_list(step_list: List[Dict[str, Any]], path: str = ""):
for i, step in enumerate(step_list):
current_path = f"{path}step[{i}]" if path else f"step[{i}]"
if step.get('type') == 'tool':
tool_name = step.get('config', {}).get('tool_name')
if tool_name and tool_name not in available_tools:
errors.append(f"{current_path}: Tool '{tool_name}' is not available for this agent")
if step.get('children'):
validate_step_list(step['children'], f"{current_path}.children.")
validate_step_list(steps)
return errors
@openapi_schema({
"type": "function",
"function": {
"name": "create_workflow",
"description": "Create a new workflow/playbook. This stores a Start node with a single child that contains config.playbook { template, variables }.",
"parameters": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of the workflow"
},
"template": {
"type": "string",
"description": "Workflow/playbook instructions text. Use {{variable}} tokens."
},
"variables": {
"type": "array",
"description": "Optional variable specs; all treated as strings currently",
"items": {
"type": "object",
"properties": {
"key": {"type": "string"},
"label": {"type": "string"},
"required": {"type": "boolean", "default": True}
},
"required": ["key"]
},
"default": []
},
"description": {
"type": "string",
"description": "Optional short description (auto-summarized from template if omitted)"
},
"is_default": {
"type": "boolean",
"description": "Whether this should be the default workflow",
"default": False
}
},
"required": ["name", "template"]
}
}
})
@usage_example('''
Company Research Playbook
Research companies and update sheets using {{sheet_id}} and {{limit}}
[{"key":"sheet_id","label":"Sheet ID","required":true},{"key":"limit","label":"Row Limit","required":false}]
''')
async def create_workflow(
self,
name: str,
template: str,
variables: Optional[List[Dict[str, Any]]] = None,
description: Optional[str] = None,
is_default: bool = False,
) -> ToolResult:
try:
client = await self.db.client
variables = variables or []
playbook_step = {
'id': 'workflow-exec',
'name': 'Execute Workflow Template',
'description': 'Execute the workflow/playbook template using provided variables.',
'type': 'instruction',
'config': {
'playbook': {
'template': template,
'variables': variables,
}
},
'order': 1,
'children': []
}
start_node = {
'id': 'start-node',
'name': 'Start',
'description': 'Click to add steps or use the Add Node button',
'type': 'instruction',
'config': {},
'order': 0,
'children': [playbook_step]
}
steps_json = self._convert_steps_to_json([start_node])
def _summarize(text: str) -> str:
s = (text or '').strip().replace('\n', ' ')
return s[:160] + ('…' if len(s) > 160 else '')
workflow_data = {
'agent_id': self.agent_id,
'name': name,
'description': description if description is not None else _summarize(template),
'trigger_phrase': None,
'is_default': is_default,
'status': 'draft',
'steps': steps_json,
}
result = await client.table('agent_workflows').insert(workflow_data).execute()
if not result.data:
return self.fail_response("Failed to create workflow")
await self._sync_workflows_to_version_config()
workflow = result.data[0]
return self.success_response({
"message": f"Workflow '{name}' created successfully",
"workflow": {
"id": workflow["id"],
"name": workflow["name"],
"description": workflow.get("description"),
"is_default": workflow["is_default"],
"status": workflow["status"],
"steps_count": len(steps_json),
"created_at": workflow["created_at"],
}
})
except Exception as e:
return self.fail_response(f"Error creating workflow: {str(e)}")
@openapi_schema({
"type": "function",
"function": {
"name": "get_workflows",
"description": "Get all workflows/playbooks for the current agent. Use this to see what workflows are already configured.",
"parameters": {
"type": "object",
"properties": {
"include_steps": {
"type": "boolean",
"description": "Whether to include detailed step information for each workflow",
"default": True
}
},
"required": []
}
}
})
@usage_example('''
true
''')
async def get_workflows(self, include_steps: bool = True) -> ToolResult:
try:
client = await self.db.client
result = await client.table('agent_workflows').select('*').eq('agent_id', self.agent_id).order('created_at', desc=True).execute()
workflows = []
for workflow_data in result.data:
workflow_info = {
"id": workflow_data["id"],
"name": workflow_data["name"],
"description": workflow_data.get("description"),
"trigger_phrase": workflow_data.get("trigger_phrase"),
"is_default": workflow_data["is_default"],
"status": workflow_data["status"],
"created_at": workflow_data["created_at"],
"updated_at": workflow_data["updated_at"]
}
if include_steps:
steps_json = workflow_data.get("steps", [])
workflow_info["steps"] = steps_json
workflow_info["steps_count"] = len(steps_json)
else:
workflow_info["steps_count"] = len(workflow_data.get("steps", []))
workflows.append(workflow_info)
return self.success_response({
"message": f"Found {len(workflows)} workflows for agent",
"workflows": workflows
})
except Exception as e:
return self.fail_response(f"Error getting workflows: {str(e)}")
@openapi_schema({
"type": "function",
"function": {
"name": "update_workflow",
"description": "Update an existing workflow or playbook. For playbooks, the first child under Start should contain config.playbook { template, variables }.",
"parameters": {
"type": "object",
"properties": {
"workflow_id": {
"type": "string",
"description": "ID of the workflow to update"
},
"name": {
"type": "string",
"description": "New name for the workflow"
},
"description": {
"type": "string",
"description": "New description for the workflow"
},
"trigger_phrase": {
"type": "string",
"description": "New trigger phrase for the workflow"
},
"is_default": {
"type": "boolean",
"description": "Whether this workflow should be the default workflow"
},
"status": {
"type": "string",
"enum": ["draft", "active", "inactive"],
"description": "Status of the workflow"
},
"validate_tools": {
"type": "boolean",
"description": "Whether to validate tool names against available tools when updating steps",
"default": True
},
"steps": {
"type": "array",
"description": "New steps. For playbooks, follow the Start + Execute Playbook structure with config.playbook.",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"description": {"type": "string"},
"type": {
"type": "string",
"enum": ["instruction", "tool", "condition"],
"default": "instruction"
},
"config": {"type": "object", "additionalProperties": True},
"conditions": {"type": "object", "additionalProperties": True},
"order": {"type": "integer"},
"children": {"type": "array", "items": {"$ref": "#"}}
},
"required": ["name", "order"]
}
}
},
"required": ["workflow_id"]
}
}
})
@usage_example('''
workflow-123
Updated Research Workflow
active
''')
async def update_workflow(
self,
workflow_id: str,
name: Optional[str] = None,
description: Optional[str] = None,
trigger_phrase: Optional[str] = None,
is_default: Optional[bool] = None,
status: Optional[str] = None,
steps: Optional[List[Dict[str, Any]]] = None,
validate_tools: bool = True
) -> ToolResult:
try:
client = await self.db.client
workflow_result = await client.table('agent_workflows').select('*').eq('id', workflow_id).eq('agent_id', self.agent_id).execute()
if not workflow_result.data:
return self.fail_response("Workflow not found or doesn't belong to this agent")
update_data = {}
if name is not None:
update_data['name'] = name
if description is not None:
update_data['description'] = description
if trigger_phrase is not None:
update_data['trigger_phrase'] = trigger_phrase
if is_default is not None:
update_data['is_default'] = is_default
if status is not None:
if status not in ['draft', 'active', 'inactive']:
return self.fail_response("Status must be 'draft', 'active', or 'inactive'")
update_data['status'] = status
if steps is not None:
if not isinstance(steps, list):
return self.fail_response("Steps must be a list")
if validate_tools:
available_tools = await self._get_available_tools_for_agent()
validation_errors = self._validate_tool_steps(steps, available_tools)
if validation_errors:
return self.fail_response(f"Tool validation failed:\n" + "\n".join(validation_errors))
update_data['steps'] = self._convert_steps_to_json(steps)
if not update_data:
return self.fail_response("No fields provided to update")
result = await client.table('agent_workflows').update(update_data).eq('id', workflow_id).execute()
if not result.data:
return self.fail_response("Failed to update workflow")
await self._sync_workflows_to_version_config()
workflow = result.data[0]
return self.success_response({
"message": f"Workflow '{workflow['name']}' updated successfully",
"updated_fields": list(update_data.keys()),
"workflow": {
"id": workflow["id"],
"name": workflow["name"],
"description": workflow.get("description"),
"trigger_phrase": workflow.get("trigger_phrase"),
"is_default": workflow["is_default"],
"status": workflow["status"],
"steps_count": len(workflow.get("steps", [])),
"updated_at": workflow["updated_at"]
}
})
except Exception as e:
return self.fail_response(f"Error updating workflow: {str(e)}")
@openapi_schema({
"type": "function",
"function": {
"name": "delete_workflow",
"description": "Delete a workflow/playbook from the agent. This action cannot be undone.",
"parameters": {
"type": "object",
"properties": {
"workflow_id": {
"type": "string",
"description": "ID of the workflow to delete"
}
},
"required": ["workflow_id"]
}
}
})
@usage_example('''
workflow-123
''')
async def delete_workflow(self, workflow_id: str) -> ToolResult:
try:
client = await self.db.client
workflow_result = await client.table('agent_workflows').select('*').eq('id', workflow_id).eq('agent_id', self.agent_id).execute()
if not workflow_result.data:
return self.fail_response("Workflow not found or doesn't belong to this agent")
workflow_name = workflow_result.data[0]['name']
result = await client.table('agent_workflows').delete().eq('id', workflow_id).execute()
await self._sync_workflows_to_version_config()
return self.success_response({
"message": f"Workflow '{workflow_name}' deleted successfully",
"workflow_id": workflow_id
})
except Exception as e:
return self.fail_response(f"Error deleting workflow: {str(e)}")
@openapi_schema({
"type": "function",
"function": {
"name": "activate_workflow",
"description": "Activate or deactivate a workflow/playbook. Only active workflows can be executed.",
"parameters": {
"type": "object",
"properties": {
"workflow_id": {
"type": "string",
"description": "ID of the workflow to activate/deactivate"
},
"active": {
"type": "boolean",
"description": "Whether to activate (true) or deactivate (false) the workflow",
"default": True
}
},
"required": ["workflow_id"]
}
}
})
@usage_example('''
workflow-123
true
''')
async def activate_workflow(self, workflow_id: str, active: bool = True) -> ToolResult:
try:
client = await self.db.client
workflow_result = await client.table('agent_workflows').select('*').eq('id', workflow_id).eq('agent_id', self.agent_id).execute()
if not workflow_result.data:
return self.fail_response("Workflow not found or doesn't belong to this agent")
workflow_name = workflow_result.data[0]['name']
new_status = 'active' if active else 'inactive'
result = await client.table('agent_workflows').update({'status': new_status}).eq('id', workflow_id).execute()
if not result.data:
return self.fail_response("Failed to update workflow status")
await self._sync_workflows_to_version_config()
action = "activated" if active else "deactivated"
return self.success_response({
"message": f"Workflow '{workflow_name}' {action} successfully",
"workflow_id": workflow_id,
"status": new_status
})
except Exception as e:
return self.fail_response(f"Error updating workflow status: {str(e)}")
def _convert_steps_to_json(self, steps: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
if not steps:
return []
result = []
for step in steps:
step_dict = {
'name': step.get('name', ''),
'description': step.get('description'),
'type': step.get('type', 'instruction'),
'config': step.get('config', {}),
'conditions': step.get('conditions'),
'order': step.get('order', 0)
}
if 'id' in step and step.get('id'):
step_dict['id'] = step['id']
if 'parentConditionalId' in step and step.get('parentConditionalId'):
step_dict['parentConditionalId'] = step['parentConditionalId']
if step.get('children'):
step_dict['children'] = self._convert_steps_to_json(step['children'])
result.append(step_dict)
return result