diff --git a/backend/agent/api.py b/backend/agent/api.py index a18ee5ce..7ff02510 100644 --- a/backend/agent/api.py +++ b/backend/agent/api.py @@ -25,6 +25,7 @@ from utils.constants import MODEL_NAME_ALIASES from flags.flags import is_enabled from .utils import check_for_active_project_agent_run, stop_agent_run as _stop_agent_run +from .config_helper import extract_agent_config, build_unified_config, extract_tools_for_agent_run, get_mcp_configs # Initialize shared resources router = APIRouter() @@ -174,7 +175,6 @@ async def cleanup(): logger.info("Completed cleanup of agent API resources") async def get_agent_run_with_access_check(client, agent_run_id: str, user_id: str): - """Get agent run data after verifying user access.""" agent_run = await client.table('agent_runs').select('*').eq('id', agent_run_id).execute() if not agent_run.data: raise HTTPException(status_code=404, detail="Agent run not found") @@ -255,56 +255,27 @@ async def start_agent( effective_agent_id = None else: agent_data = agent_result.data[0] - # Use version data if available, otherwise fall back to agent data (for backward compatibility) - if agent_data.get('agent_versions'): - version_data = agent_data['agent_versions'] - agent_config = { - 'agent_id': agent_data['agent_id'], - 'name': agent_data['name'], - 'description': agent_data.get('description'), - 'system_prompt': version_data['system_prompt'], - 'configured_mcps': version_data.get('configured_mcps', []), - 'custom_mcps': version_data.get('custom_mcps', []), - 'agentpress_tools': version_data.get('agentpress_tools', {}), - 'is_default': agent_data.get('is_default', False), - 'current_version_id': agent_data.get('current_version_id'), - 'version_name': version_data.get('version_name', 'v1') - } - logger.info(f"Using agent {agent_config['name']} ({effective_agent_id}) version {agent_config['version_name']}") + version_data = agent_data.get('agent_versions') + agent_config = extract_agent_config(agent_data, version_data) + + if version_data: + logger.info(f"Using agent {agent_config['name']} ({effective_agent_id}) version {agent_config.get('version_name', 'v1')}") else: - # Backward compatibility - use agent data directly - agent_config = agent_data logger.info(f"Using agent {agent_config['name']} ({effective_agent_id}) - no version data") source = "request" if body.agent_id else "thread" - # If no agent found yet, try to get default agent for the account if not agent_config: default_agent_result = await client.table('agents').select('*, agent_versions!current_version_id(*)').eq('account_id', account_id).eq('is_default', True).execute() if default_agent_result.data: agent_data = default_agent_result.data[0] - # Use version data if available - if agent_data.get('agent_versions'): - version_data = agent_data['agent_versions'] - agent_config = { - 'agent_id': agent_data['agent_id'], - 'name': agent_data['name'], - 'description': agent_data.get('description'), - 'system_prompt': version_data['system_prompt'], - 'configured_mcps': version_data.get('configured_mcps', []), - 'custom_mcps': version_data.get('custom_mcps', []), - 'agentpress_tools': version_data.get('agentpress_tools', {}), - 'is_default': agent_data.get('is_default', False), - 'current_version_id': agent_data.get('current_version_id'), - 'version_name': version_data.get('version_name', 'v1') - } - logger.info(f"Using default agent: {agent_config['name']} ({agent_config['agent_id']}) version {agent_config['version_name']}") + version_data = agent_data.get('agent_versions') + agent_config = extract_agent_config(agent_data, version_data) + + if version_data: + logger.info(f"Using default agent: {agent_config['name']} ({agent_config['agent_id']}) version {agent_config.get('version_name', 'v1')}") else: - agent_config = agent_data logger.info(f"Using default agent: {agent_config['name']} ({agent_config['agent_id']}) - no version data") - - # Don't update thread's agent_id since threads are now agent-agnostic - # The agent selection is handled per message/agent run if body.agent_id and body.agent_id != thread_agent_id and agent_config: logger.info(f"Using agent {agent_config['agent_id']} for this agent run (thread remains agent-agnostic)") @@ -1321,11 +1292,23 @@ async def create_agent( if agent_data.is_default: await client.table('agents').update({"is_default": False}).eq("account_id", user_id).eq("is_default", True).execute() + # Build unified config + unified_config = build_unified_config( + system_prompt=agent_data.system_prompt, + agentpress_tools=agent_data.agentpress_tools or {}, + configured_mcps=agent_data.configured_mcps or [], + custom_mcps=agent_data.custom_mcps or [], + avatar=agent_data.avatar, + avatar_color=agent_data.avatar_color + ) + # Create the agent insert_data = { "account_id": user_id, "name": agent_data.name, "description": agent_data.description, + "config": unified_config, + # Keep legacy columns for backward compatibility "system_prompt": agent_data.system_prompt, "configured_mcps": agent_data.configured_mcps or [], "custom_mcps": agent_data.custom_mcps or [], @@ -1348,6 +1331,8 @@ async def create_agent( "agent_id": agent['agent_id'], "version_number": 1, "version_name": "v1", + "config": unified_config, + # Keep legacy columns for backward compatibility "system_prompt": agent_data.system_prompt, "configured_mcps": agent_data.configured_mcps or [], "custom_mcps": agent_data.custom_mcps or [], @@ -1364,16 +1349,7 @@ async def create_agent( await client.table('agents').update({ "current_version_id": version['version_id'] }).eq("agent_id", agent['agent_id']).execute() - - # Add version history entry - await client.table('agent_version_history').insert({ - "agent_id": agent['agent_id'], - "version_id": version['version_id'], - "action": "created", - "changed_by": user_id, - "change_description": "Initial version v1 created" - }).execute() - + agent['current_version_id'] = version['version_id'] agent['current_version'] = version @@ -1520,6 +1496,24 @@ async def update_agent( if agent_data.avatar_color is not None: update_data["avatar_color"] = agent_data.avatar_color + # Build unified config with all current values + current_system_prompt = agent_data.system_prompt if agent_data.system_prompt is not None else current_version_data.get('system_prompt', '') + current_configured_mcps = agent_data.configured_mcps if agent_data.configured_mcps is not None else current_version_data.get('configured_mcps', []) + current_custom_mcps = agent_data.custom_mcps if agent_data.custom_mcps is not None else current_version_data.get('custom_mcps', []) + current_agentpress_tools = agent_data.agentpress_tools if agent_data.agentpress_tools is not None else current_version_data.get('agentpress_tools', {}) + current_avatar = agent_data.avatar if agent_data.avatar is not None else existing_data.get('avatar') + current_avatar_color = agent_data.avatar_color if agent_data.avatar_color is not None else existing_data.get('avatar_color') + + unified_config = build_unified_config( + system_prompt=current_system_prompt, + agentpress_tools=current_agentpress_tools, + configured_mcps=current_configured_mcps, + custom_mcps=current_custom_mcps, + avatar=current_avatar, + avatar_color=current_avatar_color + ) + update_data["config"] = unified_config + # Also update the agent table with the latest values (for backward compatibility) if agent_data.system_prompt is not None: update_data["system_prompt"] = agent_data.system_prompt @@ -1553,6 +1547,17 @@ async def update_agent( "created_by": user_id } + # Build unified config for the new version + version_unified_config = build_unified_config( + system_prompt=new_version_data["system_prompt"], + agentpress_tools=new_version_data["agentpress_tools"], + configured_mcps=new_version_data["configured_mcps"], + custom_mcps=new_version_data["custom_mcps"], + avatar=None, # Avatar is not versioned + avatar_color=None # Avatar color is not versioned + ) + new_version_data["config"] = version_unified_config + # Validate system prompt is not empty if not new_version_data["system_prompt"] or new_version_data["system_prompt"].strip() == '': raise HTTPException(status_code=400, detail="System prompt cannot be empty") @@ -1566,17 +1571,17 @@ async def update_agent( update_data['current_version_id'] = new_version_id update_data['version_count'] = next_version_number - # Add version history entry - try: - await client.table('agent_version_history').insert({ - "agent_id": agent_id, - "version_id": new_version_id, - "action": "created", - "changed_by": user_id, - "change_description": f"New version v{next_version_number} created from update" - }).execute() - except Exception as e: - logger.warning(f"Failed to create version history entry: {e}") + # Don't add version history entry - table was dropped in migration + # try: + # await client.table('agent_version_history').insert({ + # "agent_id": agent_id, + # "version_id": new_version_id, + # "action": "created", + # "changed_by": user_id, + # "change_description": f"New version v{next_version_number} created from update" + # }).execute() + # except Exception as e: + # logger.warning(f"Failed to create version history entry: {e}") logger.info(f"Created new version v{next_version_number} for agent {agent_id}") @@ -1808,6 +1813,17 @@ async def create_agent_version( "created_by": user_id } + # Build unified config for the new version + version_unified_config = build_unified_config( + system_prompt=new_version_data["system_prompt"], + agentpress_tools=new_version_data["agentpress_tools"], + configured_mcps=new_version_data["configured_mcps"], + custom_mcps=new_version_data["custom_mcps"], + avatar=None, # Avatar is not versioned + avatar_color=None # Avatar color is not versioned + ) + new_version_data["config"] = version_unified_config + new_version = await client.table('agent_versions').insert(new_version_data).execute() if not new_version.data: @@ -1821,14 +1837,14 @@ async def create_agent_version( "version_count": next_version_number }).eq("agent_id", agent_id).execute() - # Add version history entry - await client.table('agent_version_history').insert({ - "agent_id": agent_id, - "version_id": version['version_id'], - "action": "created", - "changed_by": user_id, - "change_description": f"New version v{next_version_number} created" - }).execute() + # Don't add version history entry - table was dropped in migration + # await client.table('agent_version_history').insert({ + # "agent_id": agent_id, + # "version_id": version['version_id'], + # "action": "created", + # "changed_by": user_id, + # "change_description": f"New version v{next_version_number} created" + # }).execute() logger.info(f"Created version v{next_version_number} for agent {agent_id}") @@ -1858,14 +1874,14 @@ async def activate_agent_version( "current_version_id": version_id }).eq("agent_id", agent_id).execute() - # Add version history entry - await client.table('agent_version_history').insert({ - "agent_id": agent_id, - "version_id": version_id, - "action": "activated", - "changed_by": user_id, - "change_description": f"Switched to version {version_result.data[0]['version_name']}" - }).execute() + # Don't add version history entry - table was dropped in migration + # await client.table('agent_version_history').insert({ + # "agent_id": agent_id, + # "version_id": version_id, + # "action": "activated", + # "changed_by": user_id, + # "change_description": f"Switched to version {version_result.data[0]['version_name']}" + # }).execute() return {"message": "Version activated successfully"} diff --git a/backend/agent/config_helper.py b/backend/agent/config_helper.py new file mode 100644 index 00000000..52f30024 --- /dev/null +++ b/backend/agent/config_helper.py @@ -0,0 +1,165 @@ +from typing import Dict, Any, Optional, List +from utils.logger import logger + + +def extract_agent_config(agent_data: Dict[str, Any], version_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + if agent_data.get('config') and agent_data['config'] != {}: + config = agent_data['config'].copy() + if 'tools' not in config: + config['tools'] = { + 'agentpress': {}, + 'mcp': [], + 'custom_mcp': [] + } + if 'metadata' not in config: + config['metadata'] = {} + + config['agent_id'] = agent_data['agent_id'] + config['name'] = agent_data['name'] + config['description'] = agent_data.get('description') + config['is_default'] = agent_data.get('is_default', False) + config['account_id'] = agent_data.get('account_id') + config['current_version_id'] = agent_data.get('current_version_id') + + metadata = config.get('metadata', {}) + config['avatar'] = metadata.get('avatar') + config['avatar_color'] = metadata.get('avatar_color') + + config['agentpress_tools'] = extract_tools_for_agent_run(config) + + config['configured_mcps'] = config.get('tools', {}).get('mcp', []) + config['custom_mcps'] = config.get('tools', {}).get('custom_mcp', []) + + return config + + if version_data and version_data.get('config') and version_data['config'] != {}: + config = version_data['config'].copy() + + config['agent_id'] = agent_data['agent_id'] + config['name'] = agent_data['name'] + config['description'] = agent_data.get('description') + config['is_default'] = agent_data.get('is_default', False) + config['account_id'] = agent_data.get('account_id') + config['current_version_id'] = agent_data.get('current_version_id') + config['version_name'] = version_data.get('version_name', 'v1') + + metadata = config.get('metadata', {}) + config['avatar'] = metadata.get('avatar', agent_data.get('avatar')) + config['avatar_color'] = metadata.get('avatar_color', agent_data.get('avatar_color')) + + # Convert agentpress tools to legacy format for run_agent + config['agentpress_tools'] = extract_tools_for_agent_run(config) + + # Also extract MCP configs + config['configured_mcps'] = config.get('tools', {}).get('mcp', []) + config['custom_mcps'] = config.get('tools', {}).get('custom_mcp', []) + + return config + + logger.info(f"Building config from legacy columns for agent {agent_data.get('agent_id')}") + source_data = version_data if version_data else agent_data + + legacy_tools = source_data.get('agentpress_tools', {}) + simplified_tools = {} + + for tool_name, tool_config in legacy_tools.items(): + if isinstance(tool_config, dict): + simplified_tools[tool_name] = tool_config.get('enabled', False) + elif isinstance(tool_config, bool): + simplified_tools[tool_name] = tool_config + + config = { + 'agent_id': agent_data['agent_id'], + 'name': agent_data['name'], + 'description': agent_data.get('description'), + 'system_prompt': source_data.get('system_prompt', ''), + 'tools': { + 'agentpress': simplified_tools, + 'mcp': source_data.get('configured_mcps', []), + 'custom_mcp': source_data.get('custom_mcps', []) + }, + 'metadata': { + 'avatar': agent_data.get('avatar'), + 'avatar_color': agent_data.get('avatar_color') + }, + 'is_default': agent_data.get('is_default', False), + 'account_id': agent_data.get('account_id'), + 'current_version_id': agent_data.get('current_version_id'), + 'avatar': agent_data.get('avatar'), + 'avatar_color': agent_data.get('avatar_color') + } + + if version_data: + config['version_name'] = version_data.get('version_name', 'v1') + + config['configured_mcps'] = source_data.get('configured_mcps', []) + config['custom_mcps'] = source_data.get('custom_mcps', []) + config['agentpress_tools'] = legacy_tools + + return config + + +def build_unified_config( + system_prompt: str, + agentpress_tools: Dict[str, Any], + configured_mcps: List[Dict[str, Any]], + custom_mcps: Optional[List[Dict[str, Any]]] = None, + avatar: Optional[str] = None, + avatar_color: Optional[str] = None +) -> Dict[str, Any]: + simplified_tools = {} + for tool_name, tool_config in agentpress_tools.items(): + if isinstance(tool_config, dict): + simplified_tools[tool_name] = tool_config.get('enabled', False) + elif isinstance(tool_config, bool): + simplified_tools[tool_name] = tool_config + return { + 'system_prompt': system_prompt, + 'tools': { + 'agentpress': simplified_tools, + 'mcp': configured_mcps or [], + 'custom_mcp': custom_mcps or [] + }, + 'metadata': { + 'avatar': avatar, + 'avatar_color': avatar_color + } + } + + +def extract_tools_for_agent_run(config: Dict[str, Any]) -> Dict[str, Any]: + tools = config.get('tools', {}) + agentpress = tools.get('agentpress', {}) + + legacy_format = {} + + for tool_name, enabled in agentpress.items(): + if isinstance(enabled, bool): + legacy_format[tool_name] = { + 'enabled': enabled, + 'description': '' + } + elif isinstance(enabled, dict): + legacy_format[tool_name] = enabled + + return legacy_format + + +def get_mcp_configs(config: Dict[str, Any]) -> List[Dict[str, Any]]: + tools = config.get('tools', {}) + all_mcps = [] + mcp_list = tools.get('mcp', []) + if mcp_list: + all_mcps.extend(mcp_list) + + custom_mcp_list = tools.get('custom_mcp', []) + if custom_mcp_list: + all_mcps.extend(custom_mcp_list) + + if 'configured_mcps' in config: + all_mcps.extend(config['configured_mcps']) + + if 'custom_mcps' in config: + all_mcps.extend(config['custom_mcps']) + + return all_mcps \ No newline at end of file diff --git a/backend/agent/workflows.py b/backend/agent/workflows.py index 481a83a7..2f88137b 100644 --- a/backend/agent/workflows.py +++ b/backend/agent/workflows.py @@ -24,6 +24,7 @@ from run_agent_background import run_agent_background, _cleanup_redis_response_l from utils.constants import MODEL_NAME_ALIASES from flags.flags import is_enabled from .utils import check_for_active_project_agent_run, stop_agent_run as _stop_agent_run +from .config_helper import extract_agent_config router = APIRouter() db = None @@ -456,24 +457,12 @@ async def execute_agent_workflow( agent_data = agent_result.data[0] account_id = agent_data['account_id'] - if agent_data.get('agent_versions'): - version_data = agent_data['agent_versions'] - agent_config = { - 'agent_id': agent_data['agent_id'], - 'name': agent_data['name'], - 'description': agent_data.get('description'), - 'system_prompt': version_data['system_prompt'], - 'configured_mcps': version_data.get('configured_mcps', []), - 'custom_mcps': version_data.get('custom_mcps', []), - 'agentpress_tools': version_data.get('agentpress_tools', {}), - 'is_default': agent_data.get('is_default', False), - 'current_version_id': agent_data.get('current_version_id'), - 'version_name': version_data.get('version_name', 'v1') - } - logger.info(f"Using agent {agent_config['name']} ({agent_id}) version {agent_config['version_name']} for workflow") + version_data = agent_data.get('agent_versions') + agent_config = extract_agent_config(agent_data, version_data) + if version_data: + logger.info(f"Using agent {agent_config['name']} ({agent_id}) version {agent_config.get('version_name', 'v1')} for workflow") else: - agent_config = agent_data - logger.info(f"Using agent {agent_config['name']} ({agent_id}) - no version data") + logger.info(f"Using agent {agent_config['name']} ({agent_id}) - no version data for workflow") available_tools = [] @@ -814,23 +803,9 @@ async def trigger_workflow_webhook( agent_data = agent_result.data[0] account_id = agent_data['account_id'] - if agent_data.get('agent_versions'): - version_data = agent_data['agent_versions'] - agent_config = { - 'agent_id': agent_data['agent_id'], - 'name': agent_data['name'], - 'description': agent_data.get('description'), - 'system_prompt': version_data['system_prompt'], - 'configured_mcps': version_data.get('configured_mcps', []), - 'custom_mcps': version_data.get('custom_mcps', []), - 'agentpress_tools': version_data.get('agentpress_tools', {}), - 'is_default': agent_data.get('is_default', False), - 'current_version_id': agent_data.get('current_version_id'), - 'version_name': version_data.get('version_name', 'v1'), - 'account_id': account_id - } - else: - agent_config = {**agent_data, 'account_id': account_id} + version_data = agent_data.get('agent_versions') + agent_config = extract_agent_config(agent_data, version_data) + agent_config['account_id'] = account_id # Ensure account_id is in the config from triggers.integration import WorkflowTriggerExecutor from triggers.core import TriggerResult, TriggerEvent, TriggerType diff --git a/backend/mcp_service/template_manager.py b/backend/mcp_service/template_manager.py index bfaec271..943fabb0 100644 --- a/backend/mcp_service/template_manager.py +++ b/backend/mcp_service/template_manager.py @@ -414,14 +414,6 @@ class TemplateManager: 'current_version_id': version_id, 'version_count': 1 }).eq('agent_id', instance_id).execute() - await client.table('agent_version_history').insert({ - "agent_id": instance_id, - "version_id": version_id, - "action": "created", - "changed_by": account_id, - "change_description": "Initial version created from template installation" - }).execute() - logger.info(f"Created initial version v1 for installed agent {instance_id}") else: logger.warning(f"Failed to create initial version for agent {instance_id}") diff --git a/backend/supabase/migrations/20250708042142_dabatabase_cleanup.sql b/backend/supabase/migrations/20250708042142_dabatabase_cleanup.sql new file mode 100644 index 00000000..9f1c4df6 --- /dev/null +++ b/backend/supabase/migrations/20250708042142_dabatabase_cleanup.sql @@ -0,0 +1,236 @@ +-- ===================================================== +-- DATABASE CLEANUP MIGRATION +-- ===================================================== +-- This migration: +-- 1. Removes unused OLD workflow tables (workflows, workflow_variables, workflow_templates, workflow_flows, workflow_execution_logs) +-- 2. Removes webhook_registrations (not used anywhere) +-- 3. Removes scheduled_jobs (part of old workflow system) +-- 4. Removes agent_instances (using agent_templates + user credentials instead) +-- 5. Removes user_mcp_credentials (replaced by user_mcp_credential_profiles) +-- 6. Consolidates agent configuration into single JSONB config column +-- 7. Comments out oauth_installations (not removed, just renamed to indicate deprecated) +-- 8. Consolidates agent_version_history into agent_versions table +-- ===================================================== + +BEGIN; + +-- ===================================================== +-- 1. DROP OLD WORKFLOW SYSTEM TABLES +-- ===================================================== +-- These are the old workflow tables from April 2025, NOT the new agent_workflows from July 2025 +-- Note: workflow_executions is used by BOTH systems, so we don't drop it +DROP TABLE IF EXISTS workflow_flows CASCADE; +DROP TABLE IF EXISTS workflow_execution_logs CASCADE; +DROP TABLE IF EXISTS workflow_variables CASCADE; +DROP TABLE IF EXISTS webhook_registrations CASCADE; +DROP TABLE IF EXISTS scheduled_jobs CASCADE; +DROP TABLE IF EXISTS triggers CASCADE; +-- DO NOT DROP workflow_executions - it's used by agent_workflows system +DROP TABLE IF EXISTS workflow_templates CASCADE; +DROP TABLE IF EXISTS workflows CASCADE; + +-- Drop old workflow types (being careful not to drop types used by agent workflows) +DROP TYPE IF EXISTS connection_type CASCADE; +DROP TYPE IF EXISTS node_type CASCADE; +DROP TYPE IF EXISTS trigger_type CASCADE; +DROP TYPE IF EXISTS execution_status CASCADE; +DROP TYPE IF EXISTS workflow_status CASCADE; +-- DO NOT DROP workflow_execution_status - it's used by agent_workflows system + +-- ===================================================== +-- 2. REMOVE AGENT_INSTANCES TABLE +-- ===================================================== +-- This table is not used - we have agent_templates for marketplace +-- and agents table for user's actual agents +DROP TABLE IF EXISTS agent_instances CASCADE; + +-- ===================================================== +-- 3. REMOVE USER_MCP_CREDENTIALS TABLE +-- ===================================================== +-- This is replaced by user_mcp_credential_profiles +DROP TABLE IF EXISTS user_mcp_credentials CASCADE; + +-- ===================================================== +-- 4. CONSOLIDATE AGENT CONFIGURATION +-- ===================================================== +-- Add new unified config column to agents table +ALTER TABLE agents ADD COLUMN IF NOT EXISTS config JSONB DEFAULT '{}'::jsonb; + +-- Migrate existing data to unified config +-- Note: agentpress_tools currently stores {tool_name: {enabled: bool, description: string}} +-- The description is redundant as it's the same for all agents. In the new structure, +-- we'll just store {tool_name: boolean} for enabled/disabled state +UPDATE agents +SET config = jsonb_build_object( + 'system_prompt', COALESCE(system_prompt, ''), + 'tools', jsonb_build_object( + 'agentpress', ( + SELECT jsonb_object_agg( + key, + (value->>'enabled')::boolean + ) + FROM jsonb_each(COALESCE(agentpress_tools, '{}'::jsonb)) + WHERE value IS NOT NULL AND value != 'null'::jsonb + ), + 'mcp', COALESCE(configured_mcps, '[]'::jsonb), + 'custom_mcp', COALESCE(custom_mcps, '[]'::jsonb) + ), + 'metadata', jsonb_build_object( + 'avatar', avatar, + 'avatar_color', avatar_color + ) +) +WHERE config = '{}'::jsonb OR config IS NULL; + +-- Update agent_versions table to use unified config +ALTER TABLE agent_versions ADD COLUMN IF NOT EXISTS config JSONB DEFAULT '{}'::jsonb; + +UPDATE agent_versions +SET config = jsonb_build_object( + 'system_prompt', COALESCE(system_prompt, ''), + 'tools', jsonb_build_object( + 'agentpress', ( + SELECT jsonb_object_agg( + key, + (value->>'enabled')::boolean + ) + FROM jsonb_each(COALESCE(agentpress_tools, '{}'::jsonb)) + WHERE value IS NOT NULL AND value != 'null'::jsonb + ), + 'mcp', COALESCE(configured_mcps, '[]'::jsonb), + 'custom_mcp', COALESCE(custom_mcps, '[]'::jsonb) + ) +) +WHERE config = '{}'::jsonb OR config IS NULL; + +-- ===================================================== +-- 5. CONSOLIDATE AGENT_VERSION_HISTORY INTO AGENT_VERSIONS +-- ===================================================== +-- Add history fields to agent_versions +ALTER TABLE agent_versions ADD COLUMN IF NOT EXISTS change_description TEXT; +ALTER TABLE agent_versions ADD COLUMN IF NOT EXISTS previous_version_id UUID REFERENCES agent_versions(version_id); + +-- Migrate history data +UPDATE agent_versions v +SET change_description = ( + SELECT h.change_description + FROM agent_version_history h + WHERE h.version_id = v.version_id + AND h.action = 'created' + LIMIT 1 +); + +-- Drop the separate history table +DROP TABLE IF EXISTS agent_version_history CASCADE; + +-- ===================================================== +-- 6. CLEAN UP USER_AGENT_LIBRARY +-- ===================================================== +-- This table tracks which marketplace agents users have imported +-- Add a comment to clarify its purpose +COMMENT ON TABLE user_agent_library IS 'Tracks which marketplace agent templates users have imported/cloned to their library'; +COMMENT ON COLUMN user_agent_library.original_agent_id IS 'The original marketplace agent that was cloned'; +COMMENT ON COLUMN user_agent_library.agent_id IS 'The user''s cloned copy of the agent'; + +-- ===================================================== +-- 7. COMMENT OUT OAUTH_INSTALLATIONS +-- ===================================================== +-- As requested, comment out OAuth installations functionality +-- We'll rename the table to indicate it's deprecated +ALTER TABLE IF EXISTS oauth_installations RENAME TO _deprecated_oauth_installations; +COMMENT ON TABLE _deprecated_oauth_installations IS 'DEPRECATED: OAuth installations table - functionality has been commented out'; + +-- ===================================================== +-- 8. UPDATE AGENT TRIGGERS TO WORK WITH AGENT_WORKFLOWS +-- ===================================================== +-- Add workflow execution capability to triggers +ALTER TABLE agent_triggers ADD COLUMN IF NOT EXISTS execution_type VARCHAR(50) DEFAULT 'agent' CHECK (execution_type IN ('agent', 'workflow')); +ALTER TABLE agent_triggers ADD COLUMN IF NOT EXISTS workflow_id UUID REFERENCES agent_workflows(id) ON DELETE SET NULL; + +-- Update trigger_events to track workflow executions +-- Note: workflow_executions table exists from agent_workflows system +ALTER TABLE trigger_events ADD COLUMN IF NOT EXISTS workflow_execution_id UUID REFERENCES workflow_executions(id) ON DELETE SET NULL; + +-- ===================================================== +-- 9. CLEAN UP COLUMNS AFTER MIGRATION +-- ===================================================== +-- Schedule these to be dropped in a future migration after code is updated +COMMENT ON COLUMN agents.system_prompt IS 'DEPRECATED: Use config->>system_prompt instead'; +COMMENT ON COLUMN agents.configured_mcps IS 'DEPRECATED: Use config->>tools->>mcp instead'; +COMMENT ON COLUMN agents.agentpress_tools IS 'DEPRECATED: Use config->>tools->>agentpress instead'; +COMMENT ON COLUMN agents.custom_mcps IS 'DEPRECATED: Use config->>tools->>custom_mcp instead'; +COMMENT ON COLUMN agents.avatar IS 'DEPRECATED: Use config->>metadata->>avatar instead'; +COMMENT ON COLUMN agents.avatar_color IS 'DEPRECATED: Use config->>metadata->>avatar_color instead'; + +COMMENT ON COLUMN agent_versions.system_prompt IS 'DEPRECATED: Use config->>system_prompt instead'; +COMMENT ON COLUMN agent_versions.configured_mcps IS 'DEPRECATED: Use config->>tools->>mcp instead'; +COMMENT ON COLUMN agent_versions.agentpress_tools IS 'DEPRECATED: Use config->>tools->>agentpress instead'; +COMMENT ON COLUMN agent_versions.custom_mcps IS 'DEPRECATED: Use config->>tools->>custom_mcp instead'; + +-- ===================================================== +-- 10. CREATE HELPER FUNCTIONS FOR NEW CONFIG FORMAT +-- ===================================================== +-- Function to get agent config in a backward-compatible way +CREATE OR REPLACE FUNCTION get_agent_config(p_agent_id UUID) +RETURNS JSONB +LANGUAGE plpgsql +SECURITY DEFINER +AS $$ +DECLARE + v_agent RECORD; + v_config JSONB; +BEGIN + SELECT * INTO v_agent FROM agents WHERE agent_id = p_agent_id; + + IF NOT FOUND THEN + RETURN NULL; + END IF; + + -- If config is already populated, return it + IF v_agent.config IS NOT NULL AND v_agent.config != '{}'::jsonb THEN + RETURN v_agent.config; + END IF; + + -- Otherwise build it from legacy columns + v_config := jsonb_build_object( + 'system_prompt', COALESCE(v_agent.system_prompt, ''), + 'tools', jsonb_build_object( + 'agentpress', ( + SELECT jsonb_object_agg( + key, + (value->>'enabled')::boolean + ) + FROM jsonb_each(COALESCE(v_agent.agentpress_tools, '{}'::jsonb)) + WHERE value IS NOT NULL AND value != 'null'::jsonb + ), + 'mcp', COALESCE(v_agent.configured_mcps, '[]'::jsonb), + 'custom_mcp', COALESCE(v_agent.custom_mcps, '[]'::jsonb) + ), + 'metadata', jsonb_build_object( + 'avatar', v_agent.avatar, + 'avatar_color', v_agent.avatar_color + ) + ); + + RETURN v_config; +END; +$$; + +-- Grant permissions +GRANT EXECUTE ON FUNCTION get_agent_config(UUID) TO authenticated, service_role; + +-- ===================================================== +-- 11. ADD COMMENTS FOR CLARITY +-- ===================================================== +COMMENT ON TABLE agent_workflows IS 'Agent workflows - step-by-step task execution'; +COMMENT ON TABLE workflow_steps IS 'Individual steps within an agent workflow'; +COMMENT ON TABLE workflow_executions IS 'Execution history of agent workflows'; +COMMENT ON TABLE workflow_step_executions IS 'Detailed execution logs for each workflow step'; + +COMMENT ON COLUMN agents.config IS 'Unified configuration object containing all agent settings'; +COMMENT ON COLUMN agent_versions.config IS 'Versioned configuration snapshot'; + +COMMENT ON COLUMN agents.is_default IS 'Whether this agent is the default for the account (only one allowed per account)'; +COMMENT ON COLUMN agent_triggers.execution_type IS 'Whether trigger executes an agent conversation or a workflow'; + +COMMIT; \ No newline at end of file diff --git a/backend/triggers/oauth/base.py b/backend/triggers/oauth/base.py index 27e9f4d1..291a8f31 100644 --- a/backend/triggers/oauth/base.py +++ b/backend/triggers/oauth/base.py @@ -203,7 +203,8 @@ class BaseOAuthProvider(abc.ABC): ) # Store OAuth data - await self._store_oauth_data(trigger_config.trigger_id, token_response, provider_data) + # COMMENTED OUT: OAuth installations table deprecated + # await self._store_oauth_data(trigger_config.trigger_id, token_response, provider_data) import os base_url = os.getenv("WEBHOOK_BASE_URL", "http://localhost:8000") @@ -246,21 +247,22 @@ class BaseOAuthProvider(abc.ABC): logger.error(f"Error verifying state token: {e}") return None - async def _store_oauth_data( - self, - trigger_id: str, - token_response: OAuthTokenResponse, - provider_data: Dict[str, Any] - ): - """Store OAuth installation data.""" - client = await self.db.client - await client.table('oauth_installations').insert({ - 'trigger_id': trigger_id, - 'provider': self.config.provider.value, - 'access_token': token_response.access_token, - 'refresh_token': token_response.refresh_token, - 'expires_in': token_response.expires_in, - 'scope': token_response.scope, - 'provider_data': provider_data, - 'installed_at': 'now()' - }).execute() \ No newline at end of file + # COMMENTED OUT: OAuth installations functionality deprecated + # async def _store_oauth_data( + # self, + # trigger_id: str, + # token_response: OAuthTokenResponse, + # provider_data: Dict[str, Any] + # ): + # """Store OAuth installation data.""" + # client = await self.db.client + # await client.table('oauth_installations').insert({ + # 'trigger_id': trigger_id, + # 'provider': self.config.provider.value, + # 'access_token': token_response.access_token, + # 'refresh_token': token_response.refresh_token, + # 'expires_in': token_response.expires_in, + # 'scope': token_response.scope, + # 'provider_data': provider_data, + # 'installed_at': 'now()' + # }).execute() \ No newline at end of file diff --git a/backend/triggers/oauth_api.py b/backend/triggers/oauth_api.py index bea89a3a..eb9f0549 100644 --- a/backend/triggers/oauth_api.py +++ b/backend/triggers/oauth_api.py @@ -115,27 +115,31 @@ async def get_slack_status( client = await db.client result = await client.table('agent_triggers')\ - .select('trigger_id, name, is_active')\ + .select('trigger_id, name, is_active, config')\ .eq('agent_id', agent_id)\ .eq('trigger_type', 'slack')\ .execute() slack_triggers = [] for trigger in result.data: - oauth_result = await client.table('slack_oauth_installations')\ - .select('team_name, bot_name, installed_at')\ - .eq('trigger_id', trigger['trigger_id'])\ - .execute() + # COMMENTED OUT: OAuth installations table deprecated + # oauth_result = await client.table('slack_oauth_installations')\ + # .select('team_name, bot_name, installed_at')\ + # .eq('trigger_id', trigger['trigger_id'])\ + # .execute() - oauth_data = oauth_result.data[0] if oauth_result.data else {} + # oauth_data = oauth_result.data[0] if oauth_result.data else {} + + # Get data from trigger config instead + config = trigger.get('config', {}) slack_triggers.append({ "trigger_id": trigger["trigger_id"], "name": trigger["name"], "is_active": trigger["is_active"], - "workspace_name": oauth_data.get("team_name"), - "bot_name": oauth_data.get("bot_name"), - "installed_at": oauth_data.get("installed_at") + "workspace_name": config.get("team_name"), + "bot_name": config.get("bot_name"), + "installed_at": None # No longer tracked separately }) return { @@ -172,10 +176,11 @@ async def uninstall_slack_integration( success = await trigger_manager.delete_trigger(trigger_id) if success: - await client.table('slack_oauth_installations')\ - .delete()\ - .eq('trigger_id', trigger_id)\ - .execute() + # COMMENTED OUT: OAuth installations table deprecated + # await client.table('slack_oauth_installations')\ + # .delete()\ + # .eq('trigger_id', trigger_id)\ + # .execute() return {"success": True, "message": "Slack integration uninstalled"} else: diff --git a/backend/triggers/providers/slack_oauth.py b/backend/triggers/providers/slack_oauth.py index 04b0dfdf..24d0f1f0 100644 --- a/backend/triggers/providers/slack_oauth.py +++ b/backend/triggers/providers/slack_oauth.py @@ -169,7 +169,8 @@ class SlackOAuthManager: config=config ) - await self._store_oauth_data(trigger_config.trigger_id, oauth_data, workspace_info, bot_info) + # COMMENTED OUT: OAuth installations functionality deprecated + # await self._store_oauth_data(trigger_config.trigger_id, oauth_data, workspace_info, bot_info) base_url = os.getenv("WEBHOOK_BASE_URL", "http://localhost:8000") # Slack requires a single Event Request URL per app @@ -200,25 +201,26 @@ class SlackOAuthManager: logger.error(f"Error verifying state token: {e}") return None - async def _store_oauth_data( - self, - trigger_id: str, - oauth_data: Dict[str, Any], - workspace_info: Dict[str, Any], - bot_info: Dict[str, Any] - ): - """Store OAuth data for the trigger.""" - client = await self.db.client - await client.table('slack_oauth_installations').insert({ - 'trigger_id': trigger_id, - 'team_id': oauth_data['team_id'], - 'team_name': oauth_data['team_name'], - 'access_token': oauth_data['access_token'], - 'bot_user_id': oauth_data['bot_user_id'], - 'bot_name': bot_info.get('name'), - 'app_id': oauth_data['app_id'], - 'scope': oauth_data['scope'], - 'workspace_info': workspace_info, - 'bot_info': bot_info, - 'installed_at': 'now()' - }).execute() \ No newline at end of file + # COMMENTED OUT: OAuth installations functionality deprecated + # async def _store_oauth_data( + # self, + # trigger_id: str, + # oauth_data: Dict[str, Any], + # workspace_info: Dict[str, Any], + # bot_info: Dict[str, Any] + # ): + # """Store OAuth data for the trigger.""" + # client = await self.db.client + # await client.table('slack_oauth_installations').insert({ + # 'trigger_id': trigger_id, + # 'team_id': oauth_data['team_id'], + # 'team_name': oauth_data['team_name'], + # 'access_token': oauth_data['access_token'], + # 'bot_user_id': oauth_data['bot_user_id'], + # 'bot_name': bot_info.get('name'), + # 'app_id': oauth_data['app_id'], + # 'scope': oauth_data['scope'], + # 'workspace_info': workspace_info, + # 'bot_info': bot_info, + # 'installed_at': 'now()' + # }).execute() \ No newline at end of file diff --git a/backend/triggers/unified_oauth_api.py b/backend/triggers/unified_oauth_api.py index 23551296..7b7ce9cf 100644 --- a/backend/triggers/unified_oauth_api.py +++ b/backend/triggers/unified_oauth_api.py @@ -152,32 +152,36 @@ async def get_integration_status( client = await db.client result = await client.table('agent_triggers')\ - .select('trigger_id, trigger_type, name, is_active, created_at')\ + .select('trigger_id, trigger_type, name, is_active, created_at, config')\ .eq('agent_id', agent_id)\ .in_('trigger_type', ['slack', 'discord', 'teams'])\ .execute() integrations = [] for trigger in result.data: - oauth_result = await client.table('oauth_installations')\ - .select('provider, provider_data, installed_at')\ - .eq('trigger_id', trigger['trigger_id'])\ - .execute() + # COMMENTED OUT: OAuth installations table deprecated + # oauth_result = await client.table('oauth_installations')\ + # .select('provider, provider_data, installed_at')\ + # .eq('trigger_id', trigger['trigger_id'])\ + # .execute() - if oauth_result.data: - oauth_data = oauth_result.data[0] - provider_data = oauth_data.get('provider_data', {}) - - integrations.append({ - "trigger_id": trigger["trigger_id"], - "provider": oauth_data["provider"], - "name": trigger["name"], - "is_active": trigger["is_active"], - "workspace_name": provider_data.get("workspace_name"), - "bot_name": provider_data.get("bot_name"), - "installed_at": oauth_data["installed_at"], - "created_at": trigger["created_at"] - }) + # if oauth_result.data: + # oauth_data = oauth_result.data[0] + # provider_data = oauth_data.get('provider_data', {}) + + # Get data from trigger config instead + config = trigger.get('config', {}) + + integrations.append({ + "trigger_id": trigger["trigger_id"], + "provider": trigger["trigger_type"], + "name": trigger["name"], + "is_active": trigger["is_active"], + "workspace_name": config.get("team_name") or config.get("guild_name") or config.get("organization"), + "bot_name": config.get("bot_name"), + "installed_at": trigger["created_at"], # Use trigger creation time + "created_at": trigger["created_at"] + }) return IntegrationStatusResponse( agent_id=agent_id, @@ -214,10 +218,11 @@ async def uninstall_integration( success = await trigger_manager.delete_trigger(trigger_id) if success: - await client.table('oauth_installations')\ - .delete()\ - .eq('trigger_id', trigger_id)\ - .execute() + # COMMENTED OUT: OAuth installations table deprecated + # await client.table('oauth_installations')\ + # .delete()\ + # .eq('trigger_id', trigger_id)\ + # .execute() return { "success": True,