diff --git a/backend/core/templates/api.py b/backend/core/templates/api.py index b992df02..190075a1 100644 --- a/backend/core/templates/api.py +++ b/backend/core/templates/api.py @@ -48,6 +48,7 @@ class InstallTemplateRequest(BaseModel): profile_mappings: Optional[Dict[str, str]] = None custom_mcp_configs: Optional[Dict[str, Dict[str, Any]]] = None trigger_configs: Optional[Dict[str, Dict[str, Any]]] = None + trigger_variables: Optional[Dict[str, Dict[str, str]]] = None class PublishTemplateRequest(BaseModel): @@ -83,6 +84,7 @@ class InstallationResponse(BaseModel): name: Optional[str] = None missing_regular_credentials: List[Dict[str, Any]] = [] missing_custom_configs: List[Dict[str, Any]] = [] + missing_trigger_variables: Optional[Dict[str, Dict[str, Any]]] = None template_info: Optional[Dict[str, Any]] = None @@ -323,7 +325,8 @@ async def install_template( custom_system_prompt=request.custom_system_prompt, profile_mappings=request.profile_mappings, custom_mcp_configs=request.custom_mcp_configs, - trigger_configs=request.trigger_configs + trigger_configs=request.trigger_configs, + trigger_variables=request.trigger_variables ) result = await installation_service.install_template(install_request) @@ -336,6 +339,7 @@ async def install_template( name=result.name, missing_regular_credentials=result.missing_regular_credentials, missing_custom_configs=result.missing_custom_configs, + missing_trigger_variables=result.missing_trigger_variables, template_info=result.template_info ) diff --git a/backend/core/templates/installation_service.py b/backend/core/templates/installation_service.py index 750a950d..4f385ad9 100644 --- a/backend/core/templates/installation_service.py +++ b/backend/core/templates/installation_service.py @@ -5,6 +5,7 @@ from uuid import uuid4 import os import json import httpx +import re from core.services.supabase import DBConnection from core.utils.logger import logger @@ -34,6 +35,7 @@ class TemplateInstallationRequest: profile_mappings: Optional[Dict[QualifiedName, ProfileId]] = None custom_mcp_configs: Optional[Dict[QualifiedName, ConfigType]] = None trigger_configs: Optional[Dict[str, Dict[str, Any]]] = None + trigger_variables: Optional[Dict[str, Dict[str, str]]] = None @dataclass class TemplateInstallationResult: @@ -42,6 +44,7 @@ class TemplateInstallationResult: name: Optional[str] = None missing_regular_credentials: List[Dict[str, Any]] = field(default_factory=list) missing_custom_configs: List[Dict[str, Any]] = field(default_factory=list) + missing_trigger_variables: Optional[Dict[str, Dict[str, Any]]] = None template_info: Optional[Dict[str, Any]] = None class TemplateInstallationError(Exception): @@ -54,6 +57,41 @@ class InstallationService: def __init__(self, db_connection: DBConnection): self._db = db_connection + def _extract_trigger_variables(self, config: Dict[str, Any]) -> Dict[str, Dict[str, Any]]: + """Extract all trigger variables from template config""" + trigger_variables = {} + triggers = config.get('triggers', []) + + for i, trigger in enumerate(triggers): + trigger_config = trigger.get('config', {}) + trigger_name = trigger.get('name', f'Trigger {i+1}') + agent_prompt = trigger_config.get('agent_prompt', '') + variables = trigger_config.get('trigger_variables', []) + + # If no variables were stored, try to extract them from the prompt + if not variables and agent_prompt: + pattern = r'\{\{(\w+)\}\}' + matches = re.findall(pattern, agent_prompt) + variables = list(set(matches)) + + if variables: + trigger_key = f"trigger_{i}" + trigger_variables[trigger_key] = { + 'trigger_name': trigger_name, + 'trigger_index': i, + 'variables': variables, + 'agent_prompt': agent_prompt + } + + return trigger_variables + + def _replace_variables_in_text(self, text: str, variable_values: Dict[str, str]) -> str: + """Replace {{variable}} patterns in text with actual values""" + for var_name, var_value in variable_values.items(): + pattern = r'\{\{' + re.escape(var_name) + r'\}\}' + text = re.sub(pattern, var_value, text) + return text + async def install_template(self, request: TemplateInstallationRequest) -> TemplateInstallationResult: logger.debug(f"Installing template {request.template_id} for user {request.account_id}") logger.debug(f"Initial profile_mappings from request: {request.profile_mappings}") @@ -86,11 +124,35 @@ class InstallationService: logger.debug(f"Missing profiles: {[p['qualified_name'] for p in missing_profiles]}") logger.debug(f"Missing configs: {[c['qualified_name'] for c in missing_configs]}") - if missing_profiles or missing_configs: + # Check for trigger variables + trigger_variables = self._extract_trigger_variables(template.config) + missing_trigger_variables = {} + + if trigger_variables and not request.trigger_variables: + # All trigger variables are missing + missing_trigger_variables = trigger_variables + elif trigger_variables and request.trigger_variables: + # Check which trigger variables are still missing + for trigger_key, trigger_data in trigger_variables.items(): + if trigger_key not in request.trigger_variables: + missing_trigger_variables[trigger_key] = trigger_data + else: + # Check if all required variables for this trigger are provided + provided_vars = request.trigger_variables.get(trigger_key, {}) + missing_vars = [] + for var in trigger_data['variables']: + if var not in provided_vars: + missing_vars.append(var) + if missing_vars: + trigger_data['missing_variables'] = missing_vars + missing_trigger_variables[trigger_key] = trigger_data + + if missing_profiles or missing_configs or missing_trigger_variables: return TemplateInstallationResult( status='configs_required', missing_regular_credentials=missing_profiles, missing_custom_configs=missing_configs, + missing_trigger_variables=missing_trigger_variables if missing_trigger_variables else None, template_info={ 'template_id': template.template_id, 'name': template.name @@ -116,7 +178,7 @@ class InstallationService: request.custom_system_prompt or template.system_prompt ) - await self._restore_triggers(agent_id, request.account_id, template.config, request.profile_mappings, request.trigger_configs) + await self._restore_triggers(agent_id, request.account_id, template.config, request.profile_mappings, request.trigger_configs, request.trigger_variables) await self._increment_download_count(template.template_id) @@ -410,7 +472,8 @@ class InstallationService: account_id: str, config: Dict[str, Any], profile_mappings: Optional[Dict[str, str]] = None, - trigger_configs: Optional[Dict[str, Dict[str, Any]]] = None + trigger_configs: Optional[Dict[str, Dict[str, Any]]] = None, + trigger_variables: Optional[Dict[str, Dict[str, str]]] = None ) -> None: triggers = config.get('triggers', []) if not triggers: @@ -425,6 +488,17 @@ class InstallationService: trigger_config = trigger.get('config', {}) provider_id = trigger_config.get('provider_id', '') + # Handle trigger variables if any + trigger_key = f"trigger_{i}" + agent_prompt = trigger_config.get('agent_prompt', '') + + if trigger_variables and trigger_key in trigger_variables and agent_prompt: + # Replace variables in the agent prompt + variable_values = trigger_variables[trigger_key] + agent_prompt = self._replace_variables_in_text(agent_prompt, variable_values) + trigger_config['agent_prompt'] = agent_prompt + logger.debug(f"Replaced variables in trigger {i} prompt: {variable_values}") + if provider_id == 'composio': qualified_name = trigger_config.get('qualified_name') @@ -455,7 +529,7 @@ class InstallationService: is_active=trigger.get('is_active', True), trigger_slug=trigger_config.get('trigger_slug', ''), qualified_name=qualified_name, - agent_prompt=trigger_config.get('agent_prompt'), + agent_prompt=agent_prompt, # Use the potentially modified agent_prompt profile_mappings=profile_mappings, trigger_profile_key=trigger_profile_key, trigger_specific_config=trigger_specific_config @@ -466,6 +540,11 @@ class InstallationService: else: failed_count += 1 else: + # For schedule triggers, the agent_prompt is already updated in trigger_config + # We need to ensure trigger_variables are removed if they exist + clean_config = trigger_config.copy() + if 'trigger_variables' in clean_config: + del clean_config['trigger_variables'] trigger_data = { 'trigger_id': str(uuid4()), @@ -474,7 +553,7 @@ class InstallationService: 'name': trigger.get('name', 'Unnamed Trigger'), 'description': trigger.get('description'), 'is_active': trigger.get('is_active', True), - 'config': trigger_config.copy(), + 'config': clean_config, 'created_at': datetime.now(timezone.utc).isoformat(), 'updated_at': datetime.now(timezone.utc).isoformat() } diff --git a/backend/core/templates/template_service.py b/backend/core/templates/template_service.py index 161092bd..970c7c17 100644 --- a/backend/core/templates/template_service.py +++ b/backend/core/templates/template_service.py @@ -1,4 +1,5 @@ import json +import re from dataclasses import dataclass, field from datetime import datetime, timezone from typing import Dict, List, Any, Optional @@ -484,11 +485,25 @@ class TemplateService: trigger_config = trigger.get('config', {}) provider_id = trigger_config.get('provider_id', '') + agent_prompt = trigger_config.get('agent_prompt', '') + sanitized_config = { 'provider_id': provider_id, - 'agent_prompt': trigger_config.get('agent_prompt', ''), + 'agent_prompt': agent_prompt, } + # Extract trigger variables if they exist in the prompt + trigger_variables = trigger_config.get('trigger_variables', []) + if not trigger_variables and agent_prompt: + # Extract variables from the prompt using regex + pattern = r'\{\{(\w+)\}\}' + matches = re.findall(pattern, agent_prompt) + if matches: + trigger_variables = list(set(matches)) + + if trigger_variables: + sanitized_config['trigger_variables'] = trigger_variables + if provider_id == 'schedule': sanitized_config['cron_expression'] = trigger_config.get('cron_expression', '') sanitized_config['timezone'] = trigger_config.get('timezone', 'UTC') @@ -499,7 +514,7 @@ class TemplateService: excluded_fields = { 'profile_id', 'composio_trigger_id', 'provider_id', - 'agent_prompt', 'trigger_slug', 'qualified_name' + 'agent_prompt', 'trigger_slug', 'qualified_name', 'trigger_variables' } trigger_fields = {} diff --git a/backend/core/tools/agent_builder_tools/trigger_tool.py b/backend/core/tools/agent_builder_tools/trigger_tool.py index 938d83e2..4a50a91e 100644 --- a/backend/core/tools/agent_builder_tools/trigger_tool.py +++ b/backend/core/tools/agent_builder_tools/trigger_tool.py @@ -1,4 +1,5 @@ import json +import re from typing import Optional, Dict, Any, List from core.agentpress.tool import ToolResult, openapi_schema from core.agentpress.thread_manager import ThreadManager @@ -17,6 +18,17 @@ from core.composio_integration.composio_trigger_service import ComposioTriggerSe class TriggerTool(AgentBuilderBaseTool): def __init__(self, thread_manager: ThreadManager, db_connection, agent_id: str): super().__init__(thread_manager, db_connection, agent_id) + + def _extract_variables(self, text: str) -> List[str]: + """Extract variable names from a text containing {{variable}} patterns""" + pattern = r'\{\{(\w+)\}\}' + matches = re.findall(pattern, text) + return list(set(matches)) + + def _has_variables(self, text: str) -> bool: + """Check if text contains any {{variable}} patterns""" + pattern = r'\{\{(\w+)\}\}' + return bool(re.search(pattern, text)) async def _sync_triggers_to_version_config(self) -> None: try: @@ -63,7 +75,7 @@ class TriggerTool(AgentBuilderBaseTool): "type": "function", "function": { "name": "create_scheduled_trigger", - "description": "Create a scheduled trigger for the agent to execute at specified times using cron expressions. This allows the agent to run automatically on a schedule.", + "description": "Create a scheduled trigger for the agent to execute at specified times using cron expressions. This allows the agent to run automatically on a schedule. TEMPLATE VARIABLES: Use {{variable_name}} syntax in prompts to create reusable templates. Example: Instead of 'Monitor Apple brand', use 'Monitor {{company_name}} brand'. Users will provide their own values when installing.", "parameters": { "type": "object", "properties": { @@ -81,7 +93,7 @@ class TriggerTool(AgentBuilderBaseTool): }, "agent_prompt": { "type": "string", - "description": "Prompt to send to the agent when triggered" + "description": "Prompt to send to the agent when triggered. Can include variables like {{variable_name}} that will be replaced when users install the template. For example: 'Monitor {{company_name}} brand across all platforms...'" } }, "required": ["name", "cron_expression", "agent_prompt"] @@ -99,12 +111,20 @@ class TriggerTool(AgentBuilderBaseTool): if not agent_prompt: return self.fail_response("agent_prompt is required") + # Extract variables from the prompt + variables = self._extract_variables(agent_prompt) + trigger_config = { "cron_expression": cron_expression, "provider_id": "schedule", "agent_prompt": agent_prompt } + # Add variables to config if any were found + if variables: + trigger_config["trigger_variables"] = variables + logger.debug(f"Found variables in trigger prompt: {variables}") + trigger_svc = get_trigger_service(self.db) try: @@ -120,6 +140,9 @@ class TriggerTool(AgentBuilderBaseTool): result_message += f"**Schedule**: {cron_expression}\n" result_message += f"**Type**: Agent execution\n" result_message += f"**Prompt**: {agent_prompt}\n" + if variables: + result_message += f"**Template Variables Detected**: {', '.join(['{{' + v + '}}' for v in variables])}\n" + result_message += f"*Note: Users will be prompted to provide values for these variables when installing this agent as a template.*\n" result_message += f"\nThe trigger is now active and will run according to the schedule." # Sync triggers to version config @@ -134,7 +157,8 @@ class TriggerTool(AgentBuilderBaseTool): "name": trigger.name, "description": trigger.description, "cron_expression": cron_expression, - "is_active": trigger.is_active + "is_active": trigger.is_active, + "variables": variables if variables else [] } }) except ValueError as ve: @@ -372,7 +396,7 @@ class TriggerTool(AgentBuilderBaseTool): "type": "function", "function": { "name": "create_event_trigger", - "description": "Create a Composio event-based trigger for this agent. First list apps and triggers, then pass the chosen trigger slug, profile_id, and trigger_config.", + "description": "Create a Composio event-based trigger for this agent. First list apps and triggers, then pass the chosen trigger slug, profile_id, and trigger_config. You can use variables in the prompt like {{company_name}} or {{brand_name}} to make templates reusable.", "parameters": { "type": "object", "properties": { @@ -380,7 +404,7 @@ class TriggerTool(AgentBuilderBaseTool): "profile_id": {"type": "string", "description": "Composio profile_id to use (must be connected)"}, "trigger_config": {"type": "object", "description": "Trigger configuration object per trigger schema", "additionalProperties": True}, "name": {"type": "string", "description": "Optional friendly name for the trigger"}, - "agent_prompt": {"type": "string", "description": "Prompt to pass to the agent when triggered"}, + "agent_prompt": {"type": "string", "description": "Prompt to pass to the agent when triggered. Can include variables like {{variable_name}} that will be replaced when users install the template. For example: 'New email received for {{company_name}}...'"}, "connected_account_id": {"type": "string", "description": "Connected account id; if omitted we try to derive from profile"} }, "required": ["slug", "profile_id", "agent_prompt"] @@ -399,6 +423,9 @@ class TriggerTool(AgentBuilderBaseTool): try: if not agent_prompt: return self.fail_response("agent_prompt is required") + + # Extract variables from the prompt + variables = self._extract_variables(agent_prompt) # Get profile config profile_service = ComposioProfileService(self.db) @@ -528,6 +555,11 @@ class TriggerTool(AgentBuilderBaseTool): "agent_prompt": agent_prompt } + # Add variables to config if any were found + if variables: + suna_config["trigger_variables"] = variables + logger.debug(f"Found variables in event trigger prompt: {variables}") + # Create Suna trigger trigger_svc = get_trigger_service(self.db) try: @@ -550,13 +582,17 @@ class TriggerTool(AgentBuilderBaseTool): message = f"Event trigger '{trigger.name}' created successfully.\n" message += "Agent execution configured." + if variables: + message += f"\n**Template Variables Detected**: {', '.join(['{{' + v + '}}' for v in variables])}\n" + message += f"*Note: Users will be prompted to provide values for these variables when installing this agent as a template.*" return self.success_response({ "message": message, "trigger": { "provider": "composio", "slug": slug, - "is_active": trigger.is_active + "is_active": trigger.is_active, + "variables": variables if variables else [] } }) except Exception as e: diff --git a/backend/core/tools/sb_docs_tool.py b/backend/core/tools/sb_docs_tool.py index 33719c04..5a8f94b7 100644 --- a/backend/core/tools/sb_docs_tool.py +++ b/backend/core/tools/sb_docs_tool.py @@ -429,7 +429,7 @@ IMPORTANT: All content must be wrapped in proper HTML tags. Do not use unsupport doc_info = all_metadata["documents"][doc_id] try: - await self.sandbox.fs.remove_file(doc_info["path"]) + await self.sandbox.fs.delete_file(doc_info["path"]) except: pass @@ -509,4 +509,400 @@ IMPORTANT: All content must be wrapped in proper HTML tags. Do not use unsupport "guide": guide, "message": "Use this guide to format HTML content for TipTap editor" }) + + def _generate_pdf_html(self, title: str, content: str, metadata: Optional[Dict] = None) -> str: + css_styles = """ + + """ + + current_time = datetime.now().strftime("%B %d, %Y at %I:%M %p") + + doc_html = f""" + + + + + + {html.escape(title)} + {css_styles} + + +
+
{html.escape(title)}
+
+ + """ + + if metadata: + if metadata.get("author"): + doc_html += f""" + + """ + + if metadata.get("tags"): + doc_html += """ + + """ + + doc_html += f""" +
+
+
+ {content} +
+ + + """ + + return doc_html + + @openapi_schema({ + "type": "function", + "function": { + "name": "convert_to_pdf", + "description": "Convert a document to PDF format", + "parameters": { + "type": "object", + "properties": { + "doc_id": { + "type": "string", + "description": "ID of the document to convert to PDF" + }, + "download": { + "type": "boolean", + "description": "If true, returns the PDF file for download. If false, saves it in the workspace", + "default": False + } + }, + "required": ["doc_id"] + } + } + }) + async def convert_to_pdf(self, doc_id: str, download: bool = False) -> ToolResult: + try: + await self._ensure_sandbox() + + all_metadata = await self._load_metadata() + + if doc_id not in all_metadata["documents"]: + return self.fail_response(f"Document with ID '{doc_id}' not found") + + doc_info = all_metadata["documents"][doc_id] + + content_raw = await self.sandbox.fs.download_file(doc_info["path"]) + content_str = content_raw.decode() + + if doc_info.get("format") in ["tiptap", "html", "doc"] or doc_info.get("is_tiptap_doc") or doc_info.get("doc_type") == "tiptap_document": + try: + document_wrapper = json.loads(content_str) + if document_wrapper.get("type") == "tiptap_document": + content = document_wrapper.get("content", "") + title = document_wrapper.get("title", doc_info["title"]) + metadata = document_wrapper.get("metadata", doc_info.get("metadata", {})) + else: + content = content_str + title = doc_info["title"] + metadata = doc_info.get("metadata", {}) + except json.JSONDecodeError: + content = content_str + title = doc_info["title"] + metadata = doc_info.get("metadata", {}) + else: + content = f"
{html.escape(content_str)}
" + title = doc_info["title"] + metadata = doc_info.get("metadata", {}) + + complete_html = self._generate_pdf_html(title, content, metadata) + + temp_html_filename = f"temp_pdf_{doc_id}.html" + temp_html_path = f"/workspace/{temp_html_filename}" + await self.sandbox.fs.upload_file(complete_html.encode(), temp_html_path) + + logger.info(f"Creating PDF from document: {title}") + + pdf_generation_script = f""" +import asyncio +from playwright.async_api import async_playwright +import sys + +async def html_to_pdf(): + try: + async with async_playwright() as p: + browser = await p.chromium.launch( + headless=True, + args=['--no-sandbox', '--disable-setuid-sandbox'] + ) + + page = await browser.new_page() + + await page.goto('file://{temp_html_path}', wait_until='networkidle') + + pdf_filename = '{self._sanitize_filename(title)}_{doc_id}.pdf' + pdf_path = f'/workspace/docs/{{pdf_filename}}' + + await page.pdf( + path=pdf_path, + format='A4', + print_background=True, + margin={{ + 'top': '0.5in', + 'right': '0.5in', + 'bottom': '0.5in', + 'left': '0.5in' + }} + ) + + await browser.close() + + print(pdf_path) + return pdf_path + + except Exception as e: + print(f"ERROR: {{str(e)}}", file=sys.stderr) + sys.exit(1) + +if __name__ == "__main__": + pdf_path = asyncio.run(html_to_pdf()) +""" + + script_path = f"/workspace/temp_pdf_script_{doc_id}.py" + await self.sandbox.fs.upload_file(pdf_generation_script.encode(), script_path) + + response = await self.sandbox.process.exec( + f"cd /workspace && python {script_path}", + timeout=30 + ) + + await self.sandbox.fs.delete_file(temp_html_path) + await self.sandbox.fs.delete_file(script_path) + + if response.exit_code != 0: + logger.error(f"PDF generation failed: {response.result}") + return self.fail_response(f"Failed to generate PDF: {response.result}") + + pdf_path = response.result.strip() + pdf_filename = pdf_path.split('/')[-1] + + pdf_info = { + "doc_id": doc_id, + "title": title, + "pdf_filename": pdf_filename, + "pdf_path": pdf_path, + "created_at": datetime.now().isoformat(), + "source_document": doc_info + } + + all_metadata["documents"][doc_id]["last_pdf_export"] = { + "filename": pdf_filename, + "path": pdf_path, + "exported_at": datetime.now().isoformat() + } + await self._save_metadata(all_metadata) + + preview_url = None + download_url = None + if hasattr(self, '_sandbox_url') and self._sandbox_url: + preview_url = f"{self._sandbox_url}/docs/{pdf_filename}" + download_url = preview_url + + if download: + pdf_content = await self.sandbox.fs.download_file(pdf_path) + + import base64 + pdf_base64 = base64.b64encode(pdf_content).decode('utf-8') + + return self.success_response({ + "success": True, + "message": f"PDF generated successfully from document '{title}'", + "pdf_info": pdf_info, + "pdf_base64": pdf_base64, + "pdf_filename": pdf_filename, + "preview_url": preview_url, + "download_url": download_url, + "sandbox_id": self.sandbox_id + }) + else: + return self.success_response({ + "success": True, + "message": f"PDF saved successfully: {pdf_filename}", + "pdf_info": pdf_info, + "preview_url": preview_url, + "download_url": download_url, + "sandbox_id": self.sandbox_id + }) + + except Exception as e: + logger.error(f"Error converting document to PDF: {str(e)}") + return self.fail_response(f"Error converting document to PDF: {str(e)}") \ No newline at end of file diff --git a/backend/core/tools/sb_kb_tool.py b/backend/core/tools/sb_kb_tool.py index 0877daba..3dc564a8 100644 --- a/backend/core/tools/sb_kb_tool.py +++ b/backend/core/tools/sb_kb_tool.py @@ -5,6 +5,7 @@ from core.sandbox.tool_base import SandboxToolsBase from core.agentpress.thread_manager import ThreadManager from core.utils.config import config from core.knowledge_base.validation import FileNameValidator, ValidationError +from core.utils.logger import logger class SandboxKbTool(SandboxToolsBase): """Tool for knowledge base operations using kb-fusion binary in a Daytona sandbox. @@ -16,11 +17,9 @@ class SandboxKbTool(SandboxToolsBase): self.kb_download_url = f"https://github.com/kortix-ai/kb-fusion/releases/download/v{self.kb_version}/kb" async def _execute_kb_command(self, command: str) -> dict: - """Execute a kb command with OPENAI_API_KEY environment variable set.""" await self._ensure_sandbox() - + env = {"OPENAI_API_KEY": config.OPENAI_API_KEY} if config.OPENAI_API_KEY else {} - response = await self.sandbox.process.exec(command, env=env) return { @@ -244,7 +243,6 @@ class SandboxKbTool(SandboxToolsBase): async def ls_kb(self) -> ToolResult: try: result = await self._execute_kb_command("kb ls") - if result["exit_code"] != 0: return self.fail_response(f"List operation failed: {result['output']}") diff --git a/backend/core/utils/tool_groups.py b/backend/core/utils/tool_groups.py index 31283e9d..c9b76042 100644 --- a/backend/core/utils/tool_groups.py +++ b/backend/core/utils/tool_groups.py @@ -631,6 +631,12 @@ TOOL_GROUPS: Dict[str, ToolGroup] = { description="Create new documents with rich text content", enabled=True ), + ToolMethod( + name="convert_to_pdf", + display_name="Convert to PDF", + description="Convert a document to PDF format", + enabled=True + ), ToolMethod( name="read_document", display_name="Read Document", diff --git a/frontend/src/app/(dashboard)/agents/page.tsx b/frontend/src/app/(dashboard)/agents/page.tsx index afe6ffbb..91a80367 100644 --- a/frontend/src/app/(dashboard)/agents/page.tsx +++ b/frontend/src/app/(dashboard)/agents/page.tsx @@ -333,7 +333,8 @@ export default function AgentsPage() { instanceName?: string, profileMappings?: Record, customMcpConfigs?: Record>, - triggerConfigs?: Record> + triggerConfigs?: Record>, + triggerVariables?: Record> ) => { setInstallingItemId(item.id); @@ -373,19 +374,37 @@ export default function AgentsPage() { return; } + console.log('Installing template with:', { + template_id: item.template_id, + instance_name: instanceName, + profile_mappings: profileMappings, + custom_mcp_configs: customMcpConfigs, + trigger_configs: triggerConfigs, + trigger_variables: triggerVariables + }); + const result = await installTemplateMutation.mutateAsync({ template_id: item.template_id, instance_name: instanceName, profile_mappings: profileMappings, custom_mcp_configs: customMcpConfigs, - trigger_configs: triggerConfigs + trigger_configs: triggerConfigs, + trigger_variables: triggerVariables }); + + console.log('Installation result:', result); if (result.status === 'installed') { toast.success(`Agent "${instanceName}" installed successfully!`); setShowInstallDialog(false); handleTabChange('my-agents'); } else if (result.status === 'configs_required') { + if (result.missing_trigger_variables && Object.keys(result.missing_trigger_variables).length > 0) { + toast.warning('Please provide values for template trigger variables.'); + setInstallingItemId(''); + return; + } + if (result.missing_regular_credentials && result.missing_regular_credentials.length > 0) { const updatedRequirements = [ ...(item.mcp_requirements || []), @@ -409,7 +428,12 @@ export default function AgentsPage() { toast.warning('Additional configurations required. Please complete the setup.'); return; + } else if (result.missing_custom_configs && result.missing_custom_configs.length > 0) { + console.error('Missing custom configs:', result.missing_custom_configs); + toast.error('Please provide all required custom MCP configurations'); + return; } else { + console.error('Unknown config required response:', result); toast.error('Please provide all required configurations'); return; } diff --git a/frontend/src/components/agents/installation/streamlined-install-dialog.tsx b/frontend/src/components/agents/installation/streamlined-install-dialog.tsx index 30a1f929..ebbfd4de 100644 --- a/frontend/src/components/agents/installation/streamlined-install-dialog.tsx +++ b/frontend/src/components/agents/installation/streamlined-install-dialog.tsx @@ -24,6 +24,8 @@ import { CustomServerStep } from './custom-server-step'; import type { MarketplaceTemplate, SetupStep } from './types'; import { AgentAvatar } from '@/components/thread/content/agent-avatar'; import { TriggerConfigStep } from './trigger-config-step'; +import { TriggerVariablesStep, type TriggerVariable } from './trigger-variables-step'; +import { toast } from 'sonner'; interface StreamlinedInstallDialogProps { item: MarketplaceTemplate | null; @@ -34,9 +36,11 @@ interface StreamlinedInstallDialogProps { instanceName: string, profileMappings: Record, customMcpConfigs: Record>, - triggerConfigs?: Record> + triggerConfigs?: Record>, + triggerVariables?: Record> ) => Promise; isInstalling: boolean; + onTriggerVariablesRequired?: (variables: Record) => void; } export const StreamlinedInstallDialog: React.FC = ({ @@ -44,13 +48,16 @@ export const StreamlinedInstallDialog: React.FC = open, onOpenChange, onInstall, - isInstalling + isInstalling, + onTriggerVariablesRequired }) => { const [currentStep, setCurrentStep] = useState(0); const [instanceName, setInstanceName] = useState(''); const [profileMappings, setProfileMappings] = useState>({}); const [customMcpConfigs, setCustomMcpConfigs] = useState>>({}); const [triggerConfigs, setTriggerConfigs] = useState>>({}); + const [triggerVariables, setTriggerVariables] = useState>>({}); + const [missingTriggerVariables, setMissingTriggerVariables] = useState>({}); const [setupSteps, setSetupSteps] = useState([]); const [isLoading, setIsLoading] = useState(false); @@ -150,10 +157,38 @@ export const StreamlinedInstallDialog: React.FC = setProfileMappings({}); setCustomMcpConfigs({}); setTriggerConfigs({}); + setTriggerVariables({}); + setMissingTriggerVariables({}); setIsLoading(true); const steps = generateSetupSteps(); setSetupSteps(steps); + + const triggers = item.config?.triggers || []; + const triggerVars: Record = {}; + + triggers.forEach((trigger, index) => { + const config = trigger.config || {}; + const variables = config.trigger_variables || []; + const agent_prompt = config.agent_prompt || ''; + let extractedVars = variables; + if (extractedVars.length === 0 && agent_prompt) { + const pattern = /\{\{(\w+)\}\}/g; + const matches = [...agent_prompt.matchAll(pattern)]; + extractedVars = [...new Set(matches.map(m => m[1]))]; + } + + if (extractedVars.length > 0) { + triggerVars[`trigger_${index}`] = { + trigger_name: trigger.name || `Trigger ${index + 1}`, + trigger_index: index, + variables: extractedVars, + agent_prompt: agent_prompt + }; + } + }); + + setMissingTriggerVariables(triggerVars); setIsLoading(false); } }, [open, item, generateSetupSteps]); @@ -220,8 +255,39 @@ export const StreamlinedInstallDialog: React.FC = } }, [currentStep]); + const areAllTriggerVariablesFilled = useCallback((): boolean => { + if (Object.keys(missingTriggerVariables).length === 0) return true; + + for (const [triggerKey, triggerData] of Object.entries(missingTriggerVariables)) { + const triggerVars = triggerVariables[triggerKey] || {}; + for (const varName of triggerData.variables) { + if (!triggerVars[varName] || triggerVars[varName].trim() === '') { + return false; + } + } + } + return true; + }, [missingTriggerVariables, triggerVariables]); + const handleInstall = useCallback(async () => { if (!item || !instanceName.trim()) return; + + console.log('Dialog handleInstall - triggerVariables:', triggerVariables); + console.log('Dialog handleInstall - missingTriggerVariables:', missingTriggerVariables); + + // Validate trigger variables if they exist + if (Object.keys(missingTriggerVariables).length > 0) { + for (const [triggerKey, triggerData] of Object.entries(missingTriggerVariables)) { + const triggerVars = triggerVariables[triggerKey] || {}; + for (const varName of triggerData.variables) { + if (!triggerVars[varName] || triggerVars[varName].trim() === '') { + toast.error(`Please provide all trigger variables for ${triggerData.trigger_name}`); + return; + } + } + } + } + const finalCustomConfigs = { ...customMcpConfigs }; setupSteps.forEach(step => { @@ -235,8 +301,8 @@ export const StreamlinedInstallDialog: React.FC = } }); - await onInstall(item, instanceName, profileMappings, finalCustomConfigs, triggerConfigs); - }, [item, instanceName, profileMappings, customMcpConfigs, triggerConfigs, setupSteps, onInstall]); + await onInstall(item, instanceName, profileMappings, finalCustomConfigs, triggerConfigs, triggerVariables); + }, [item, instanceName, profileMappings, customMcpConfigs, triggerConfigs, triggerVariables, missingTriggerVariables, setupSteps, onInstall]); const currentStepData = setupSteps[currentStep]; const isOnFinalStep = currentStep >= setupSteps.length; @@ -277,17 +343,45 @@ export const StreamlinedInstallDialog: React.FC = ) : (setupSteps.length === 0 || isOnFinalStep) ? (
-
-
- -
-
-

Ready to install!

-

- Give your agent a name and we'll set everything up. -

-
-
+ {Object.keys(missingTriggerVariables).length > 0 ? ( + <> +
+
+ +
+
+

Customize Trigger Variables

+

+ This template has triggers with variables that need your values. +

+
+
+ { + setTriggerVariables(prev => ({ + ...prev, + [triggerKey]: variables + })); + }} + /> + + ) : ( + <> +
+
+ +
+
+

Ready to install!

+

+ Give your agent a name and we'll set everything up. +

+
+
+ + )}
@@ -397,7 +491,7 @@ export const StreamlinedInstallDialog: React.FC = {isOnFinalStep ? (
diff --git a/frontend/src/components/agents/triggers/providers/simplified-schedule-config.tsx b/frontend/src/components/agents/triggers/providers/simplified-schedule-config.tsx index 57fa1b37..be218861 100644 --- a/frontend/src/components/agents/triggers/providers/simplified-schedule-config.tsx +++ b/frontend/src/components/agents/triggers/providers/simplified-schedule-config.tsx @@ -1044,7 +1044,7 @@ export const SimplifiedScheduleConfig: React.FC =

{errors.agent_prompt}

)}

- Use payload to include trigger data + Use {'{{variable_name}}'} to add variables to the prompt

diff --git a/frontend/src/components/dashboard/custom-agents-section.tsx b/frontend/src/components/dashboard/custom-agents-section.tsx index 9300b9cf..5d51eda3 100644 --- a/frontend/src/components/dashboard/custom-agents-section.tsx +++ b/frontend/src/components/dashboard/custom-agents-section.tsx @@ -100,7 +100,8 @@ export function CustomAgentsSection({ onAgentSelect }: CustomAgentsSectionProps) instanceName: string, profileMappings: Record, customServerConfigs: Record, - triggerConfigs?: Record> + triggerConfigs?: Record>, + triggerVariables?: Record> ) => { if (!item) return; @@ -113,6 +114,7 @@ export function CustomAgentsSection({ onAgentSelect }: CustomAgentsSectionProps) profile_mappings: profileMappings, custom_mcp_configs: customServerConfigs, trigger_configs: triggerConfigs, + trigger_variables: triggerVariables, }); if (result.status === 'installed' && result.instance_id) { @@ -123,6 +125,11 @@ export function CustomAgentsSection({ onAgentSelect }: CustomAgentsSectionProps) onAgentSelect(result.instance_id); } } else if (result.status === 'configs_required') { + if (result.missing_trigger_variables && Object.keys(result.missing_trigger_variables).length > 0) { + toast.warning('Please provide values for template trigger variables.'); + setInstallingItemId(''); + return; + } toast.error('Please provide all required configurations'); return; } else { diff --git a/frontend/src/hooks/react-query/secure-mcp/use-secure-mcp.ts b/frontend/src/hooks/react-query/secure-mcp/use-secure-mcp.ts index 0ef4a403..607dbea3 100644 --- a/frontend/src/hooks/react-query/secure-mcp/use-secure-mcp.ts +++ b/frontend/src/hooks/react-query/secure-mcp/use-secure-mcp.ts @@ -76,7 +76,7 @@ export interface MCPRequirement { display_name: string; enabled_tools: string[]; required_config: string[]; - custom_type?: 'sse' | 'http'; // For custom MCP servers + custom_type?: 'sse' | 'http'; } export interface InstallTemplateRequest { @@ -86,11 +86,13 @@ export interface InstallTemplateRequest { profile_mappings?: Record; custom_mcp_configs?: Record>; trigger_configs?: Record>; + trigger_variables?: Record>; } export interface InstallationResponse { status: 'installed' | 'configs_required'; instance_id?: string; + name?: string; missing_regular_credentials?: { qualified_name: string; display_name: string; @@ -102,6 +104,13 @@ export interface InstallationResponse { custom_type: string; required_config: string[]; }[]; + missing_trigger_variables?: Record; template?: { template_id: string; name: string; @@ -116,10 +125,6 @@ export interface CreateTemplateRequest { usage_examples?: UsageExampleMessage[]; } -// ===================================================== -// CREDENTIAL MANAGEMENT HOOKS -// ===================================================== - export function useUserCredentials() { return useQuery({ queryKey: ['secure-mcp', 'credentials'],