mirror of https://github.com/kortix-ai/suna.git
feat: implement JSON agent export/import with improved UX
- Add backend API endpoints for agent export/import - GET /agents/{agent_id}/export - exports agent configuration as JSON - POST /agents/import - imports agent from JSON (new agents only) - Add AgentExportData and AgentImportRequest Pydantic models - Integrate export functionality in agent config header - Add always-visible 3-dots menu left of "Prompt to Build" tab - Include export option in dropdown menu for all agent types - Handle loading states during export process - Add subtle import functionality to new agent dialog - Replace mode selection with "or import from JSON" link - Support file upload and validation for agent JSON - Streamline UX for creating agents from templates - Create React Query hooks for export/import operations - useExportAgent: handles JSON download with proper error handling - useImportAgent: creates new agent and invalidates cache - Remove update existing agent option (import creates new agents only) - Fix API integration to use direct fetch calls with backend URL - Clean up unused components and improve code organization This enables users to share agent configurations across instances and create agents from templates with an intuitive interface.
This commit is contained in:
parent
286e7edeef
commit
adf9326f61
|
@ -146,6 +146,27 @@ class ThreadAgentResponse(BaseModel):
|
|||
source: str # "thread", "default", "none", "missing"
|
||||
message: str
|
||||
|
||||
class AgentExportData(BaseModel):
|
||||
"""Exportable agent configuration data"""
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
system_prompt: str
|
||||
agentpress_tools: Dict[str, Any]
|
||||
configured_mcps: List[Dict[str, Any]]
|
||||
custom_mcps: List[Dict[str, Any]]
|
||||
avatar: Optional[str] = None
|
||||
avatar_color: Optional[str] = None
|
||||
tags: Optional[List[str]] = []
|
||||
metadata: Optional[Dict[str, Any]] = None
|
||||
export_version: str = "1.0"
|
||||
exported_at: str
|
||||
exported_by: Optional[str] = None
|
||||
|
||||
class AgentImportRequest(BaseModel):
|
||||
"""Request to import an agent from JSON"""
|
||||
import_data: AgentExportData
|
||||
import_as_new: bool = True # Always true, only creating new agents is supported
|
||||
|
||||
def initialize(
|
||||
_db: DBConnection,
|
||||
_instance_id: Optional[str] = None
|
||||
|
@ -1589,6 +1610,140 @@ async def get_agent(agent_id: str, user_id: str = Depends(get_current_user_id_fr
|
|||
logger.error(f"Error fetching agent {agent_id} for user {user_id}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to fetch agent: {str(e)}")
|
||||
|
||||
@router.get("/agents/{agent_id}/export", response_model=AgentExportData)
|
||||
async def export_agent(agent_id: str, user_id: str = Depends(get_current_user_id_from_jwt)):
|
||||
"""Export an agent configuration as JSON"""
|
||||
logger.info(f"Exporting agent {agent_id} for user: {user_id}")
|
||||
|
||||
try:
|
||||
client = await db.client
|
||||
|
||||
# Get agent data
|
||||
agent_result = await client.table('agents').select('*').eq('agent_id', agent_id).eq('account_id', user_id).execute()
|
||||
if not agent_result.data:
|
||||
raise HTTPException(status_code=404, detail="Agent not found")
|
||||
|
||||
agent = agent_result.data[0]
|
||||
|
||||
# Get current version data if available
|
||||
current_version = None
|
||||
if agent.get('current_version_id'):
|
||||
version_result = await client.table('agent_versions').select('*').eq('version_id', agent['current_version_id']).execute()
|
||||
if version_result.data:
|
||||
current_version = version_result.data[0]
|
||||
|
||||
# Extract configuration using existing helper
|
||||
from agent.config_helper import extract_agent_config
|
||||
config = extract_agent_config(agent, current_version)
|
||||
|
||||
# Clean metadata for export (remove instance-specific data)
|
||||
export_metadata = {}
|
||||
if agent.get('metadata'):
|
||||
export_metadata = {k: v for k, v in agent['metadata'].items()
|
||||
if k not in ['is_suna_default', 'centrally_managed', 'installation_date', 'last_central_update']}
|
||||
|
||||
# Create export data
|
||||
export_data = AgentExportData(
|
||||
name=config.get('name', ''),
|
||||
description=config.get('description', ''),
|
||||
system_prompt=config.get('system_prompt', ''),
|
||||
agentpress_tools=config.get('agentpress_tools', {}),
|
||||
configured_mcps=config.get('configured_mcps', []),
|
||||
custom_mcps=config.get('custom_mcps', []),
|
||||
avatar=config.get('avatar'),
|
||||
avatar_color=config.get('avatar_color'),
|
||||
tags=agent.get('tags', []),
|
||||
metadata=export_metadata,
|
||||
exported_at=datetime.utcnow().isoformat(),
|
||||
exported_by=user_id
|
||||
)
|
||||
|
||||
logger.info(f"Successfully exported agent {agent_id}")
|
||||
return export_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error exporting agent {agent_id}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to export agent: {str(e)}")
|
||||
|
||||
@router.post("/agents/import", response_model=AgentResponse)
|
||||
async def import_agent(
|
||||
import_request: AgentImportRequest,
|
||||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
"""Import an agent from JSON configuration"""
|
||||
logger.info(f"Importing agent for user: {user_id}")
|
||||
|
||||
if not await is_enabled("custom_agents"):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Custom agents currently disabled. This feature is not available at the moment."
|
||||
)
|
||||
|
||||
try:
|
||||
client = await db.client
|
||||
import_data = import_request.import_data
|
||||
|
||||
# Validate import data
|
||||
if not import_data.name or not import_data.system_prompt:
|
||||
raise HTTPException(status_code=400, detail="Agent name and system prompt are required")
|
||||
|
||||
# Create new agent
|
||||
logger.info(f"Creating new agent from import: {import_data.name}")
|
||||
|
||||
# Check if user wants to set as default, and if so, unset other defaults
|
||||
is_default = False # Imported agents are not default by default
|
||||
|
||||
insert_data = {
|
||||
"account_id": user_id,
|
||||
"name": import_data.name,
|
||||
"description": import_data.description,
|
||||
"avatar": import_data.avatar,
|
||||
"avatar_color": import_data.avatar_color,
|
||||
"is_default": is_default,
|
||||
"tags": import_data.tags or [],
|
||||
"version_count": 1,
|
||||
"metadata": import_data.metadata or {}
|
||||
}
|
||||
|
||||
new_agent = await client.table('agents').insert(insert_data).execute()
|
||||
|
||||
if not new_agent.data:
|
||||
raise HTTPException(status_code=500, detail="Failed to create agent from import")
|
||||
|
||||
agent = new_agent.data[0]
|
||||
agent_id = agent['agent_id']
|
||||
|
||||
# Create initial version
|
||||
try:
|
||||
version_service = await _get_version_service()
|
||||
|
||||
version = await version_service.create_version(
|
||||
agent_id=agent_id,
|
||||
user_id=user_id,
|
||||
system_prompt=import_data.system_prompt,
|
||||
configured_mcps=import_data.configured_mcps,
|
||||
custom_mcps=import_data.custom_mcps,
|
||||
agentpress_tools=import_data.agentpress_tools,
|
||||
version_name="v1",
|
||||
change_description="Initial version from import"
|
||||
)
|
||||
|
||||
logger.info(f"Successfully imported agent {agent_id}")
|
||||
return await get_agent(agent_id, user_id)
|
||||
|
||||
except Exception as e:
|
||||
# Clean up agent if anything goes wrong
|
||||
await client.table('agents').delete().eq('agent_id', agent_id).execute()
|
||||
raise e
|
||||
|
||||
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error importing agent: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to import agent: {str(e)}")
|
||||
|
||||
@router.post("/agents", response_model=AgentResponse)
|
||||
async def create_agent(
|
||||
agent_data: AgentCreateRequest,
|
||||
|
|
|
@ -23,6 +23,7 @@ import { cn } from '@/lib/utils';
|
|||
|
||||
import { AgentHeader, VersionAlert, AgentBuilderTab, ConfigurationTab } from '@/components/agents/config';
|
||||
import { UpcomingRunsDropdown } from '@/components/agents/upcoming-runs-dropdown';
|
||||
import { useExportAgent } from '@/hooks/react-query/agents/use-agent-export-import';
|
||||
|
||||
interface FormData {
|
||||
name: string;
|
||||
|
@ -50,6 +51,7 @@ export default function AgentConfigurationPage() {
|
|||
const updateAgentMutation = useUpdateAgent();
|
||||
const createVersionMutation = useCreateAgentVersion();
|
||||
const activateVersionMutation = useActivateAgentVersion();
|
||||
const exportMutation = useExportAgent();
|
||||
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
name: '',
|
||||
|
@ -389,6 +391,11 @@ export default function AgentConfigurationPage() {
|
|||
}
|
||||
}, [agentId, activateVersionMutation]);
|
||||
|
||||
const handleExport = useCallback(() => {
|
||||
if (!agentId) return;
|
||||
exportMutation.mutate(agentId);
|
||||
}, [agentId, exportMutation]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isViewingOldVersion && activeTab === 'agent-builder') {
|
||||
setActiveTab('configuration');
|
||||
|
@ -526,6 +533,8 @@ export default function AgentConfigurationPage() {
|
|||
onFieldChange={handleFieldChange}
|
||||
onStyleChange={handleStyleChange}
|
||||
onTabChange={setActiveTab}
|
||||
onExport={handleExport}
|
||||
isExporting={exportMutation.isPending}
|
||||
agentMetadata={agent?.metadata}
|
||||
/>
|
||||
</div>
|
||||
|
@ -655,6 +664,8 @@ export default function AgentConfigurationPage() {
|
|||
onFieldChange={handleFieldChange}
|
||||
onStyleChange={handleStyleChange}
|
||||
onTabChange={setActiveTab}
|
||||
onExport={handleExport}
|
||||
isExporting={exportMutation.isPending}
|
||||
agentMetadata={agent?.metadata}
|
||||
/>
|
||||
</div>
|
||||
|
@ -724,6 +735,8 @@ export default function AgentConfigurationPage() {
|
|||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
import React from 'react';
|
||||
import { Sparkles, Settings } from 'lucide-react';
|
||||
import { Sparkles, Settings, MoreHorizontal, Download } from 'lucide-react';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { EditableText } from '@/components/ui/editable';
|
||||
import { StylePicker } from '../style-picker';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { toast } from 'sonner';
|
||||
import { KortixLogo } from '@/components/sidebar/kortix-logo';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface AgentHeaderProps {
|
||||
agentId: string;
|
||||
|
@ -22,6 +29,8 @@ interface AgentHeaderProps {
|
|||
onFieldChange: (field: string, value: any) => void;
|
||||
onStyleChange: (emoji: string, color: string) => void;
|
||||
onTabChange: (value: string) => void;
|
||||
onExport?: () => void;
|
||||
isExporting?: boolean;
|
||||
agentMetadata?: {
|
||||
is_suna_default?: boolean;
|
||||
restrictions?: {
|
||||
|
@ -39,6 +48,8 @@ export function AgentHeader({
|
|||
onFieldChange,
|
||||
onStyleChange,
|
||||
onTabChange,
|
||||
onExport,
|
||||
isExporting = false,
|
||||
agentMetadata,
|
||||
}: AgentHeaderProps) {
|
||||
const isSunaAgent = agentMetadata?.is_suna_default || false;
|
||||
|
@ -60,7 +71,7 @@ export function AgentHeader({
|
|||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
{isSunaAgent ? (
|
||||
<div className="h-9 w-9 bg-background rounded-lg bg-muted border border flex items-center justify-center">
|
||||
<div className="h-9 w-9 rounded-lg bg-muted border flex items-center justify-center">
|
||||
<KortixLogo size={16} />
|
||||
</div>
|
||||
) : (
|
||||
|
@ -93,30 +104,59 @@ export function AgentHeader({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{!isSunaAgent && (
|
||||
<Tabs value={activeTab} onValueChange={onTabChange}>
|
||||
<TabsList className="grid grid-cols-2 bg-muted/50 h-9">
|
||||
<TabsTrigger
|
||||
value="agent-builder"
|
||||
disabled={isViewingOldVersion}
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm data-[state=active]:bg-background data-[state=active]:shadow-sm",
|
||||
isViewingOldVersion && "opacity-50 cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
<Sparkles className="h-3 w-3" />
|
||||
Prompt to Build
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="configuration"
|
||||
className="flex items-center gap-2 text-sm data-[state=active]:bg-background data-[state=active]:shadow-sm"
|
||||
>
|
||||
<Settings className="h-3 w-3" />
|
||||
Manual Config
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 3-dots menu for actions - always show if onExport is available */}
|
||||
{onExport && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
||||
disabled={isExporting}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-40">
|
||||
<DropdownMenuItem
|
||||
onClick={onExport}
|
||||
disabled={isExporting}
|
||||
className="flex items-center gap-2 text-xs"
|
||||
>
|
||||
<Download className="h-3 w-3" />
|
||||
Export agent
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
{/* Only show tabs for non-Suna agents */}
|
||||
{!isSunaAgent && (
|
||||
<Tabs value={activeTab} onValueChange={onTabChange}>
|
||||
<TabsList className="grid grid-cols-2 bg-muted/50 h-9">
|
||||
<TabsTrigger
|
||||
value="agent-builder"
|
||||
disabled={isViewingOldVersion}
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm data-[state=active]:bg-background data-[state=active]:shadow-sm",
|
||||
isViewingOldVersion && "opacity-50 cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
<Sparkles className="h-3 w-3" />
|
||||
Prompt to Build
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="configuration"
|
||||
className="flex items-center gap-2 text-sm data-[state=active]:bg-background data-[state=active]:shadow-sm"
|
||||
>
|
||||
<Settings className="h-3 w-3" />
|
||||
Manual Config
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { Loader2, Upload, Plus, FileJson, CheckCircle2, AlertCircle } from 'lucide-react';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
|
@ -12,7 +12,13 @@ import {
|
|||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { useCreateNewAgent } from '@/hooks/react-query/agents/use-agents';
|
||||
import { useImportAgent, parseAgentImportFile, type AgentExportData } from '@/hooks/react-query/agents/use-agent-export-import';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface NewAgentDialogProps {
|
||||
open: boolean;
|
||||
|
@ -21,7 +27,14 @@ interface NewAgentDialogProps {
|
|||
}
|
||||
|
||||
export function NewAgentDialog({ open, onOpenChange, onSuccess }: NewAgentDialogProps) {
|
||||
const [mode, setMode] = useState<'create' | 'import'>('create');
|
||||
const [importFile, setImportFile] = useState<File | null>(null);
|
||||
const [importData, setImportData] = useState<AgentExportData | null>(null);
|
||||
const [isProcessingFile, setIsProcessingFile] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const createNewAgentMutation = useCreateNewAgent();
|
||||
const importMutation = useImportAgent();
|
||||
|
||||
const handleCreateNewAgent = () => {
|
||||
createNewAgentMutation.mutate(undefined, {
|
||||
|
@ -36,33 +49,188 @@ export function NewAgentDialog({ open, onOpenChange, onSuccess }: NewAgentDialog
|
|||
});
|
||||
};
|
||||
|
||||
const isLoading = createNewAgentMutation.isPending;
|
||||
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
if (!file.name.endsWith('.json')) {
|
||||
toast.error('Please select a JSON file');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsProcessingFile(true);
|
||||
try {
|
||||
const data = await parseAgentImportFile(file);
|
||||
setImportFile(file);
|
||||
setImportData(data);
|
||||
toast.success('Import file loaded successfully');
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to parse import file');
|
||||
setImportFile(null);
|
||||
setImportData(null);
|
||||
} finally {
|
||||
setIsProcessingFile(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = () => {
|
||||
if (!importData) {
|
||||
toast.error('No import data available');
|
||||
return;
|
||||
}
|
||||
|
||||
importMutation.mutate({
|
||||
import_data: importData,
|
||||
import_as_new: true
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
onOpenChange(false);
|
||||
onSuccess?.();
|
||||
// Reset form
|
||||
setImportFile(null);
|
||||
setImportData(null);
|
||||
setMode('create');
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleDialogClose = (open: boolean) => {
|
||||
if (!open) {
|
||||
// Reset form when closing
|
||||
setImportFile(null);
|
||||
setImportData(null);
|
||||
setMode('create');
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
}
|
||||
onOpenChange(open);
|
||||
};
|
||||
|
||||
const isLoading = createNewAgentMutation.isPending || importMutation.isPending;
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialog open={open} onOpenChange={handleDialogClose}>
|
||||
<AlertDialogContent className="max-w-md">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Create New Agent</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will create a new agent with a default name and description.
|
||||
This will create a new agent that you can customize and configure.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Content based on mode */}
|
||||
{mode === 'create' ? (
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
This will create a new agent with a default name and description that you can customize later.
|
||||
</div>
|
||||
|
||||
{/* Subtle import option */}
|
||||
<div className="text-center">
|
||||
<button
|
||||
onClick={() => setMode('import')}
|
||||
className="text-xs text-muted-foreground hover:text-foreground underline transition-colors"
|
||||
disabled={isLoading}
|
||||
>
|
||||
or import from JSON
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="import-file">Select Agent JSON File</Label>
|
||||
<button
|
||||
onClick={() => setMode('create')}
|
||||
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
disabled={isLoading}
|
||||
>
|
||||
← back to create blank
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
ref={fileInputRef}
|
||||
id="import-file"
|
||||
type="file"
|
||||
accept=".json"
|
||||
onChange={handleFileChange}
|
||||
className="mt-1"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
|
||||
{isProcessingFile && (
|
||||
<Alert>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<AlertDescription>
|
||||
Processing import file...
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{importData && (
|
||||
<Alert>
|
||||
<CheckCircle2 className="h-4 w-4 text-green-600" />
|
||||
<AlertDescription>
|
||||
<div className="space-y-1">
|
||||
<div><strong>Agent:</strong> {importData.name}</div>
|
||||
{importData.description && (
|
||||
<div><strong>Description:</strong> {importData.description}</div>
|
||||
)}
|
||||
<div><strong>Exported:</strong> {new Date(importData.exported_at).toLocaleString()}</div>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isLoading}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleCreateNewAgent}
|
||||
disabled={isLoading}
|
||||
className="min-w-[100px]"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Creating...
|
||||
</>
|
||||
) : (
|
||||
'Create'
|
||||
)}
|
||||
</AlertDialogAction>
|
||||
|
||||
{mode === 'create' ? (
|
||||
<AlertDialogAction
|
||||
onClick={handleCreateNewAgent}
|
||||
disabled={isLoading}
|
||||
className="min-w-[100px]"
|
||||
>
|
||||
{createNewAgentMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Creating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Create Agent
|
||||
</>
|
||||
)}
|
||||
</AlertDialogAction>
|
||||
) : (
|
||||
<AlertDialogAction
|
||||
onClick={handleImport}
|
||||
disabled={!importData || isLoading}
|
||||
className="min-w-[100px]"
|
||||
>
|
||||
{importMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Importing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
Import Agent
|
||||
</>
|
||||
)}
|
||||
</AlertDialogAction>
|
||||
)}
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
|
|
@ -0,0 +1,207 @@
|
|||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { createClient } from "@/lib/supabase/client";
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || '';
|
||||
|
||||
export interface AgentExportData {
|
||||
name: string;
|
||||
description?: string;
|
||||
system_prompt: string;
|
||||
agentpress_tools: Record<string, any>;
|
||||
configured_mcps: Array<{
|
||||
name: string;
|
||||
config: Record<string, any>;
|
||||
}>;
|
||||
custom_mcps: Array<{
|
||||
name: string;
|
||||
type: 'json' | 'sse';
|
||||
config: Record<string, any>;
|
||||
enabledTools: string[];
|
||||
}>;
|
||||
avatar?: string;
|
||||
avatar_color?: string;
|
||||
tags?: string[];
|
||||
metadata?: Record<string, any>;
|
||||
export_version: string;
|
||||
exported_at: string;
|
||||
exported_by?: string;
|
||||
}
|
||||
|
||||
export interface AgentImportRequest {
|
||||
import_data: AgentExportData;
|
||||
import_as_new: boolean;
|
||||
}
|
||||
|
||||
// Export agent hook
|
||||
export const useExportAgent = () => {
|
||||
return useMutation({
|
||||
mutationFn: async (agentId: string): Promise<AgentExportData> => {
|
||||
console.log('Exporting agent:', agentId);
|
||||
|
||||
const supabase = createClient();
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
if (!session) {
|
||||
throw new Error('You must be logged in to export agents');
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_URL}/agents/${agentId}/export`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${session.access_token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Unknown error' }));
|
||||
throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Export response:', data);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (data, agentId) => {
|
||||
console.log('Export successful for agent:', agentId, 'Data:', data);
|
||||
|
||||
// Create and download JSON file
|
||||
const jsonString = JSON.stringify(data, null, 2);
|
||||
const blob = new Blob([jsonString], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `${data.name.replace(/[^a-zA-Z0-9]/g, '_')}_agent_export.json`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
toast.success(`Agent "${data.name}" exported successfully`);
|
||||
},
|
||||
onError: (error: any) => {
|
||||
console.error('Export error:', error);
|
||||
toast.error(error?.message || "Failed to export agent");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Import agent hook
|
||||
export const useImportAgent = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (importRequest: AgentImportRequest) => {
|
||||
console.log('Importing agent:', importRequest);
|
||||
|
||||
const supabase = createClient();
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
if (!session) {
|
||||
throw new Error('You must be logged in to import agents');
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_URL}/agents/import`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${session.access_token}`,
|
||||
},
|
||||
body: JSON.stringify(importRequest),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Unknown error' }));
|
||||
throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Import response:', data);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (data, variables) => {
|
||||
console.log('Import successful:', data);
|
||||
|
||||
// Invalidate agents list to refresh data
|
||||
queryClient.invalidateQueries({ queryKey: ['agents'] });
|
||||
|
||||
const action = variables.import_as_new ? 'imported' : 'updated';
|
||||
toast.success(`Agent "${variables.import_data.name}" ${action} successfully`);
|
||||
},
|
||||
onError: (error: any) => {
|
||||
console.error('Import error:', error);
|
||||
toast.error(error?.message || "Failed to import agent");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Helper function to validate import data
|
||||
export const validateImportData = (data: any): { isValid: boolean; errors: string[] } => {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!data) {
|
||||
errors.push("No data provided");
|
||||
return { isValid: false, errors };
|
||||
}
|
||||
|
||||
if (!data.name || typeof data.name !== 'string') {
|
||||
errors.push("Agent name is required");
|
||||
}
|
||||
|
||||
if (!data.system_prompt || typeof data.system_prompt !== 'string') {
|
||||
errors.push("System prompt is required");
|
||||
}
|
||||
|
||||
if (!data.export_version) {
|
||||
errors.push("Export version is missing - this may not be a valid agent export file");
|
||||
}
|
||||
|
||||
if (data.agentpress_tools && typeof data.agentpress_tools !== 'object') {
|
||||
errors.push("Invalid agentpress_tools format");
|
||||
}
|
||||
|
||||
if (data.configured_mcps && !Array.isArray(data.configured_mcps)) {
|
||||
errors.push("Invalid configured_mcps format");
|
||||
}
|
||||
|
||||
if (data.custom_mcps && !Array.isArray(data.custom_mcps)) {
|
||||
errors.push("Invalid custom_mcps format");
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
};
|
||||
|
||||
// Helper function to read and parse JSON file
|
||||
export const parseAgentImportFile = (file: File): Promise<AgentExportData> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const content = e.target?.result as string;
|
||||
const data = JSON.parse(content);
|
||||
|
||||
const validation = validateImportData(data);
|
||||
if (!validation.isValid) {
|
||||
reject(new Error(`Invalid import file: ${validation.errors.join(', ')}`));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(data as AgentExportData);
|
||||
} catch (error) {
|
||||
reject(new Error('Failed to parse JSON file'));
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = () => {
|
||||
reject(new Error('Failed to read file'));
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
});
|
||||
};
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"name": "Research Assistant",
|
||||
"description": "An AI assistant specialized in conducting research and providing comprehensive analysis",
|
||||
"system_prompt": "You are a research assistant with expertise in gathering, analyzing, and synthesizing information from various sources. Your approach is thorough and methodical.\n\nKey capabilities:\n- Conduct comprehensive web research\n- Analyze and synthesize information from multiple sources\n- Provide well-structured reports and summaries\n- Fact-check information for accuracy\n- Identify trends and patterns in data\n\nAlways cite your sources and provide evidence-based conclusions.",
|
||||
"agentpress_tools": {
|
||||
"web_search": {
|
||||
"enabled": true,
|
||||
"description": "Search the web for current information and research"
|
||||
},
|
||||
"sb_files": {
|
||||
"enabled": true,
|
||||
"description": "Read and write files for research documentation"
|
||||
},
|
||||
"code_interpreter": {
|
||||
"enabled": true,
|
||||
"description": "Analyze data and create visualizations"
|
||||
}
|
||||
},
|
||||
"configured_mcps": [
|
||||
{
|
||||
"name": "filesystem",
|
||||
"qualifiedName": "@modelcontextprotocol/server-filesystem",
|
||||
"config": {
|
||||
"path": "/research-data"
|
||||
},
|
||||
"enabledTools": [
|
||||
"read_file",
|
||||
"write_file",
|
||||
"list_directory"
|
||||
]
|
||||
}
|
||||
],
|
||||
"custom_mcps": [],
|
||||
"avatar": "🔬",
|
||||
"avatar_color": "#4F46E5",
|
||||
"tags": ["research", "analysis", "academic"],
|
||||
"metadata": {
|
||||
"category": "productivity",
|
||||
"use_cases": ["academic research", "market analysis", "fact checking"]
|
||||
},
|
||||
"export_version": "1.0",
|
||||
"exported_at": "2024-01-15T10:30:00.000Z",
|
||||
"exported_by": "user123"
|
||||
}
|
Loading…
Reference in New Issue