mirror of https://github.com/kortix-ai/suna.git
Compare commits
3 Commits
98ca19b6c3
...
3ed60b291a
Author | SHA1 | Date |
---|---|---|
|
3ed60b291a | |
|
d3f5d4fec8 | |
|
014e6cf222 |
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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 = {}
|
||||
|
|
|
@ -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
|
||||
|
@ -18,6 +19,17 @@ 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:
|
||||
client = await self.db.client
|
||||
|
@ -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"]
|
||||
|
@ -400,6 +424,9 @@ 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)
|
||||
|
||||
# Get profile config
|
||||
profile_service = ComposioProfileService(self.db)
|
||||
try:
|
||||
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -510,3 +510,399 @@ IMPORTANT: All content must be wrapped in proper HTML tags. Do not use unsupport
|
|||
"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)}")
|
||||
|
|
@ -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']}")
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -333,7 +333,8 @@ export default function AgentsPage() {
|
|||
instanceName?: string,
|
||||
profileMappings?: Record<string, string>,
|
||||
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);
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<string, string>,
|
||||
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>;
|
||||
isInstalling: boolean;
|
||||
onTriggerVariablesRequired?: (variables: Record<string, TriggerVariable>) => void;
|
||||
}
|
||||
|
||||
export const StreamlinedInstallDialog: React.FC<StreamlinedInstallDialogProps> = ({
|
||||
|
@ -44,13 +48,16 @@ export const StreamlinedInstallDialog: React.FC<StreamlinedInstallDialogProps> =
|
|||
open,
|
||||
onOpenChange,
|
||||
onInstall,
|
||||
isInstalling
|
||||
isInstalling,
|
||||
onTriggerVariablesRequired
|
||||
}) => {
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [instanceName, setInstanceName] = useState('');
|
||||
const [profileMappings, setProfileMappings] = useState<Record<string, string>>({});
|
||||
const [customMcpConfigs, setCustomMcpConfigs] = 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 [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
|
@ -150,10 +157,38 @@ export const StreamlinedInstallDialog: React.FC<StreamlinedInstallDialogProps> =
|
|||
setProfileMappings({});
|
||||
setCustomMcpConfigs({});
|
||||
setTriggerConfigs({});
|
||||
setTriggerVariables({});
|
||||
setMissingTriggerVariables({});
|
||||
setIsLoading(true);
|
||||
|
||||
const steps = generateSetupSteps();
|
||||
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);
|
||||
}
|
||||
}, [open, item, generateSetupSteps]);
|
||||
|
@ -220,8 +255,39 @@ export const StreamlinedInstallDialog: React.FC<StreamlinedInstallDialogProps> =
|
|||
}
|
||||
}, [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<StreamlinedInstallDialogProps> =
|
|||
}
|
||||
});
|
||||
|
||||
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<StreamlinedInstallDialogProps> =
|
|||
</div>
|
||||
) : (setupSteps.length === 0 || isOnFinalStep) ? (
|
||||
<div className="space-y-6">
|
||||
<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>
|
||||
{Object.keys(missingTriggerVariables).length > 0 ? (
|
||||
<>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-8 w-8 rounded-full bg-orange-100 dark:bg-orange-900/20 flex items-center justify-center">
|
||||
<Zap className="h-5 w-5 text-orange-600 dark:text-orange-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold">Customize Trigger Variables</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This template has triggers with variables that need your values.
|
||||
</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">
|
||||
<Label htmlFor="instance-name">Agent Name</Label>
|
||||
|
@ -397,7 +491,7 @@ export const StreamlinedInstallDialog: React.FC<StreamlinedInstallDialogProps> =
|
|||
{isOnFinalStep ? (
|
||||
<Button
|
||||
onClick={handleInstall}
|
||||
disabled={isInstalling || !instanceName.trim()}
|
||||
disabled={isInstalling || !instanceName.trim() || !areAllTriggerVariablesFilled()}
|
||||
className="flex-1"
|
||||
>
|
||||
{isInstalling ? (
|
||||
|
@ -415,7 +509,7 @@ export const StreamlinedInstallDialog: React.FC<StreamlinedInstallDialogProps> =
|
|||
) : setupSteps.length === 0 ? (
|
||||
<Button
|
||||
onClick={handleInstall}
|
||||
disabled={isInstalling || !instanceName.trim()}
|
||||
disabled={isInstalling || !instanceName.trim() || !areAllTriggerVariablesFilled()}
|
||||
className="flex-1"
|
||||
>
|
||||
{isInstalling ? (
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -1079,6 +1079,12 @@ export const TOOL_GROUPS: Record<string, ToolGroup> = {
|
|||
description: 'Create new documents with rich text content',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'convert_to_pdf',
|
||||
displayName: 'Convert to PDF',
|
||||
description: 'Convert a document to PDF format',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'read_document',
|
||||
displayName: 'Read Document',
|
||||
|
|
|
@ -823,7 +823,7 @@ export const EventBasedTriggerDialog: React.FC<EventBasedTriggerDialogProps> = (
|
|||
placeholder="What should the agent do when this event occurs?"
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1044,7 +1044,7 @@ export const SimplifiedScheduleConfig: React.FC<SimplifiedScheduleConfigProps> =
|
|||
<p className="text-sm text-destructive">{errors.agent_prompt}</p>
|
||||
)}
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -100,7 +100,8 @@ export function CustomAgentsSection({ onAgentSelect }: CustomAgentsSectionProps)
|
|||
instanceName: string,
|
||||
profileMappings: Record<string, string>,
|
||||
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;
|
||||
|
||||
|
@ -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 {
|
||||
|
|
|
@ -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<string, string>;
|
||||
custom_mcp_configs?: Record<string, Record<string, any>>;
|
||||
trigger_configs?: Record<string, Record<string, any>>;
|
||||
trigger_variables?: Record<string, Record<string, string>>;
|
||||
}
|
||||
|
||||
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<string, {
|
||||
trigger_name: string;
|
||||
trigger_index: number;
|
||||
variables: string[];
|
||||
agent_prompt: string;
|
||||
missing_variables?: string[];
|
||||
}>;
|
||||
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'],
|
||||
|
|
Loading…
Reference in New Issue