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
if finish_reason == "xml_tool_limit_reached":
logger.info("XML tool call limit reached, continuing stream to capture usage data")
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"))
# Don't break - continue processing stream to capture final usage chunk
logger.info("Stopping stream processing after loop due to XML tool call limit")
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"))
break
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
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
)

View File

@ -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()
}

View File

@ -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 = {}

View File

@ -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:

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]
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)}")

View File

@ -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']}")

View File

@ -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",

View File

@ -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;
}

View File

@ -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,6 +343,32 @@ export const StreamlinedInstallDialog: React.FC<StreamlinedInstallDialogProps> =
</div>
) : (setupSteps.length === 0 || isOnFinalStep) ? (
<div className="space-y-6">
{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" />
@ -288,6 +380,8 @@ export const StreamlinedInstallDialog: React.FC<StreamlinedInstallDialogProps> =
</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 ? (

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',
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',

View File

@ -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>

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-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>

View File

@ -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 {

View File

@ -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'],