Compare commits

...

6 Commits

Author SHA1 Message Date
Codeded 1a3e023d95
Merge 2178963cf9 into 3ed60b291a 2025-10-09 17:55:29 +08:00
Bobbie 3ed60b291a
Merge pull request #1795 from escapade-mckv/schedule-variable
Schedule variable
2025-10-09 14:03:17 +05:30
Saumya d3f5d4fec8 add variables in trigger prompt 2025-10-09 14:02:09 +05:30
Saumya 014e6cf222 add variables in trigger prompt 2025-10-09 13:59:40 +05:30
Krishav 98ca19b6c3
Merge pull request #1793 from KrishavRajSingh/main
Revert "fiix: get usage info in case of tool"
2025-10-09 03:53:28 +05:30
Krishav Raj Singh e129709d96 Revert "fiix: get usage info in case of tool"
This reverts commit 90ee3585cb.
2025-10-09 03:52:48 +05:30
16 changed files with 860 additions and 49 deletions

View File

@ -488,9 +488,9 @@ class ResponseProcessor:
tool_index += 1 tool_index += 1
if finish_reason == "xml_tool_limit_reached": if finish_reason == "xml_tool_limit_reached":
logger.info("XML tool call limit reached, continuing stream to capture usage data") logger.info("Stopping stream processing after loop due to XML tool call limit")
self.trace.event(name="xml_tool_call_limit_reached_continuing_stream", level="DEFAULT", status_message=(f"XML tool call limit reached, continuing stream to capture usage data")) self.trace.event(name="stopping_stream_processing_after_loop_due_to_xml_tool_call_limit", level="DEFAULT", status_message=(f"Stopping stream processing after loop due to XML tool call limit"))
# Don't break - continue processing stream to capture final usage chunk break
logger.info(f"Stream complete. Total chunks: {chunk_count}") logger.info(f"Stream complete. Total chunks: {chunk_count}")

View File

@ -48,6 +48,7 @@ class InstallTemplateRequest(BaseModel):
profile_mappings: Optional[Dict[str, str]] = None profile_mappings: Optional[Dict[str, str]] = None
custom_mcp_configs: Optional[Dict[str, Dict[str, Any]]] = None custom_mcp_configs: Optional[Dict[str, Dict[str, Any]]] = 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
class PublishTemplateRequest(BaseModel): class PublishTemplateRequest(BaseModel):
@ -83,6 +84,7 @@ class InstallationResponse(BaseModel):
name: Optional[str] = None name: Optional[str] = None
missing_regular_credentials: List[Dict[str, Any]] = [] missing_regular_credentials: List[Dict[str, Any]] = []
missing_custom_configs: 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 template_info: Optional[Dict[str, Any]] = None
@ -323,7 +325,8 @@ async def install_template(
custom_system_prompt=request.custom_system_prompt, custom_system_prompt=request.custom_system_prompt,
profile_mappings=request.profile_mappings, profile_mappings=request.profile_mappings,
custom_mcp_configs=request.custom_mcp_configs, 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) result = await installation_service.install_template(install_request)
@ -336,6 +339,7 @@ async def install_template(
name=result.name, name=result.name,
missing_regular_credentials=result.missing_regular_credentials, missing_regular_credentials=result.missing_regular_credentials,
missing_custom_configs=result.missing_custom_configs, missing_custom_configs=result.missing_custom_configs,
missing_trigger_variables=result.missing_trigger_variables,
template_info=result.template_info template_info=result.template_info
) )

View File

@ -5,6 +5,7 @@ from uuid import uuid4
import os import os
import json import json
import httpx import httpx
import re
from core.services.supabase import DBConnection from core.services.supabase import DBConnection
from core.utils.logger import logger from core.utils.logger import logger
@ -34,6 +35,7 @@ class TemplateInstallationRequest:
profile_mappings: Optional[Dict[QualifiedName, ProfileId]] = None profile_mappings: Optional[Dict[QualifiedName, ProfileId]] = None
custom_mcp_configs: Optional[Dict[QualifiedName, ConfigType]] = None custom_mcp_configs: Optional[Dict[QualifiedName, ConfigType]] = 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
@dataclass @dataclass
class TemplateInstallationResult: class TemplateInstallationResult:
@ -42,6 +44,7 @@ class TemplateInstallationResult:
name: Optional[str] = None name: Optional[str] = None
missing_regular_credentials: List[Dict[str, Any]] = field(default_factory=list) missing_regular_credentials: List[Dict[str, Any]] = field(default_factory=list)
missing_custom_configs: 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 template_info: Optional[Dict[str, Any]] = None
class TemplateInstallationError(Exception): class TemplateInstallationError(Exception):
@ -54,6 +57,41 @@ class InstallationService:
def __init__(self, db_connection: DBConnection): def __init__(self, db_connection: DBConnection):
self._db = db_connection 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: async def install_template(self, request: TemplateInstallationRequest) -> TemplateInstallationResult:
logger.debug(f"Installing template {request.template_id} for user {request.account_id}") logger.debug(f"Installing template {request.template_id} for user {request.account_id}")
logger.debug(f"Initial profile_mappings from request: {request.profile_mappings}") 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 profiles: {[p['qualified_name'] for p in missing_profiles]}")
logger.debug(f"Missing configs: {[c['qualified_name'] for c in missing_configs]}") 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( return TemplateInstallationResult(
status='configs_required', status='configs_required',
missing_regular_credentials=missing_profiles, missing_regular_credentials=missing_profiles,
missing_custom_configs=missing_configs, missing_custom_configs=missing_configs,
missing_trigger_variables=missing_trigger_variables if missing_trigger_variables else None,
template_info={ template_info={
'template_id': template.template_id, 'template_id': template.template_id,
'name': template.name 'name': template.name
@ -116,7 +178,7 @@ class InstallationService:
request.custom_system_prompt or template.system_prompt 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) await self._increment_download_count(template.template_id)
@ -410,7 +472,8 @@ class InstallationService:
account_id: str, account_id: str,
config: Dict[str, Any], config: Dict[str, Any],
profile_mappings: Optional[Dict[str, str]] = None, 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: ) -> None:
triggers = config.get('triggers', []) triggers = config.get('triggers', [])
if not triggers: if not triggers:
@ -425,6 +488,17 @@ class InstallationService:
trigger_config = trigger.get('config', {}) trigger_config = trigger.get('config', {})
provider_id = trigger_config.get('provider_id', '') 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': if provider_id == 'composio':
qualified_name = trigger_config.get('qualified_name') qualified_name = trigger_config.get('qualified_name')
@ -455,7 +529,7 @@ class InstallationService:
is_active=trigger.get('is_active', True), is_active=trigger.get('is_active', True),
trigger_slug=trigger_config.get('trigger_slug', ''), trigger_slug=trigger_config.get('trigger_slug', ''),
qualified_name=qualified_name, 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, profile_mappings=profile_mappings,
trigger_profile_key=trigger_profile_key, trigger_profile_key=trigger_profile_key,
trigger_specific_config=trigger_specific_config trigger_specific_config=trigger_specific_config
@ -466,6 +540,11 @@ class InstallationService:
else: else:
failed_count += 1 failed_count += 1
else: 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_data = {
'trigger_id': str(uuid4()), 'trigger_id': str(uuid4()),
@ -474,7 +553,7 @@ class InstallationService:
'name': trigger.get('name', 'Unnamed Trigger'), 'name': trigger.get('name', 'Unnamed Trigger'),
'description': trigger.get('description'), 'description': trigger.get('description'),
'is_active': trigger.get('is_active', True), 'is_active': trigger.get('is_active', True),
'config': trigger_config.copy(), 'config': clean_config,
'created_at': datetime.now(timezone.utc).isoformat(), 'created_at': datetime.now(timezone.utc).isoformat(),
'updated_at': datetime.now(timezone.utc).isoformat() 'updated_at': datetime.now(timezone.utc).isoformat()
} }

View File

@ -1,4 +1,5 @@
import json import json
import re
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Dict, List, Any, Optional from typing import Dict, List, Any, Optional
@ -484,11 +485,25 @@ class TemplateService:
trigger_config = trigger.get('config', {}) trigger_config = trigger.get('config', {})
provider_id = trigger_config.get('provider_id', '') provider_id = trigger_config.get('provider_id', '')
agent_prompt = trigger_config.get('agent_prompt', '')
sanitized_config = { sanitized_config = {
'provider_id': provider_id, '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': if provider_id == 'schedule':
sanitized_config['cron_expression'] = trigger_config.get('cron_expression', '') sanitized_config['cron_expression'] = trigger_config.get('cron_expression', '')
sanitized_config['timezone'] = trigger_config.get('timezone', 'UTC') sanitized_config['timezone'] = trigger_config.get('timezone', 'UTC')
@ -499,7 +514,7 @@ class TemplateService:
excluded_fields = { excluded_fields = {
'profile_id', 'composio_trigger_id', 'provider_id', 'profile_id', 'composio_trigger_id', 'provider_id',
'agent_prompt', 'trigger_slug', 'qualified_name' 'agent_prompt', 'trigger_slug', 'qualified_name', 'trigger_variables'
} }
trigger_fields = {} trigger_fields = {}

View File

@ -1,4 +1,5 @@
import json import json
import re
from typing import Optional, Dict, Any, List from typing import Optional, Dict, Any, List
from core.agentpress.tool import ToolResult, openapi_schema from core.agentpress.tool import ToolResult, openapi_schema
from core.agentpress.thread_manager import ThreadManager from core.agentpress.thread_manager import ThreadManager
@ -17,6 +18,17 @@ from core.composio_integration.composio_trigger_service import ComposioTriggerSe
class TriggerTool(AgentBuilderBaseTool): class TriggerTool(AgentBuilderBaseTool):
def __init__(self, thread_manager: ThreadManager, db_connection, agent_id: str): def __init__(self, thread_manager: ThreadManager, db_connection, agent_id: str):
super().__init__(thread_manager, db_connection, agent_id) 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: async def _sync_triggers_to_version_config(self) -> None:
try: try:
@ -63,7 +75,7 @@ class TriggerTool(AgentBuilderBaseTool):
"type": "function", "type": "function",
"function": { "function": {
"name": "create_scheduled_trigger", "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": { "parameters": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -81,7 +93,7 @@ class TriggerTool(AgentBuilderBaseTool):
}, },
"agent_prompt": { "agent_prompt": {
"type": "string", "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"] "required": ["name", "cron_expression", "agent_prompt"]
@ -99,12 +111,20 @@ class TriggerTool(AgentBuilderBaseTool):
if not agent_prompt: if not agent_prompt:
return self.fail_response("agent_prompt is required") return self.fail_response("agent_prompt is required")
# Extract variables from the prompt
variables = self._extract_variables(agent_prompt)
trigger_config = { trigger_config = {
"cron_expression": cron_expression, "cron_expression": cron_expression,
"provider_id": "schedule", "provider_id": "schedule",
"agent_prompt": agent_prompt "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) trigger_svc = get_trigger_service(self.db)
try: try:
@ -120,6 +140,9 @@ class TriggerTool(AgentBuilderBaseTool):
result_message += f"**Schedule**: {cron_expression}\n" result_message += f"**Schedule**: {cron_expression}\n"
result_message += f"**Type**: Agent execution\n" result_message += f"**Type**: Agent execution\n"
result_message += f"**Prompt**: {agent_prompt}\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." result_message += f"\nThe trigger is now active and will run according to the schedule."
# Sync triggers to version config # Sync triggers to version config
@ -134,7 +157,8 @@ class TriggerTool(AgentBuilderBaseTool):
"name": trigger.name, "name": trigger.name,
"description": trigger.description, "description": trigger.description,
"cron_expression": cron_expression, "cron_expression": cron_expression,
"is_active": trigger.is_active "is_active": trigger.is_active,
"variables": variables if variables else []
} }
}) })
except ValueError as ve: except ValueError as ve:
@ -372,7 +396,7 @@ class TriggerTool(AgentBuilderBaseTool):
"type": "function", "type": "function",
"function": { "function": {
"name": "create_event_trigger", "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": { "parameters": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -380,7 +404,7 @@ class TriggerTool(AgentBuilderBaseTool):
"profile_id": {"type": "string", "description": "Composio profile_id to use (must be connected)"}, "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}, "trigger_config": {"type": "object", "description": "Trigger configuration object per trigger schema", "additionalProperties": True},
"name": {"type": "string", "description": "Optional friendly name for the trigger"}, "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"} "connected_account_id": {"type": "string", "description": "Connected account id; if omitted we try to derive from profile"}
}, },
"required": ["slug", "profile_id", "agent_prompt"] "required": ["slug", "profile_id", "agent_prompt"]
@ -399,6 +423,9 @@ class TriggerTool(AgentBuilderBaseTool):
try: try:
if not agent_prompt: if not agent_prompt:
return self.fail_response("agent_prompt is required") return self.fail_response("agent_prompt is required")
# Extract variables from the prompt
variables = self._extract_variables(agent_prompt)
# Get profile config # Get profile config
profile_service = ComposioProfileService(self.db) profile_service = ComposioProfileService(self.db)
@ -528,6 +555,11 @@ class TriggerTool(AgentBuilderBaseTool):
"agent_prompt": agent_prompt "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 # Create Suna trigger
trigger_svc = get_trigger_service(self.db) trigger_svc = get_trigger_service(self.db)
try: try:
@ -550,13 +582,17 @@ class TriggerTool(AgentBuilderBaseTool):
message = f"Event trigger '{trigger.name}' created successfully.\n" message = f"Event trigger '{trigger.name}' created successfully.\n"
message += "Agent execution configured." 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({ return self.success_response({
"message": message, "message": message,
"trigger": { "trigger": {
"provider": "composio", "provider": "composio",
"slug": slug, "slug": slug,
"is_active": trigger.is_active "is_active": trigger.is_active,
"variables": variables if variables else []
} }
}) })
except Exception as e: except Exception as e:

View File

@ -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] doc_info = all_metadata["documents"][doc_id]
try: try:
await self.sandbox.fs.remove_file(doc_info["path"]) await self.sandbox.fs.delete_file(doc_info["path"])
except: except:
pass pass
@ -509,4 +509,400 @@ IMPORTANT: All content must be wrapped in proper HTML tags. Do not use unsupport
"guide": guide, "guide": guide,
"message": "Use this guide to format HTML content for TipTap editor" "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 = """
<style>
@page {
size: A4;
margin: 1in;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #333;
background: white;
max-width: 100%;
}
.header {
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 2px solid #e5e7eb;
}
.title {
font-size: 2.5rem;
font-weight: 700;
color: #111827;
margin-bottom: 0.5rem;
}
.metadata {
display: flex;
gap: 1.5rem;
color: #6b7280;
font-size: 0.9rem;
margin-top: 0.5rem;
}
.metadata-item {
display: flex;
align-items: center;
gap: 0.25rem;
}
.tag {
display: inline-block;
padding: 0.125rem 0.5rem;
background: #eff6ff;
color: #1e40af;
border-radius: 0.25rem;
font-size: 0.875rem;
margin-right: 0.25rem;
}
.content {
margin-top: 2rem;
}
h1 {
font-size: 2rem;
font-weight: 700;
margin: 1.5rem 0 0.75rem;
color: #111827;
page-break-after: avoid;
}
h2 {
font-size: 1.5rem;
font-weight: 600;
margin: 1.25rem 0 0.625rem;
color: #374151;
page-break-after: avoid;
}
h3 {
font-size: 1.25rem;
font-weight: 600;
margin: 1rem 0 0.5rem;
color: #4b5563;
page-break-after: avoid;
}
p {
margin-bottom: 1rem;
text-align: justify;
}
ul, ol {
margin: 0.75rem 0 0.75rem 1.5rem;
page-break-inside: avoid;
}
li {
margin-bottom: 0.25rem;
}
blockquote {
border-left: 4px solid #3b82f6;
padding-left: 1rem;
margin: 1rem 0;
color: #4b5563;
font-style: italic;
background: #f9fafb;
padding: 0.75rem 1rem;
page-break-inside: avoid;
}
pre {
background: #1f2937;
color: #f3f4f6;
padding: 1rem;
margin: 1rem 0;
border-radius: 0.5rem;
overflow-x: auto;
font-family: 'Courier New', monospace;
font-size: 0.9rem;
page-break-inside: avoid;
}
code {
background: #f3f4f6;
color: #dc2626;
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
font-family: 'Courier New', monospace;
font-size: 0.9em;
}
pre code {
background: transparent;
color: inherit;
padding: 0;
}
table {
border-collapse: collapse;
width: 100%;
margin: 1rem 0;
page-break-inside: avoid;
}
th, td {
border: 1px solid #e5e7eb;
padding: 0.75rem;
text-align: left;
}
th {
background: #f9fafb;
font-weight: 600;
color: #374151;
}
img {
max-width: 100%;
height: auto;
display: block;
margin: 1rem 0;
}
a {
color: #2563eb;
text-decoration: none;
border-bottom: 1px solid transparent;
}
a:hover {
border-bottom-color: #2563eb;
}
hr {
border: none;
border-top: 1px solid #e5e7eb;
margin: 1.5rem 0;
}
.footer {
margin-top: 3rem;
padding-top: 1rem;
border-top: 1px solid #e5e7eb;
color: #6b7280;
font-size: 0.875rem;
text-align: center;
}
</style>
"""
current_time = datetime.now().strftime("%B %d, %Y at %I:%M %p")
doc_html = f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{html.escape(title)}</title>
{css_styles}
</head>
<body>
<div class="header">
<div class="title">{html.escape(title)}</div>
<div class="metadata">
<div class="metadata-item">
<span>Generated on {current_time}</span>
</div>
"""
if metadata:
if metadata.get("author"):
doc_html += f"""
<div class="metadata-item">
<span>Author: {html.escape(metadata["author"])}</span>
</div>
"""
if metadata.get("tags"):
doc_html += """
<div class="metadata-item">
<span>Tags: </span>
"""
for tag in metadata["tags"]:
doc_html += f'<span class="tag">{html.escape(tag)}</span>'
doc_html += """
</div>
"""
doc_html += f"""
</div>
</div>
<div class="content">
{content}
</div>
</body>
</html>
"""
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"<pre>{html.escape(content_str)}</pre>"
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)}")

View File

@ -5,6 +5,7 @@ from core.sandbox.tool_base import SandboxToolsBase
from core.agentpress.thread_manager import ThreadManager from core.agentpress.thread_manager import ThreadManager
from core.utils.config import config from core.utils.config import config
from core.knowledge_base.validation import FileNameValidator, ValidationError from core.knowledge_base.validation import FileNameValidator, ValidationError
from core.utils.logger import logger
class SandboxKbTool(SandboxToolsBase): class SandboxKbTool(SandboxToolsBase):
"""Tool for knowledge base operations using kb-fusion binary in a Daytona sandbox. """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" 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: async def _execute_kb_command(self, command: str) -> dict:
"""Execute a kb command with OPENAI_API_KEY environment variable set."""
await self._ensure_sandbox() await self._ensure_sandbox()
env = {"OPENAI_API_KEY": config.OPENAI_API_KEY} if config.OPENAI_API_KEY else {} env = {"OPENAI_API_KEY": config.OPENAI_API_KEY} if config.OPENAI_API_KEY else {}
response = await self.sandbox.process.exec(command, env=env) response = await self.sandbox.process.exec(command, env=env)
return { return {
@ -244,7 +243,6 @@ class SandboxKbTool(SandboxToolsBase):
async def ls_kb(self) -> ToolResult: async def ls_kb(self) -> ToolResult:
try: try:
result = await self._execute_kb_command("kb ls") result = await self._execute_kb_command("kb ls")
if result["exit_code"] != 0: if result["exit_code"] != 0:
return self.fail_response(f"List operation failed: {result['output']}") return self.fail_response(f"List operation failed: {result['output']}")

View File

@ -631,6 +631,12 @@ TOOL_GROUPS: Dict[str, ToolGroup] = {
description="Create new documents with rich text content", description="Create new documents with rich text content",
enabled=True enabled=True
), ),
ToolMethod(
name="convert_to_pdf",
display_name="Convert to PDF",
description="Convert a document to PDF format",
enabled=True
),
ToolMethod( ToolMethod(
name="read_document", name="read_document",
display_name="Read Document", display_name="Read Document",

View File

@ -333,7 +333,8 @@ export default function AgentsPage() {
instanceName?: string, instanceName?: string,
profileMappings?: Record<string, string>, profileMappings?: Record<string, string>,
customMcpConfigs?: Record<string, Record<string, any>>, customMcpConfigs?: Record<string, Record<string, any>>,
triggerConfigs?: Record<string, Record<string, any>> triggerConfigs?: Record<string, Record<string, any>>,
triggerVariables?: Record<string, Record<string, string>>
) => { ) => {
setInstallingItemId(item.id); setInstallingItemId(item.id);
@ -373,19 +374,37 @@ export default function AgentsPage() {
return; 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({ const result = await installTemplateMutation.mutateAsync({
template_id: item.template_id, template_id: item.template_id,
instance_name: instanceName, instance_name: instanceName,
profile_mappings: profileMappings, profile_mappings: profileMappings,
custom_mcp_configs: customMcpConfigs, custom_mcp_configs: customMcpConfigs,
trigger_configs: triggerConfigs trigger_configs: triggerConfigs,
trigger_variables: triggerVariables
}); });
console.log('Installation result:', result);
if (result.status === 'installed') { if (result.status === 'installed') {
toast.success(`Agent "${instanceName}" installed successfully!`); toast.success(`Agent "${instanceName}" installed successfully!`);
setShowInstallDialog(false); setShowInstallDialog(false);
handleTabChange('my-agents'); handleTabChange('my-agents');
} else if (result.status === 'configs_required') { } 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) { if (result.missing_regular_credentials && result.missing_regular_credentials.length > 0) {
const updatedRequirements = [ const updatedRequirements = [
...(item.mcp_requirements || []), ...(item.mcp_requirements || []),
@ -409,7 +428,12 @@ export default function AgentsPage() {
toast.warning('Additional configurations required. Please complete the setup.'); toast.warning('Additional configurations required. Please complete the setup.');
return; 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 { } else {
console.error('Unknown config required response:', result);
toast.error('Please provide all required configurations'); toast.error('Please provide all required configurations');
return; return;
} }

View File

@ -24,6 +24,8 @@ import { CustomServerStep } from './custom-server-step';
import type { MarketplaceTemplate, SetupStep } from './types'; import type { MarketplaceTemplate, SetupStep } from './types';
import { AgentAvatar } from '@/components/thread/content/agent-avatar'; import { AgentAvatar } from '@/components/thread/content/agent-avatar';
import { TriggerConfigStep } from './trigger-config-step'; import { TriggerConfigStep } from './trigger-config-step';
import { TriggerVariablesStep, type TriggerVariable } from './trigger-variables-step';
import { toast } from 'sonner';
interface StreamlinedInstallDialogProps { interface StreamlinedInstallDialogProps {
item: MarketplaceTemplate | null; item: MarketplaceTemplate | null;
@ -34,9 +36,11 @@ interface StreamlinedInstallDialogProps {
instanceName: string, instanceName: string,
profileMappings: Record<string, string>, profileMappings: Record<string, string>,
customMcpConfigs: Record<string, Record<string, any>>, customMcpConfigs: Record<string, Record<string, any>>,
triggerConfigs?: Record<string, Record<string, any>> triggerConfigs?: Record<string, Record<string, any>>,
triggerVariables?: Record<string, Record<string, string>>
) => Promise<void>; ) => Promise<void>;
isInstalling: boolean; isInstalling: boolean;
onTriggerVariablesRequired?: (variables: Record<string, TriggerVariable>) => void;
} }
export const StreamlinedInstallDialog: React.FC<StreamlinedInstallDialogProps> = ({ export const StreamlinedInstallDialog: React.FC<StreamlinedInstallDialogProps> = ({
@ -44,13 +48,16 @@ export const StreamlinedInstallDialog: React.FC<StreamlinedInstallDialogProps> =
open, open,
onOpenChange, onOpenChange,
onInstall, onInstall,
isInstalling isInstalling,
onTriggerVariablesRequired
}) => { }) => {
const [currentStep, setCurrentStep] = useState(0); const [currentStep, setCurrentStep] = useState(0);
const [instanceName, setInstanceName] = useState(''); const [instanceName, setInstanceName] = useState('');
const [profileMappings, setProfileMappings] = useState<Record<string, string>>({}); const [profileMappings, setProfileMappings] = useState<Record<string, string>>({});
const [customMcpConfigs, setCustomMcpConfigs] = useState<Record<string, Record<string, any>>>({}); const [customMcpConfigs, setCustomMcpConfigs] = useState<Record<string, Record<string, any>>>({});
const [triggerConfigs, setTriggerConfigs] = useState<Record<string, Record<string, any>>>({}); const [triggerConfigs, setTriggerConfigs] = useState<Record<string, Record<string, any>>>({});
const [triggerVariables, setTriggerVariables] = useState<Record<string, Record<string, string>>>({});
const [missingTriggerVariables, setMissingTriggerVariables] = useState<Record<string, TriggerVariable>>({});
const [setupSteps, setSetupSteps] = useState<SetupStep[]>([]); const [setupSteps, setSetupSteps] = useState<SetupStep[]>([]);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@ -150,10 +157,38 @@ export const StreamlinedInstallDialog: React.FC<StreamlinedInstallDialogProps> =
setProfileMappings({}); setProfileMappings({});
setCustomMcpConfigs({}); setCustomMcpConfigs({});
setTriggerConfigs({}); setTriggerConfigs({});
setTriggerVariables({});
setMissingTriggerVariables({});
setIsLoading(true); setIsLoading(true);
const steps = generateSetupSteps(); const steps = generateSetupSteps();
setSetupSteps(steps); setSetupSteps(steps);
const triggers = item.config?.triggers || [];
const triggerVars: Record<string, TriggerVariable> = {};
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); setIsLoading(false);
} }
}, [open, item, generateSetupSteps]); }, [open, item, generateSetupSteps]);
@ -220,8 +255,39 @@ export const StreamlinedInstallDialog: React.FC<StreamlinedInstallDialogProps> =
} }
}, [currentStep]); }, [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 () => { const handleInstall = useCallback(async () => {
if (!item || !instanceName.trim()) return; 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 }; const finalCustomConfigs = { ...customMcpConfigs };
setupSteps.forEach(step => { setupSteps.forEach(step => {
@ -235,8 +301,8 @@ export const StreamlinedInstallDialog: React.FC<StreamlinedInstallDialogProps> =
} }
}); });
await onInstall(item, instanceName, profileMappings, finalCustomConfigs, triggerConfigs); await onInstall(item, instanceName, profileMappings, finalCustomConfigs, triggerConfigs, triggerVariables);
}, [item, instanceName, profileMappings, customMcpConfigs, triggerConfigs, setupSteps, onInstall]); }, [item, instanceName, profileMappings, customMcpConfigs, triggerConfigs, triggerVariables, missingTriggerVariables, setupSteps, onInstall]);
const currentStepData = setupSteps[currentStep]; const currentStepData = setupSteps[currentStep];
const isOnFinalStep = currentStep >= setupSteps.length; const isOnFinalStep = currentStep >= setupSteps.length;
@ -277,17 +343,45 @@ export const StreamlinedInstallDialog: React.FC<StreamlinedInstallDialogProps> =
</div> </div>
) : (setupSteps.length === 0 || isOnFinalStep) ? ( ) : (setupSteps.length === 0 || isOnFinalStep) ? (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center gap-3"> {Object.keys(missingTriggerVariables).length > 0 ? (
<div className="h-8 w-8 rounded-full bg-green-100 dark:bg-green-900/20 flex items-center justify-center"> <>
<CheckCircle className="h-5 w-5 text-green-600 dark:text-green-400" /> <div className="flex items-center gap-3">
</div> <div className="h-8 w-8 rounded-full bg-orange-100 dark:bg-orange-900/20 flex items-center justify-center">
<div> <Zap className="h-5 w-5 text-orange-600 dark:text-orange-400" />
<h3 className="font-semibold">Ready to install!</h3> </div>
<p className="text-sm text-muted-foreground"> <div>
Give your agent a name and we'll set everything up. <h3 className="font-semibold">Customize Trigger Variables</h3>
</p> <p className="text-sm text-muted-foreground">
</div> This template has triggers with variables that need your values.
</div> </p>
</div>
</div>
<TriggerVariablesStep
triggerVariables={missingTriggerVariables}
values={triggerVariables}
onValuesChange={(triggerKey, variables) => {
setTriggerVariables(prev => ({
...prev,
[triggerKey]: variables
}));
}}
/>
</>
) : (
<>
<div className="flex items-center gap-3">
<div className="h-8 w-8 rounded-full bg-green-100 dark:bg-green-900/20 flex items-center justify-center">
<CheckCircle className="h-5 w-5 text-green-600 dark:text-green-400" />
</div>
<div>
<h3 className="font-semibold">Ready to install!</h3>
<p className="text-sm text-muted-foreground">
Give your agent a name and we'll set everything up.
</p>
</div>
</div>
</>
)}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="instance-name">Agent Name</Label> <Label htmlFor="instance-name">Agent Name</Label>
@ -397,7 +491,7 @@ export const StreamlinedInstallDialog: React.FC<StreamlinedInstallDialogProps> =
{isOnFinalStep ? ( {isOnFinalStep ? (
<Button <Button
onClick={handleInstall} onClick={handleInstall}
disabled={isInstalling || !instanceName.trim()} disabled={isInstalling || !instanceName.trim() || !areAllTriggerVariablesFilled()}
className="flex-1" className="flex-1"
> >
{isInstalling ? ( {isInstalling ? (
@ -415,7 +509,7 @@ export const StreamlinedInstallDialog: React.FC<StreamlinedInstallDialogProps> =
) : setupSteps.length === 0 ? ( ) : setupSteps.length === 0 ? (
<Button <Button
onClick={handleInstall} onClick={handleInstall}
disabled={isInstalling || !instanceName.trim()} disabled={isInstalling || !instanceName.trim() || !areAllTriggerVariablesFilled()}
className="flex-1" className="flex-1"
> >
{isInstalling ? ( {isInstalling ? (

View File

@ -0,0 +1,141 @@
import React from 'react';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Info } from 'lucide-react';
export interface TriggerVariable {
trigger_name: string;
trigger_index: number;
variables: string[];
agent_prompt: string;
missing_variables?: string[];
}
interface TriggerVariablesStepProps {
triggerVariables: Record<string, TriggerVariable>;
values: Record<string, Record<string, string>>;
onValuesChange: (triggerKey: string, variables: Record<string, string>) => void;
}
export const TriggerVariablesStep: React.FC<TriggerVariablesStepProps> = ({
triggerVariables,
values,
onValuesChange,
}) => {
const handleVariableChange = (triggerKey: string, varName: string, value: string) => {
const currentValues = values[triggerKey] || {};
onValuesChange(triggerKey, {
...currentValues,
[varName]: value
});
};
const getVariableValue = (triggerKey: string, varName: string): string => {
return values[triggerKey]?.[varName] || '';
};
const formatVariableName = (varName: string): string => {
return varName.replace(/_/g, ' ')
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ');
};
const getVariablePlaceholder = (varName: string): string => {
const lowerName = varName.toLowerCase();
if (lowerName.includes('name') || lowerName.includes('brand')) {
return 'e.g., Acme Corporation';
}
if (lowerName.includes('email')) {
return 'e.g., contact@example.com';
}
if (lowerName.includes('url') || lowerName.includes('website')) {
return 'e.g., https://example.com';
}
if (lowerName.includes('key') || lowerName.includes('token')) {
return 'Enter your API key or token';
}
return `Enter ${formatVariableName(varName)}`;
};
const showPreview = (triggerKey: string, triggerData: TriggerVariable): string => {
let preview = triggerData.agent_prompt;
const triggerValues = values[triggerKey] || {};
triggerData.variables.forEach(varName => {
const value = triggerValues[varName];
if (value) {
const pattern = new RegExp(`\\{\\{${varName}\\}\\}`, 'g');
preview = preview.replace(pattern, value);
}
});
return preview;
};
const triggers = Object.entries(triggerVariables);
if (triggers.length === 0) {
return (
<Alert>
<Info className="h-4 w-4" />
<AlertDescription>
No trigger variables require configuration.
</AlertDescription>
</Alert>
);
}
return (
<div className="space-y-6">
{triggers.map(([triggerKey, triggerData]) => {
const preview = showPreview(triggerKey, triggerData);
const hasAllValues = triggerData.variables.every(varName =>
values[triggerKey]?.[varName] && values[triggerKey][varName].trim() !== ''
);
return (
<div key={triggerKey} className="space-y-4">
<div className="space-y-1">
<h4 className="font-medium text-sm">
{triggerData.trigger_name}
</h4>
<p className="text-xs text-muted-foreground">
Customize the values for this trigger
</p>
</div>
<div className="space-y-3">
{triggerData.variables.map(varName => (
<div key={varName} className="space-y-2">
<Label htmlFor={`${triggerKey}-${varName}`}>
{formatVariableName(varName)}
<span className="text-destructive ml-1">*</span>
</Label>
<Input
id={`${triggerKey}-${varName}`}
type="text"
placeholder={getVariablePlaceholder(varName)}
value={getVariableValue(triggerKey, varName)}
onChange={(e) => handleVariableChange(triggerKey, varName, e.target.value)}
className="w-full"
/>
</div>
))}
</div>
{hasAllValues && (
<div className="p-3 bg-muted rounded-lg space-y-2">
<p className="text-xs font-medium text-muted-foreground">Preview:</p>
<p className="text-xs break-words whitespace-pre-wrap">
{preview}
</p>
</div>
)}
</div>
);
})}
</div>
);
};

View File

@ -1079,6 +1079,12 @@ export const TOOL_GROUPS: Record<string, ToolGroup> = {
description: 'Create new documents with rich text content', description: 'Create new documents with rich text content',
enabled: true, enabled: true,
}, },
{
name: 'convert_to_pdf',
displayName: 'Convert to PDF',
description: 'Convert a document to PDF format',
enabled: true,
},
{ {
name: 'read_document', name: 'read_document',
displayName: 'Read Document', displayName: 'Read Document',

View File

@ -823,7 +823,7 @@ export const EventBasedTriggerDialog: React.FC<EventBasedTriggerDialogProps> = (
placeholder="What should the agent do when this event occurs?" placeholder="What should the agent do when this event occurs?"
/> />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Use <code className="text-xs bg-muted px-1 rounded">payload</code> to include trigger data Use <code className="text-xs bg-muted px-1 rounded">{'{{variable_name}}'}</code> to add variables to the prompt
</p> </p>
</div> </div>
</div> </div>

View File

@ -1044,7 +1044,7 @@ export const SimplifiedScheduleConfig: React.FC<SimplifiedScheduleConfigProps> =
<p className="text-sm text-destructive">{errors.agent_prompt}</p> <p className="text-sm text-destructive">{errors.agent_prompt}</p>
)} )}
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Use <code className="text-xs bg-muted px-1 rounded">payload</code> to include trigger data Use <code className="text-xs bg-muted px-1 rounded">{'{{variable_name}}'}</code> to add variables to the prompt
</p> </p>
</div> </div>
</div> </div>

View File

@ -100,7 +100,8 @@ export function CustomAgentsSection({ onAgentSelect }: CustomAgentsSectionProps)
instanceName: string, instanceName: string,
profileMappings: Record<string, string>, profileMappings: Record<string, string>,
customServerConfigs: Record<string, any>, customServerConfigs: Record<string, any>,
triggerConfigs?: Record<string, Record<string, any>> triggerConfigs?: Record<string, Record<string, any>>,
triggerVariables?: Record<string, Record<string, string>>
) => { ) => {
if (!item) return; if (!item) return;
@ -113,6 +114,7 @@ export function CustomAgentsSection({ onAgentSelect }: CustomAgentsSectionProps)
profile_mappings: profileMappings, profile_mappings: profileMappings,
custom_mcp_configs: customServerConfigs, custom_mcp_configs: customServerConfigs,
trigger_configs: triggerConfigs, trigger_configs: triggerConfigs,
trigger_variables: triggerVariables,
}); });
if (result.status === 'installed' && result.instance_id) { if (result.status === 'installed' && result.instance_id) {
@ -123,6 +125,11 @@ export function CustomAgentsSection({ onAgentSelect }: CustomAgentsSectionProps)
onAgentSelect(result.instance_id); onAgentSelect(result.instance_id);
} }
} else if (result.status === 'configs_required') { } 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'); toast.error('Please provide all required configurations');
return; return;
} else { } else {

View File

@ -76,7 +76,7 @@ export interface MCPRequirement {
display_name: string; display_name: string;
enabled_tools: string[]; enabled_tools: string[];
required_config: string[]; required_config: string[];
custom_type?: 'sse' | 'http'; // For custom MCP servers custom_type?: 'sse' | 'http';
} }
export interface InstallTemplateRequest { export interface InstallTemplateRequest {
@ -86,11 +86,13 @@ export interface InstallTemplateRequest {
profile_mappings?: Record<string, string>; profile_mappings?: Record<string, string>;
custom_mcp_configs?: Record<string, Record<string, any>>; custom_mcp_configs?: Record<string, Record<string, any>>;
trigger_configs?: Record<string, Record<string, any>>; trigger_configs?: Record<string, Record<string, any>>;
trigger_variables?: Record<string, Record<string, string>>;
} }
export interface InstallationResponse { export interface InstallationResponse {
status: 'installed' | 'configs_required'; status: 'installed' | 'configs_required';
instance_id?: string; instance_id?: string;
name?: string;
missing_regular_credentials?: { missing_regular_credentials?: {
qualified_name: string; qualified_name: string;
display_name: string; display_name: string;
@ -102,6 +104,13 @@ export interface InstallationResponse {
custom_type: string; custom_type: string;
required_config: string[]; required_config: string[];
}[]; }[];
missing_trigger_variables?: Record<string, {
trigger_name: string;
trigger_index: number;
variables: string[];
agent_prompt: string;
missing_variables?: string[];
}>;
template?: { template?: {
template_id: string; template_id: string;
name: string; name: string;
@ -116,10 +125,6 @@ export interface CreateTemplateRequest {
usage_examples?: UsageExampleMessage[]; usage_examples?: UsageExampleMessage[];
} }
// =====================================================
// CREDENTIAL MANAGEMENT HOOKS
// =====================================================
export function useUserCredentials() { export function useUserCredentials() {
return useQuery({ return useQuery({
queryKey: ['secure-mcp', 'credentials'], queryKey: ['secure-mcp', 'credentials'],