Merge pull request #1781 from escapade-mckv/fix-trigger-installation

Fix trigger installation
This commit is contained in:
Bobbie 2025-10-07 16:06:17 +05:30 committed by GitHub
commit 13f32c9e85
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 616 additions and 187 deletions

View File

@ -120,6 +120,7 @@ async def update_agent(
"version_number": 1,
"version_name": "v1",
"system_prompt": existing_data.get('system_prompt', ''),
"model": existing_data.get('model'),
"configured_mcps": existing_data.get('configured_mcps', []),
"custom_mcps": existing_data.get('custom_mcps', []),
"agentpress_tools": existing_data.get('agentpress_tools', {}),
@ -150,6 +151,7 @@ async def update_agent(
else:
current_version_data = {
'system_prompt': existing_data.get('system_prompt', ''),
'model': existing_data.get('model'),
'configured_mcps': existing_data.get('configured_mcps', []),
'custom_mcps': existing_data.get('custom_mcps', []),
'agentpress_tools': existing_data.get('agentpress_tools', {})
@ -158,6 +160,7 @@ async def update_agent(
logger.warning(f"Failed to create initial version for agent {agent_id}: {e}")
current_version_data = {
'system_prompt': existing_data.get('system_prompt', ''),
'model': existing_data.get('model'),
'configured_mcps': existing_data.get('configured_mcps', []),
'custom_mcps': existing_data.get('custom_mcps', []),
'agentpress_tools': existing_data.get('agentpress_tools', {})
@ -181,6 +184,10 @@ async def update_agent(
needs_new_version = True
version_changes['system_prompt'] = agent_data.system_prompt
if values_different(agent_data.model, current_version_data.get('model')):
needs_new_version = True
version_changes['model'] = agent_data.model
if values_different(agent_data.configured_mcps, current_version_data.get('configured_mcps', [])):
needs_new_version = True
version_changes['configured_mcps'] = agent_data.configured_mcps
@ -220,6 +227,7 @@ async def update_agent(
print(f"[DEBUG] update_agent: Prepared update_data with icon fields - icon_name={update_data.get('icon_name')}, icon_color={update_data.get('icon_color')}, icon_background={update_data.get('icon_background')}")
current_system_prompt = agent_data.system_prompt if agent_data.system_prompt is not None else current_version_data.get('system_prompt', '')
current_model = agent_data.model if agent_data.model is not None else current_version_data.get('model')
if agent_data.configured_mcps is not None:
if agent_data.replace_mcps:
@ -257,6 +265,7 @@ async def update_agent(
agent_id=agent_id,
user_id=user_id,
system_prompt=current_system_prompt,
model=current_model,
configured_mcps=current_configured_mcps,
custom_mcps=current_custom_mcps,
agentpress_tools=current_agentpress_tools,

View File

@ -25,6 +25,7 @@ class AgentUpdateRequest(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
system_prompt: Optional[str] = None
model: Optional[str] = None
configured_mcps: Optional[List[Dict[str, Any]]] = None
custom_mcps: Optional[List[Dict[str, Any]]] = None
agentpress_tools: Optional[Dict[str, Any]] = None

View File

@ -23,6 +23,7 @@ from .composio_service import (
from .toolkit_service import ToolkitService, ToolsListResponse
from .composio_profile_service import ComposioProfileService, ComposioProfile
from .composio_trigger_service import ComposioTriggerService
from .trigger_schema import TriggerSchemaService
from core.triggers.trigger_service import get_trigger_service, TriggerEvent, TriggerType
from core.triggers.execution_service import get_execution_service
from .client import ComposioClient
@ -643,6 +644,22 @@ async def list_triggers_for_app(
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/triggers/schema/{trigger_slug}")
async def get_trigger_schema(
trigger_slug: str,
user_id: str = Depends(verify_and_get_user_id_from_jwt),
) -> Dict[str, Any]:
try:
schema_service = TriggerSchemaService()
schema = await schema_service.get_trigger_schema(trigger_slug)
return schema
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to get trigger schema for {trigger_slug}: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error")
class CreateComposioTriggerRequest(BaseModel):
agent_id: str
profile_id: str

View File

@ -0,0 +1,47 @@
import os
import httpx
from typing import Dict, Any, Optional
from fastapi import HTTPException
from core.utils.logger import logger
class TriggerSchemaService:
def __init__(self):
self.api_base = os.getenv("COMPOSIO_API_BASE", "https://backend.composio.dev").rstrip("/")
self.api_key = os.getenv("COMPOSIO_API_KEY")
async def get_trigger_schema(self, trigger_slug: str) -> Dict[str, Any]:
if not self.api_key:
raise HTTPException(status_code=500, detail="COMPOSIO_API_KEY not configured")
try:
headers = {"x-api-key": self.api_key}
url = f"{self.api_base}/api/v3/triggers_types/{trigger_slug}"
async with httpx.AsyncClient(timeout=10) as http_client:
response = await http_client.get(url, headers=headers)
if response.status_code == 404:
raise HTTPException(status_code=404, detail=f"Trigger {trigger_slug} not found")
elif response.status_code != 200:
raise HTTPException(status_code=response.status_code,
detail=f"Failed to fetch trigger schema: {response.text}")
data = response.json()
return {
"slug": trigger_slug,
"name": data.get("name", trigger_slug),
"description": data.get("description"),
"config": data.get("config", {}),
"app": data.get("app"),
}
except httpx.TimeoutException:
logger.error(f"Timeout fetching trigger schema for {trigger_slug}")
raise HTTPException(status_code=504, detail="Timeout fetching trigger schema")
except httpx.HTTPError as e:
logger.error(f"HTTP error fetching trigger schema: {e}")
raise HTTPException(status_code=500, detail="Failed to fetch trigger schema")
except Exception as e:
logger.error(f"Unexpected error fetching trigger schema: {e}")
raise HTTPException(status_code=500, detail="Internal server error")

View File

@ -47,6 +47,7 @@ class InstallTemplateRequest(BaseModel):
custom_system_prompt: Optional[str] = None
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
class PublishTemplateRequest(BaseModel):
@ -74,7 +75,7 @@ class TemplateResponse(BaseModel):
metadata: Dict[str, Any]
creator_name: Optional[str] = None
usage_examples: Optional[List[UsageExampleMessage]] = None
config: Optional[Dict[str, Any]] = None
class InstallationResponse(BaseModel):
status: str
@ -313,13 +314,16 @@ async def install_template(
installation_service = get_installation_service(db)
logger.info(f"Installing template with trigger_configs: {request.trigger_configs}")
install_request = TemplateInstallationRequest(
template_id=request.template_id,
account_id=user_id,
instance_name=request.instance_name,
custom_system_prompt=request.custom_system_prompt,
profile_mappings=request.profile_mappings,
custom_mcp_configs=request.custom_mcp_configs
custom_mcp_configs=request.custom_mcp_configs,
trigger_configs=request.trigger_configs
)
result = await installation_service.install_template(install_request)

View File

@ -3,6 +3,7 @@ from datetime import datetime, timezone
from typing import Dict, List, Any, Optional
from uuid import uuid4
import os
import json
import httpx
from core.services.supabase import DBConnection
@ -32,6 +33,7 @@ class TemplateInstallationRequest:
custom_system_prompt: Optional[str] = None
profile_mappings: Optional[Dict[QualifiedName, ProfileId]] = None
custom_mcp_configs: Optional[Dict[QualifiedName, ConfigType]] = None
trigger_configs: Optional[Dict[str, Dict[str, Any]]] = None
@dataclass
class TemplateInstallationResult:
@ -114,7 +116,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)
await self._restore_triggers(agent_id, request.account_id, template.config, request.profile_mappings, request.trigger_configs)
await self._increment_download_count(template.template_id)
@ -407,7 +409,8 @@ class InstallationService:
agent_id: str,
account_id: str,
config: Dict[str, Any],
profile_mappings: Optional[Dict[str, str]] = None
profile_mappings: Optional[Dict[str, str]] = None,
trigger_configs: Optional[Dict[str, Dict[str, Any]]] = None
) -> None:
triggers = config.get('triggers', [])
if not triggers:
@ -427,6 +430,23 @@ class InstallationService:
trigger_profile_key = f"{qualified_name}_trigger_{i}"
trigger_specific_config = {}
if trigger_configs and trigger_profile_key in trigger_configs:
trigger_specific_config = trigger_configs[trigger_profile_key].copy()
logger.info(f"Using user-provided trigger config for {trigger_profile_key}: {trigger_specific_config}")
else:
logger.info(f"No user trigger config found for key {trigger_profile_key}. Available keys: {list(trigger_configs.keys()) if trigger_configs else 'None'}")
metadata_fields = {
'provider_id', 'qualified_name', 'trigger_slug',
'agent_prompt', 'profile_id', 'composio_trigger_id',
'trigger_fields'
}
for key, value in trigger_config.items():
if key not in metadata_fields and key not in trigger_specific_config:
trigger_specific_config[key] = value
success = await self._create_composio_trigger(
agent_id=agent_id,
account_id=account_id,
@ -437,7 +457,8 @@ class InstallationService:
qualified_name=qualified_name,
agent_prompt=trigger_config.get('agent_prompt'),
profile_mappings=profile_mappings,
trigger_profile_key=trigger_profile_key
trigger_profile_key=trigger_profile_key,
trigger_specific_config=trigger_specific_config
)
if success:
@ -522,7 +543,8 @@ class InstallationService:
qualified_name: Optional[str],
agent_prompt: Optional[str],
profile_mappings: Dict[str, str],
trigger_profile_key: Optional[str] = None
trigger_profile_key: Optional[str] = None,
trigger_specific_config: Optional[Dict[str, Any]] = None
) -> bool:
try:
if not trigger_slug:
@ -593,30 +615,28 @@ class InstallationService:
if vercel_bypass:
webhook_headers["X-Vercel-Protection-Bypass"] = vercel_bypass
logger.info(f"Creating trigger {trigger_slug} with config: {trigger_specific_config}")
body = {
"user_id": composio_user_id,
"userId": composio_user_id,
"trigger_config": {},
"triggerConfig": {},
"webhook": {
"url": f"{base_url}/api/composio/webhook",
"headers": webhook_headers,
"method": "POST",
},
"trigger_config": trigger_specific_config or {},
}
if connected_account_id:
body["connectedAccountId"] = connected_account_id
body["connected_account_id"] = connected_account_id
body["connectedAccountIds"] = [connected_account_id]
body["connected_account_ids"] = [connected_account_id]
logger.debug(f"Adding connected_account_id to Composio trigger request: {connected_account_id}")
else:
logger.warning("No connected_account_id found - trigger creation may fail for OAuth apps")
logger.debug(f"Creating Composio trigger with URL: {url}")
logger.debug(f"Request body: {json.dumps(body, indent=2)}")
async with httpx.AsyncClient(timeout=20) as http_client:
resp = await http_client.post(url, headers=headers, json=body)
if resp.status_code != 200:
logger.error(f"Composio API error response: {resp.status_code} - {resp.text}")
resp.raise_for_status()
created = resp.json()
def _extract_id(obj: Dict[str, Any]) -> Optional[str]:
@ -657,6 +677,10 @@ class InstallationService:
"profile_id": profile_id,
"provider_id": "composio"
}
if trigger_specific_config:
config.update(trigger_specific_config)
if agent_prompt:
config["agent_prompt"] = agent_prompt

View File

@ -496,6 +496,28 @@ class TemplateService:
sanitized_config['trigger_slug'] = trigger_config.get('trigger_slug', '')
if 'qualified_name' in trigger_config:
sanitized_config['qualified_name'] = trigger_config['qualified_name']
excluded_fields = {
'profile_id', 'composio_trigger_id', 'provider_id',
'agent_prompt', 'trigger_slug', 'qualified_name'
}
trigger_fields = {}
for key, value in trigger_config.items():
if key not in excluded_fields:
if isinstance(value, bool):
trigger_fields[key] = {'type': 'boolean', 'required': True}
elif isinstance(value, (int, float)):
trigger_fields[key] = {'type': 'number', 'required': True}
elif isinstance(value, list):
trigger_fields[key] = {'type': 'array', 'required': True}
elif isinstance(value, dict):
trigger_fields[key] = {'type': 'object', 'required': True}
else:
trigger_fields[key] = {'type': 'string', 'required': True}
if trigger_fields:
sanitized_config['trigger_fields'] = trigger_fields
sanitized_trigger = {
'name': trigger.get('name'),

View File

@ -134,7 +134,8 @@ def format_template_for_response(template: AgentTemplate) -> Dict[str, Any]:
'icon_background': template.icon_background,
'metadata': template.metadata,
'creator_name': template.creator_name,
'usage_examples': template.usage_examples
'usage_examples': template.usage_examples,
'config': template.config,
}
logger.debug(f"Response for {template.template_id} includes usage_examples: {response.get('usage_examples')}")

View File

@ -211,6 +211,7 @@ export default function AgentsPage() {
mcp_requirements: template.mcp_requirements,
metadata: template.metadata,
usage_examples: template.usage_examples,
config: template.config,
};
items.push(item);
@ -331,7 +332,8 @@ export default function AgentsPage() {
item: MarketplaceTemplate,
instanceName?: string,
profileMappings?: Record<string, string>,
customMcpConfigs?: Record<string, Record<string, any>>
customMcpConfigs?: Record<string, Record<string, any>>,
triggerConfigs?: Record<string, Record<string, any>>
) => {
setInstallingItemId(item.id);
@ -375,7 +377,8 @@ export default function AgentsPage() {
template_id: item.template_id,
instance_name: instanceName,
profile_mappings: profileMappings,
custom_mcp_configs: customMcpConfigs
custom_mcp_configs: customMcpConfigs,
trigger_configs: triggerConfigs
});
if (result.status === 'installed') {

View File

@ -140,7 +140,7 @@ export function AgentConfigurationDialog({
const newFormData = {
name: configSource.name || '',
system_prompt: configSource.system_prompt || '',
model: configSource.model,
model: configSource.model || undefined,
agentpress_tools: ensureCoreToolsEnabled(configSource.agentpress_tools || DEFAULT_AGENTPRESS_TOOLS),
configured_mcps: configSource.configured_mcps || [],
custom_mcps: configSource.custom_mcps || [],
@ -177,7 +177,7 @@ export function AgentConfigurationDialog({
agentpress_tools: formData.agentpress_tools,
};
if (formData.model !== undefined) updateData.model = formData.model;
if (formData.model !== undefined && formData.model !== null) updateData.model = formData.model;
if (formData.icon_name !== undefined) updateData.icon_name = formData.icon_name;
if (formData.icon_color !== undefined) updateData.icon_color = formData.icon_color;
if (formData.icon_background !== undefined) updateData.icon_background = formData.icon_background;
@ -254,7 +254,7 @@ export function AgentConfigurationDialog({
};
const handleModelChange = (model: string) => {
setFormData(prev => ({ ...prev, model }));
setFormData(prev => ({ ...prev, model: model || undefined }));
};
const handleToolsChange = (tools: Record<string, boolean | { enabled: boolean; description: string }>) => {

View File

@ -36,24 +36,35 @@ export const CustomServerStep: React.FC<CustomServerStepProps> = ({
</div>
)}
<div className="space-y-4">
{step.required_fields?.map((field) => (
<div key={field.key} className="space-y-2">
<Input
id={field.key}
type={field.type}
placeholder={field.placeholder}
value={config[field.key] || ''}
onChange={(e) => handleFieldChange(field.key, e.target.value)}
className="h-11"
/>
{field.description && (
<div className="flex items-start gap-2">
<Info className="h-3 w-3 text-muted-foreground mt-0.5 flex-shrink-0" />
<p className="text-xs text-muted-foreground">{field.description}</p>
</div>
)}
</div>
))}
{step.required_config?.map(key => {
const label = key === 'url' ? `${step.service_name} Server URL` : key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
const type = key === 'url' ? 'url' : 'text';
const placeholder = key === 'url' ? `https://your-${step.service_name.toLowerCase()}-server.com` : `Enter ${key}`;
const description = key === 'url' ? `Your personal ${step.service_name} server endpoint` : undefined;
return (
<div key={key} className="space-y-2">
<Label htmlFor={key}>
{label}
<span className="text-destructive ml-1">*</span>
</Label>
<Input
id={key}
type={type}
placeholder={placeholder}
value={config[key] || ''}
onChange={(e) => handleFieldChange(key, e.target.value)}
className="h-11"
/>
{description && (
<div className="flex items-start gap-2">
<Info className="h-3 w-3 text-muted-foreground mt-0.5 flex-shrink-0" />
<p className="text-xs text-muted-foreground">{description}</p>
</div>
)}
</div>
);
})}
</div>
</div>
);

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
@ -23,6 +23,7 @@ import { ProfileConnector } from './streamlined-profile-connector';
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';
interface StreamlinedInstallDialogProps {
item: MarketplaceTemplate | null;
@ -32,7 +33,8 @@ interface StreamlinedInstallDialogProps {
item: MarketplaceTemplate,
instanceName: string,
profileMappings: Record<string, string>,
customMcpConfigs: Record<string, Record<string, any>>
customMcpConfigs: Record<string, Record<string, any>>,
triggerConfigs?: Record<string, Record<string, any>>
) => Promise<void>;
isInstalling: boolean;
}
@ -48,6 +50,7 @@ export const StreamlinedInstallDialog: React.FC<StreamlinedInstallDialogProps> =
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 [setupSteps, setSetupSteps] = useState<SetupStep[]>([]);
const [isLoading, setIsLoading] = useState(false);
@ -55,6 +58,7 @@ export const StreamlinedInstallDialog: React.FC<StreamlinedInstallDialogProps> =
if (!item?.mcp_requirements) return [];
const steps: SetupStep[] = [];
const triggers = item.config?.triggers || [];
item.mcp_requirements
.filter(req => {
@ -71,10 +75,18 @@ export const StreamlinedInstallDialog: React.FC<StreamlinedInstallDialogProps> =
? `${req.qualified_name}_trigger_${req.trigger_index}`
: req.qualified_name;
const trigger = req.source === 'trigger' && req.trigger_index !== undefined
? triggers[req.trigger_index] : null;
const triggerSlug = trigger?.config?.trigger_slug;
const triggerFields = req.source === 'trigger' ? trigger?.config?.trigger_fields : undefined;
steps.push({
id: stepId,
title: req.source === 'trigger' ? req.display_name : `Connect ${req.display_name}`,
description: req.source === 'trigger'
description: req.source === 'trigger' && triggerFields
? `Select a ${req.display_name.split(' (')[0]} profile and configure trigger settings`
: req.source === 'trigger'
? `Select a ${req.display_name.split(' (')[0]} profile for this trigger`
: `Select an existing ${req.display_name} profile or create a new one`,
type: 'composio_profile',
@ -82,7 +94,10 @@ export const StreamlinedInstallDialog: React.FC<StreamlinedInstallDialogProps> =
qualified_name: req.qualified_name,
app_slug: app_slug === 'composio' ? 'composio' : app_slug,
app_name: req.display_name,
source: req.source
source: req.source,
trigger_slug: triggerSlug,
trigger_index: req.trigger_index,
trigger_fields: triggerFields || undefined
});
});
@ -121,13 +136,7 @@ export const StreamlinedInstallDialog: React.FC<StreamlinedInstallDialogProps> =
service_name: req.display_name,
qualified_name: req.qualified_name,
custom_type: req.custom_type,
required_fields: req.required_config?.map(key => ({
key,
label: key === 'url' ? `${req.display_name} Server URL` : key,
type: key === 'url' ? 'url' : 'text',
placeholder: key === 'url' ? `https://your-${req.display_name.toLowerCase()}-server.com` : `Enter your ${key}`,
description: key === 'url' ? `Your personal ${req.display_name} server endpoint` : undefined
})) || []
required_config: req.required_config || []
});
});
@ -140,6 +149,7 @@ export const StreamlinedInstallDialog: React.FC<StreamlinedInstallDialogProps> =
setInstanceName(item.name);
setProfileMappings({});
setCustomMcpConfigs({});
setTriggerConfigs({});
setIsLoading(true);
const steps = generateSetupSteps();
@ -152,10 +162,10 @@ export const StreamlinedInstallDialog: React.FC<StreamlinedInstallDialogProps> =
setInstanceName(e.target.value);
}, []);
const handleProfileSelect = useCallback((qualifiedName: string, profileId: string | null) => {
const handleProfileSelect = useCallback((stepId: string, profileId: string | null) => {
setProfileMappings(prev => ({
...prev,
[qualifiedName]: profileId || ''
[stepId]: profileId || ''
}));
}, []);
@ -166,6 +176,13 @@ export const StreamlinedInstallDialog: React.FC<StreamlinedInstallDialogProps> =
}));
}, []);
const handleTriggerConfigUpdate = useCallback((stepId: string, config: Record<string, any>) => {
setTriggerConfigs(prev => ({
...prev,
[stepId]: config
}));
}, []);
const isCurrentStepComplete = useCallback((): boolean => {
if (setupSteps.length === 0) return true;
if (currentStep >= setupSteps.length) return !!instanceName.trim();
@ -175,13 +192,17 @@ export const StreamlinedInstallDialog: React.FC<StreamlinedInstallDialogProps> =
switch (step.type) {
case 'credential_profile':
case 'composio_profile':
return !!profileMappings[step.qualified_name];
const hasProfile = !!profileMappings[step.id];
if (!hasProfile) return false;
return true;
case 'custom_server':
const config = customMcpConfigs[step.qualified_name] || {};
return step.required_fields?.every(field => {
const value = config[field.key];
if (!step.required_config || step.required_config.length === 0) return true;
return step.required_config.every(key => {
const value = config[key];
return value && value.toString().trim().length > 0;
}) || false;
});
default:
return false;
}
@ -214,128 +235,14 @@ export const StreamlinedInstallDialog: React.FC<StreamlinedInstallDialogProps> =
}
});
await onInstall(item, instanceName, profileMappings, finalCustomConfigs);
}, [item, instanceName, profileMappings, customMcpConfigs, setupSteps, onInstall]);
await onInstall(item, instanceName, profileMappings, finalCustomConfigs, triggerConfigs);
}, [item, instanceName, profileMappings, customMcpConfigs, triggerConfigs, setupSteps, onInstall]);
const currentStepData = setupSteps[currentStep];
const isOnFinalStep = currentStep >= setupSteps.length;
if (!item) return null;
const StepContent = () => {
if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
<div className="flex items-center gap-2">
<Loader2 className="h-5 w-5 animate-spin" />
<span className="text-sm">Preparing installation...</span>
</div>
</div>
);
}
if (setupSteps.length === 0 || isOnFinalStep) {
return (
<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>
<div className="space-y-2">
<Label htmlFor="instance-name">Agent Name</Label>
<Input
id="instance-name"
placeholder="Enter a name for this agent"
value={instanceName}
onChange={handleInstanceNameChange}
className="h-11"
/>
</div>
</div>
);
}
return (
<div className="space-y-6">
<div className="space-y-2">
<div className="flex items-center gap-2">
<div className="h-8 w-8 rounded-full bg-primary/10 text-primary flex items-center justify-center">
{currentStepData.source === 'trigger' ? (
<Zap className="h-4 w-4" />
) : (
<Shield className="h-4 w-4" />
)}
</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<h3 className="font-semibold">{currentStepData.title}</h3>
{currentStepData.source === 'trigger' && (
<Badge variant="secondary" className="text-xs text-white">
<Zap className="h-3 w-3" />
For Triggers
</Badge>
)}
</div>
<p className="text-sm text-muted-foreground">
{currentStepData.description}
</p>
</div>
</div>
</div>
<div>
{(currentStepData.type === 'credential_profile' || currentStepData.type === 'composio_profile') && (
<ProfileConnector
step={currentStepData}
selectedProfileId={profileMappings[currentStepData.id]}
onProfileSelect={handleProfileSelect}
onComplete={() => {
if (currentStep < setupSteps.length - 1) {
setTimeout(() => setCurrentStep(currentStep + 1), 500);
}
}}
/>
)}
{currentStepData.type === 'custom_server' && (
<CustomServerStep
step={currentStepData}
config={customMcpConfigs[currentStepData.qualified_name] || {}}
onConfigUpdate={handleCustomConfigUpdate}
/>
)}
</div>
{setupSteps.length > 1 && (
<div className="space-y-2">
<div className="flex items-center gap-2">
{setupSteps.map((_, index) => (
<div
key={index}
className={cn(
"h-1 flex-1 rounded-full transition-colors",
index <= currentStep ? 'bg-primary' : 'bg-muted'
)}
/>
))}
</div>
<p className="text-xs text-muted-foreground">
Step {currentStep + 1} of {setupSteps.length}
</p>
</div>
)}
</div>
);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-lg max-h-[90vh] overflow-y-auto">
@ -360,9 +267,124 @@ export const StreamlinedInstallDialog: React.FC<StreamlinedInstallDialogProps> =
</div>
</div>
</DialogHeader>
<div className="mt-6">
<StepContent />
{isLoading ? (
<div className="flex items-center justify-center py-12">
<div className="flex items-center gap-2">
<Loader2 className="h-5 w-5 animate-spin" />
<span className="text-sm">Preparing installation...</span>
</div>
</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>
<div className="space-y-2">
<Label htmlFor="instance-name">Agent Name</Label>
<Input
id="instance-name"
placeholder="Enter a name for this agent"
value={instanceName}
onChange={handleInstanceNameChange}
className="h-11"
/>
</div>
</div>
) : (
<div className="space-y-6">
<div className="space-y-2">
<div className="flex items-center gap-2">
<div className="h-8 w-8 rounded-full bg-primary/10 text-primary flex items-center justify-center">
{currentStepData.source === 'trigger' ? (
<Zap className="h-4 w-4" />
) : (
<Shield className="h-4 w-4" />
)}
</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<h3 className="font-semibold">{currentStepData.title}</h3>
{currentStepData.source === 'trigger' && (
<Badge variant="secondary" className="text-xs text-white">
<Zap className="h-3 w-3" />
For Triggers
</Badge>
)}
</div>
<p className="text-sm text-muted-foreground">
{currentStepData.description}
</p>
</div>
</div>
</div>
<div>
{(currentStepData.type === 'credential_profile' || currentStepData.type === 'composio_profile') && (
<>
<ProfileConnector
step={currentStepData}
selectedProfileId={profileMappings[currentStepData.id]}
onProfileSelect={handleProfileSelect}
onComplete={() => {
if (currentStep < setupSteps.length - 1) {
setTimeout(() => setCurrentStep(currentStep + 1), 500);
}
}}
/>
{currentStepData.trigger_fields && profileMappings[currentStepData.id] && (
<div className="mt-4">
<TriggerConfigStep
step={currentStepData}
profileId={profileMappings[currentStepData.id]}
config={triggerConfigs[currentStepData.id] || {}}
onProfileSelect={handleProfileSelect}
onConfigUpdate={handleTriggerConfigUpdate}
/>
</div>
)}
</>
)}
{currentStepData.type === 'custom_server' && (
<CustomServerStep
step={currentStepData}
config={customMcpConfigs[currentStepData.qualified_name] || {}}
onConfigUpdate={handleCustomConfigUpdate}
/>
)}
</div>
{setupSteps.length > 1 && (
<div className="space-y-2">
<div className="flex items-center gap-2">
{setupSteps.map((_, index) => (
<div
key={index}
className={cn(
"h-1 flex-1 rounded-full transition-colors",
index <= currentStep ? 'bg-primary' : 'bg-muted'
)}
/>
))}
</div>
<p className="text-xs text-muted-foreground">
Step {currentStep + 1} of {setupSteps.length}
</p>
</div>
)}
</div>
)}
</div>
<div className="flex gap-3 pt-6 border-t">

View File

@ -0,0 +1,246 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Loader2, Info } from 'lucide-react';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Checkbox } from '@/components/ui/checkbox';
import type { SetupStep } from './types';
interface TriggerField {
key: string;
label: string;
type: 'string' | 'number' | 'boolean' | 'array' | 'select';
required: boolean;
description?: string;
placeholder?: string;
enum?: string[];
default?: any;
}
interface TriggerConfigStepProps {
step: SetupStep & {
trigger_slug?: string;
trigger_index?: number;
trigger_fields?: Record<string, { type: string; required: boolean }>;
};
profileId: string | null;
config: Record<string, any>;
onProfileSelect: (stepId: string, profileId: string | null) => void;
onConfigUpdate: (stepId: string, config: Record<string, any>) => void;
}
const FieldRenderer = ({
field,
value,
onChange
}: {
field: TriggerField;
value: any;
onChange: (key: string, value: any, type: string) => void;
}) => {
const fieldValue = value ?? field.default ?? '';
switch (field.type) {
case 'select':
return (
<Select
value={fieldValue.toString()}
onValueChange={(val) => onChange(field.key, val, field.type)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder={field.placeholder || `Select ${field.label}`} />
</SelectTrigger>
<SelectContent>
{field.enum?.map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
);
case 'boolean':
return (
<div className="flex items-center space-x-2">
<Checkbox
id={field.key}
checked={fieldValue === true}
onCheckedChange={(checked) => onChange(field.key, checked, field.type)}
/>
<label
htmlFor={field.key}
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{field.label}
</label>
</div>
);
case 'array':
return (
<Input
type="text"
value={Array.isArray(fieldValue) ? fieldValue.join(', ') : fieldValue}
onChange={(e) => onChange(field.key, e.target.value, field.type)}
placeholder={field.placeholder || 'Enter comma-separated values'}
className="w-full"
/>
);
case 'number':
return (
<Input
type="number"
value={fieldValue}
onChange={(e) => onChange(field.key, e.target.value, field.type)}
placeholder={field.placeholder}
className="w-full"
/>
);
default:
return (
<Input
type="text"
value={fieldValue}
onChange={(e) => onChange(field.key, e.target.value, field.type)}
placeholder={field.placeholder}
className="w-full"
/>
);
}
};
export const TriggerConfigStep: React.FC<TriggerConfigStepProps> = ({
step,
profileId,
config,
onProfileSelect,
onConfigUpdate,
}) => {
const [isLoadingFields, setIsLoadingFields] = useState(false);
const [triggerFields, setTriggerFields] = useState<TriggerField[]>([]);
const [error, setError] = useState<string | null>(null);
const fetchTriggerFields = useCallback(async () => {
if (step.trigger_fields) {
const fields: TriggerField[] = Object.entries(step.trigger_fields).map(([key, fieldInfo]) => ({
key,
label: key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
type: fieldInfo.type === 'boolean' ? 'boolean' :
fieldInfo.type === 'number' ? 'number' :
fieldInfo.type === 'array' ? 'array' :
'string',
required: fieldInfo.required,
description: `Required field from template`,
placeholder: `Enter ${key}`,
}));
setTriggerFields(fields);
setIsLoadingFields(false);
return;
}
if (!step.trigger_slug) return;
setIsLoadingFields(true);
setError(null);
try {
const response = await fetch(`/api/composio/triggers/schema/${step.trigger_slug}`);
if (!response.ok) {
throw new Error('Failed to fetch trigger fields');
}
const data = await response.json();
const fields: TriggerField[] = [];
if (data.config?.properties) {
Object.entries(data.config.properties).forEach(([key, prop]: [string, any]) => {
fields.push({
key,
label: prop.title || key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
type: prop.enum ? 'select' : prop.type || 'string',
required: data.config.required?.includes(key) || false,
description: prop.description,
placeholder: prop.example || prop.default || `Enter ${key}`,
enum: prop.enum,
default: prop.default,
});
});
}
setTriggerFields(fields);
} catch (err) {
console.error('Error fetching trigger fields:', err);
setError('Failed to load trigger configuration fields');
} finally {
setIsLoadingFields(false);
}
}, [step.trigger_slug, step.trigger_fields]);
useEffect(() => {
fetchTriggerFields();
}, [fetchTriggerFields]);
const handleFieldChange = useCallback((key: string, value: any, type: string) => {
let processedValue = value;
if (type === 'number' && typeof value === 'string') {
processedValue = value ? parseFloat(value) : undefined;
} else if (type === 'boolean') {
processedValue = value;
} else if (type === 'array' && typeof value === 'string') {
processedValue = value.split(',').map(s => s.trim()).filter(Boolean);
}
onConfigUpdate(step.id, {
...config,
[key]: processedValue
});
}, [step.id, config, onConfigUpdate]);
return (
<div className="p-4 bg-muted/50 rounded-lg">
<h4 className="font-medium mb-3">Trigger Configuration</h4>
{isLoadingFields ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
<span className="ml-2 text-sm text-muted-foreground">Loading configuration fields...</span>
</div>
) : error ? (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
) : triggerFields.length === 0 ? (
<Alert>
<Info className="h-4 w-4" />
<AlertDescription>
No additional configuration needed for this trigger.
</AlertDescription>
</Alert>
) : (
<div className="space-y-4">
{triggerFields.map((field) => (
<div key={field.key} className="space-y-2">
<Label htmlFor={field.key}>
{field.label}
{field.required && <span className="text-destructive ml-1">*</span>}
</Label>
{field.description && (
<p className="text-xs text-muted-foreground">{field.description}</p>
)}
<FieldRenderer
key={field.key}
field={field}
value={config[field.key]}
onChange={handleFieldChange}
/>
</div>
))}
</div>
)}
</div>
);
};

View File

@ -42,6 +42,15 @@ export interface MarketplaceTemplate {
source_version_id?: string;
source_version_name?: string;
};
config?: {
triggers?: Array<{
name: string;
description?: string;
trigger_type: string;
is_active: boolean;
config: Record<string, any>;
}>;
};
}
export interface SetupStep {
@ -62,4 +71,8 @@ export interface SetupStep {
description?: string;
}>;
source?: 'trigger' | 'tool';
trigger_slug?: string;
trigger_index?: number;
trigger_fields?: Record<string, { type: string; required: boolean }>;
required_config?: string[];
}

View File

@ -36,7 +36,7 @@ export function CustomAgentsSection({ onAgentSelect }: CustomAgentsSectionProps)
const router = useRouter();
const { data: templates, isLoading, error } = useKortixTeamTemplates();
const installTemplate = useInstallTemplate();
const [selectedTemplate, setSelectedTemplate] = React.useState<MarketplaceTemplate | null>(null);
const [isPreviewOpen, setIsPreviewOpen] = React.useState(false);
const [showInstallDialog, setShowInstallDialog] = React.useState(false);
@ -65,8 +65,9 @@ export function CustomAgentsSection({ onAgentSelect }: CustomAgentsSectionProps)
model: template.metadata?.model,
marketplace_published_at: template.marketplace_published_at,
usage_examples: template.usage_examples,
config: template.config,
};
console.log('usage examples', template.usage_examples);
setSelectedTemplate(marketplaceTemplate);
setIsPreviewOpen(true);
};
@ -94,12 +95,12 @@ export function CustomAgentsSection({ onAgentSelect }: CustomAgentsSectionProps)
setShowInstallDialog(true);
};
// Handle the actual installation from the streamlined dialog
const handleInstall = async (
item: MarketplaceTemplate,
instanceName: string,
profileMappings: Record<string, string>,
customServerConfigs: Record<string, any>
customServerConfigs: Record<string, any>,
triggerConfigs?: Record<string, Record<string, any>>
) => {
if (!item) return;
@ -111,6 +112,7 @@ export function CustomAgentsSection({ onAgentSelect }: CustomAgentsSectionProps)
instance_name: instanceName,
profile_mappings: profileMappings,
custom_mcp_configs: customServerConfigs,
trigger_configs: triggerConfigs,
});
if (result.status === 'installed' && result.instance_id) {

View File

@ -6,6 +6,7 @@ export type Agent = {
agent_id: string;
name: string;
system_prompt: string;
model?: string | null;
configured_mcps: Array<{
name: string;
config: Record<string, any>;
@ -97,8 +98,6 @@ export type AgentCreateRequest = {
}>;
agentpress_tools?: Record<string, any>;
is_default?: boolean;
// New
// Icon system fields
icon_name?: string | null;
icon_color?: string | null;
icon_background?: string | null;
@ -106,7 +105,7 @@ export type AgentCreateRequest = {
export type AgentVersionCreateRequest = {
system_prompt: string;
model?: string; // Add model field
model?: string;
configured_mcps?: Array<{
name: string;
config: Record<string, any>;
@ -143,6 +142,7 @@ export type AgentUpdateRequest = {
name?: string;
description?: string;
system_prompt?: string;
model?: string | null;
configured_mcps?: Array<{
name: string;
config: Record<string, any>;
@ -155,12 +155,9 @@ export type AgentUpdateRequest = {
}>;
agentpress_tools?: Record<string, any>;
is_default?: boolean;
// New
// Icon system fields
icon_name?: string | null;
icon_color?: string | null;
icon_background?: string | null;
// MCP replacement flag
replace_mcps?: boolean;
};

View File

@ -60,6 +60,15 @@ export interface AgentTemplate {
source_version_name?: string;
model?: string;
};
config?: {
triggers?: Array<{
name: string;
description?: string;
trigger_type: string;
is_active: boolean;
config: Record<string, any>;
}>;
};
}
export interface MCPRequirement {
@ -76,6 +85,7 @@ export interface InstallTemplateRequest {
custom_system_prompt?: string;
profile_mappings?: Record<string, string>;
custom_mcp_configs?: Record<string, Record<string, any>>;
trigger_configs?: Record<string, Record<string, any>>;
}
export interface InstallationResponse {