mirror of https://github.com/kortix-ai/suna.git
limit agent creation based on tiers
This commit is contained in:
parent
f1f8f82b07
commit
c786e190b6
|
@ -1767,7 +1767,6 @@ async def import_agent_from_json(
|
|||
request: JsonImportRequestModel,
|
||||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
"""Import an agent from JSON with credential mappings"""
|
||||
logger.info(f"Importing agent from JSON - user: {user_id}")
|
||||
|
||||
if not await is_enabled("custom_agents"):
|
||||
|
@ -1776,6 +1775,21 @@ async def import_agent_from_json(
|
|||
detail="Custom agents currently disabled. This feature is not available at the moment."
|
||||
)
|
||||
|
||||
client = await db.client
|
||||
from .utils import check_agent_count_limit
|
||||
limit_check = await check_agent_count_limit(client, user_id)
|
||||
|
||||
if not limit_check['can_create']:
|
||||
error_detail = {
|
||||
"message": f"Maximum of {limit_check['limit']} agents allowed for your current plan. You have {limit_check['current_count']} agents.",
|
||||
"current_count": limit_check['current_count'],
|
||||
"limit": limit_check['limit'],
|
||||
"tier_name": limit_check['tier_name'],
|
||||
"error_code": "AGENT_LIMIT_EXCEEDED"
|
||||
}
|
||||
logger.warning(f"Agent limit exceeded for account {user_id}: {limit_check['current_count']}/{limit_check['limit']} agents")
|
||||
raise HTTPException(status_code=402, detail=error_detail)
|
||||
|
||||
try:
|
||||
from agent.json_import_service import JsonImportService, JsonImportRequest
|
||||
import_service = JsonImportService(db)
|
||||
|
@ -1817,6 +1831,20 @@ async def create_agent(
|
|||
)
|
||||
client = await db.client
|
||||
|
||||
from .utils import check_agent_count_limit
|
||||
limit_check = await check_agent_count_limit(client, user_id)
|
||||
|
||||
if not limit_check['can_create']:
|
||||
error_detail = {
|
||||
"message": f"Maximum of {limit_check['limit']} agents allowed for your current plan. You have {limit_check['current_count']} agents.",
|
||||
"current_count": limit_check['current_count'],
|
||||
"limit": limit_check['limit'],
|
||||
"tier_name": limit_check['tier_name'],
|
||||
"error_code": "AGENT_LIMIT_EXCEEDED"
|
||||
}
|
||||
logger.warning(f"Agent limit exceeded for account {user_id}: {limit_check['current_count']}/{limit_check['limit']} agents")
|
||||
raise HTTPException(status_code=402, detail=error_detail)
|
||||
|
||||
try:
|
||||
if agent_data.is_default:
|
||||
await client.table('agents').update({"is_default": False}).eq("account_id", user_id).eq("is_default", True).execute()
|
||||
|
@ -1874,6 +1902,10 @@ async def create_agent(
|
|||
await client.table('agents').delete().eq('agent_id', agent['agent_id']).execute()
|
||||
raise HTTPException(status_code=500, detail="Failed to create initial version")
|
||||
|
||||
# Invalidate agent count cache after successful creation
|
||||
from utils.cache import Cache
|
||||
await Cache.invalidate(f"agent_count_limit:{user_id}")
|
||||
|
||||
logger.info(f"Created agent {agent['agent_id']} with v1 for user: {user_id}")
|
||||
return AgentResponse(
|
||||
agent_id=agent['agent_id'],
|
||||
|
@ -2275,7 +2307,20 @@ async def delete_agent(agent_id: str, user_id: str = Depends(get_current_user_id
|
|||
if agent['is_default']:
|
||||
raise HTTPException(status_code=400, detail="Cannot delete default agent")
|
||||
|
||||
await client.table('agents').delete().eq('agent_id', agent_id).execute()
|
||||
if agent.get('metadata', {}).get('is_suna_default', False):
|
||||
raise HTTPException(status_code=400, detail="Cannot delete Suna default agent")
|
||||
|
||||
delete_result = await client.table('agents').delete().eq('agent_id', agent_id).execute()
|
||||
|
||||
if not delete_result.data:
|
||||
logger.warning(f"No agent was deleted for agent_id: {agent_id}, user_id: {user_id}")
|
||||
raise HTTPException(status_code=403, detail="Unable to delete agent - permission denied or agent not found")
|
||||
|
||||
try:
|
||||
from utils.cache import Cache
|
||||
await Cache.invalidate(f"agent_count_limit:{user_id}")
|
||||
except Exception as cache_error:
|
||||
logger.warning(f"Cache invalidation failed for user {user_id}: {str(cache_error)}")
|
||||
|
||||
logger.info(f"Successfully deleted agent: {agent_id}")
|
||||
return {"message": "Agent deleted successfully"}
|
||||
|
|
|
@ -114,6 +114,9 @@ class JsonImportService:
|
|||
request.custom_system_prompt or json_data.get('system_prompt', '')
|
||||
)
|
||||
|
||||
from utils.cache import Cache
|
||||
await Cache.invalidate(f"agent_count_limit:{request.account_id}")
|
||||
|
||||
logger.info(f"Successfully imported agent {agent_id} from JSON")
|
||||
|
||||
return JsonImportResult(
|
||||
|
|
|
@ -138,3 +138,63 @@ async def check_agent_run_limit(client, account_id: str) -> Dict[str, Any]:
|
|||
'running_count': 0,
|
||||
'running_thread_ids': []
|
||||
}
|
||||
|
||||
|
||||
async def check_agent_count_limit(client, account_id: str) -> Dict[str, Any]:
|
||||
try:
|
||||
try:
|
||||
result = await Cache.get(f"agent_count_limit:{account_id}")
|
||||
if result:
|
||||
logger.debug(f"Cache hit for agent count limit: {account_id}")
|
||||
return result
|
||||
except Exception as cache_error:
|
||||
logger.warning(f"Cache read failed for agent count limit {account_id}: {str(cache_error)}")
|
||||
|
||||
agents_result = await client.table('agents').select('agent_id, metadata').eq('account_id', account_id).execute()
|
||||
|
||||
non_suna_agents = []
|
||||
for agent in agents_result.data or []:
|
||||
metadata = agent.get('metadata', {}) or {}
|
||||
is_suna_default = metadata.get('is_suna_default', False)
|
||||
if not is_suna_default:
|
||||
non_suna_agents.append(agent)
|
||||
|
||||
current_count = len(non_suna_agents)
|
||||
logger.debug(f"Account {account_id} has {current_count} custom agents (excluding Suna defaults)")
|
||||
|
||||
try:
|
||||
from services.billing import get_subscription_tier
|
||||
tier_name = await get_subscription_tier(client, account_id)
|
||||
logger.debug(f"Account {account_id} subscription tier: {tier_name}")
|
||||
except Exception as billing_error:
|
||||
logger.warning(f"Could not get subscription tier for {account_id}: {str(billing_error)}, defaulting to free")
|
||||
tier_name = 'free'
|
||||
|
||||
agent_limit = config.AGENT_LIMITS.get(tier_name, config.AGENT_LIMITS['free'])
|
||||
|
||||
can_create = current_count < agent_limit
|
||||
|
||||
result = {
|
||||
'can_create': can_create,
|
||||
'current_count': current_count,
|
||||
'limit': agent_limit,
|
||||
'tier_name': tier_name
|
||||
}
|
||||
|
||||
try:
|
||||
await Cache.set(f"agent_count_limit:{account_id}", result, ttl=300)
|
||||
except Exception as cache_error:
|
||||
logger.warning(f"Cache write failed for agent count limit {account_id}: {str(cache_error)}")
|
||||
|
||||
logger.info(f"Account {account_id} has {current_count}/{agent_limit} agents (tier: {tier_name}) - can_create: {can_create}")
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking agent count limit for account {account_id}: {str(e)}", exc_info=True)
|
||||
return {
|
||||
'can_create': True,
|
||||
'current_count': 0,
|
||||
'limit': config.AGENT_LIMITS['free'],
|
||||
'tier_name': 'free'
|
||||
}
|
||||
|
|
|
@ -592,6 +592,30 @@ async def can_use_model(client, user_id: str, model_name: str):
|
|||
|
||||
return False, f"Your current subscription plan does not include access to {model_name}. Please upgrade your subscription or choose from your available models: {', '.join(allowed_models)}", allowed_models
|
||||
|
||||
async def get_subscription_tier(client, user_id: str) -> str:
|
||||
try:
|
||||
subscription = await get_user_subscription(user_id)
|
||||
|
||||
if not subscription:
|
||||
return 'free'
|
||||
|
||||
price_id = None
|
||||
if subscription.get('items') and subscription['items'].get('data') and len(subscription['items']['data']) > 0:
|
||||
price_id = subscription['items']['data'][0]['price']['id']
|
||||
else:
|
||||
price_id = subscription.get('price_id', config.STRIPE_FREE_TIER_ID)
|
||||
|
||||
tier_info = SUBSCRIPTION_TIERS.get(price_id)
|
||||
if tier_info:
|
||||
return tier_info['name']
|
||||
|
||||
logger.warning(f"Unknown price_id {price_id} for user {user_id}, defaulting to free tier")
|
||||
return 'free'
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting subscription tier for user {user_id}: {str(e)}")
|
||||
return 'free'
|
||||
|
||||
async def check_billing_status(client, user_id: str) -> Tuple[bool, str, Optional[Dict]]:
|
||||
"""
|
||||
Check if a user can run agents based on their subscription and usage.
|
||||
|
@ -634,8 +658,6 @@ async def check_billing_status(client, user_id: str) -> Tuple[bool, str, Optiona
|
|||
# Calculate current month's usage
|
||||
current_usage = await calculate_monthly_usage(client, user_id)
|
||||
|
||||
# TODO: also do user's AAL check
|
||||
# Check if within limits
|
||||
if current_usage >= tier_info['cost']:
|
||||
return False, f"Monthly limit of {tier_info['cost']} dollars reached. Please upgrade your plan or wait until next month.", subscription
|
||||
|
||||
|
|
|
@ -325,15 +325,22 @@ async def install_template(
|
|||
request: InstallTemplateRequest,
|
||||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
"""
|
||||
Install a template as a new agent instance.
|
||||
|
||||
Requires:
|
||||
- User must have access to the template (own it or it's public)
|
||||
"""
|
||||
try:
|
||||
# Validate template access first
|
||||
template = await validate_template_access_and_get(request.template_id, user_id)
|
||||
await validate_template_access_and_get(request.template_id, user_id)
|
||||
client = await db.client
|
||||
from agent.utils import check_agent_count_limit
|
||||
limit_check = await check_agent_count_limit(client, user_id)
|
||||
|
||||
if not limit_check['can_create']:
|
||||
error_detail = {
|
||||
"message": f"Maximum of {limit_check['limit']} agents allowed for your current plan. You have {limit_check['current_count']} agents.",
|
||||
"current_count": limit_check['current_count'],
|
||||
"limit": limit_check['limit'],
|
||||
"tier_name": limit_check['tier_name'],
|
||||
"error_code": "AGENT_LIMIT_EXCEEDED"
|
||||
}
|
||||
logger.warning(f"Agent limit exceeded for account {user_id}: {limit_check['current_count']}/{limit_check['limit']} agents")
|
||||
raise HTTPException(status_code=402, detail=error_detail)
|
||||
|
||||
logger.info(f"User {user_id} installing template {request.template_id}")
|
||||
|
||||
|
@ -362,7 +369,6 @@ async def install_template(
|
|||
)
|
||||
|
||||
except HTTPException:
|
||||
# Re-raise HTTP exceptions from our validation functions
|
||||
raise
|
||||
except TemplateInstallationError as e:
|
||||
logger.warning(f"Template installation failed: {e}")
|
||||
|
|
|
@ -107,6 +107,9 @@ class InstallationService:
|
|||
|
||||
await self._increment_download_count(template.template_id)
|
||||
|
||||
from utils.cache import Cache
|
||||
await Cache.invalidate(f"agent_count_limit:{request.account_id}")
|
||||
|
||||
agent_name = request.instance_name or f"{template.name} (from marketplace)"
|
||||
logger.info(f"Successfully installed template {template.template_id} as agent {agent_id}")
|
||||
|
||||
|
|
|
@ -271,6 +271,30 @@ class Configuration:
|
|||
# Agent execution limits (can be overridden via environment variable)
|
||||
_MAX_PARALLEL_AGENT_RUNS_ENV: Optional[str] = None
|
||||
|
||||
# Agent limits per billing tier
|
||||
AGENT_LIMITS = {
|
||||
'free': 2,
|
||||
'tier_2_20': 5,
|
||||
'tier_6_50': 20,
|
||||
'tier_12_100': 20,
|
||||
'tier_25_200': 100,
|
||||
'tier_50_400': 100,
|
||||
'tier_125_800': 100,
|
||||
'tier_200_1000': 100,
|
||||
# Yearly plans have same limits as monthly
|
||||
'tier_2_20_yearly': 5,
|
||||
'tier_6_50_yearly': 20,
|
||||
'tier_12_100_yearly': 20,
|
||||
'tier_25_200_yearly': 100,
|
||||
'tier_50_400_yearly': 100,
|
||||
'tier_125_800_yearly': 100,
|
||||
'tier_200_1000_yearly': 100,
|
||||
# Yearly commitment plans
|
||||
'tier_2_17_yearly_commitment': 5,
|
||||
'tier_6_42_yearly_commitment': 20,
|
||||
'tier_25_170_yearly_commitment': 100,
|
||||
}
|
||||
|
||||
@property
|
||||
def MAX_PARALLEL_AGENT_RUNS(self) -> int:
|
||||
"""
|
||||
|
|
|
@ -23,6 +23,8 @@ import { PublishDialog } from '@/components/agents/custom-agents-page/publish-di
|
|||
import { LoadingSkeleton } from '@/components/agents/custom-agents-page/loading-skeleton';
|
||||
import { NewAgentDialog } from '@/components/agents/new-agent-dialog';
|
||||
import { MarketplaceAgentPreviewDialog } from '@/components/agents/marketplace-agent-preview-dialog';
|
||||
import { AgentCountLimitDialog } from '@/components/agents/agent-count-limit-dialog';
|
||||
import { AgentCountLimitError } from '@/lib/api';
|
||||
|
||||
type ViewMode = 'grid' | 'list';
|
||||
type AgentSortOption = 'name' | 'created_at' | 'updated_at' | 'tools_count';
|
||||
|
@ -86,6 +88,8 @@ export default function AgentsPage() {
|
|||
|
||||
const [publishingAgentId, setPublishingAgentId] = useState<string | null>(null);
|
||||
const [showNewAgentDialog, setShowNewAgentDialog] = useState(false);
|
||||
const [showAgentLimitDialog, setShowAgentLimitDialog] = useState(false);
|
||||
const [agentLimitError, setAgentLimitError] = useState<AgentCountLimitError | null>(null);
|
||||
|
||||
const activeTab = useMemo(() => {
|
||||
return searchParams.get('tab') || 'my-agents';
|
||||
|
@ -146,7 +150,6 @@ export default function AgentsPage() {
|
|||
|
||||
if (marketplaceTemplates) {
|
||||
marketplaceTemplates.forEach(template => {
|
||||
|
||||
const item: MarketplaceTemplate = {
|
||||
id: template.template_id,
|
||||
creator_id: template.creator_id,
|
||||
|
@ -174,14 +177,12 @@ export default function AgentsPage() {
|
|||
item.creator_name?.toLowerCase().includes(searchLower);
|
||||
})();
|
||||
|
||||
if (!matchesSearch) return; // Skip items that don't match search
|
||||
if (!matchesSearch) return;
|
||||
|
||||
// Always add user's own templates to mineItems for the "mine" filter
|
||||
if (user?.id === template.creator_id) {
|
||||
mineItems.push(item);
|
||||
}
|
||||
|
||||
// Categorize all templates (including user's own) for the "all" view
|
||||
if (template.is_kortix_team) {
|
||||
kortixItems.push(item);
|
||||
} else {
|
||||
|
@ -392,6 +393,12 @@ export default function AgentsPage() {
|
|||
} catch (error: any) {
|
||||
console.error('Installation error:', error);
|
||||
|
||||
if (error instanceof AgentCountLimitError) {
|
||||
setAgentLimitError(error);
|
||||
setShowAgentLimitDialog(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (error.message?.includes('already in your library')) {
|
||||
toast.error('This agent is already in your library');
|
||||
} else if (error.message?.includes('Credential profile not found')) {
|
||||
|
@ -628,6 +635,15 @@ export default function AgentsPage() {
|
|||
onInstall={handlePreviewInstall}
|
||||
isInstalling={installingItemId === selectedItem?.id}
|
||||
/>
|
||||
{agentLimitError && (
|
||||
<AgentCountLimitDialog
|
||||
open={showAgentLimitDialog}
|
||||
onOpenChange={setShowAgentLimitDialog}
|
||||
currentCount={agentLimitError.detail.current_count}
|
||||
limit={agentLimitError.detail.limit}
|
||||
tierName={agentLimitError.detail.tier_name}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { PricingSection } from '@/components/home/sections/pricing-section';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
|
||||
interface AgentCountLimitDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
currentCount: number;
|
||||
limit: number;
|
||||
tierName: string;
|
||||
}
|
||||
|
||||
export const AgentCountLimitDialog: React.FC<AgentCountLimitDialogProps> = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
currentCount,
|
||||
limit,
|
||||
tierName,
|
||||
}) => {
|
||||
const returnUrl = typeof window !== 'undefined' ? window.location.href : '/';
|
||||
|
||||
const getNextTierRecommendation = () => {
|
||||
if (tierName === 'free') {
|
||||
return {
|
||||
name: 'Plus',
|
||||
price: '$20/month',
|
||||
agentLimit: 5,
|
||||
};
|
||||
} else if (tierName.includes('tier_2_20')) {
|
||||
return {
|
||||
name: 'Pro',
|
||||
price: '$50/month',
|
||||
agentLimit: 20,
|
||||
};
|
||||
} else if (tierName.includes('tier_6_50')) {
|
||||
return {
|
||||
name: 'Business',
|
||||
price: '$200/month',
|
||||
agentLimit: 100,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const nextTier = getNextTierRecommendation();
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-5xl h-auto overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="border border-amber-300 dark:border-amber-900 flex h-8 w-8 items-center justify-center rounded-full bg-amber-100 dark:bg-amber-900">
|
||||
<AlertTriangle className="h-4 w-4 text-amber-600 dark:text-amber-400" />
|
||||
</div>
|
||||
<DialogTitle className="text-lg font-semibold">
|
||||
Agent Limit Reached
|
||||
</DialogTitle>
|
||||
</div>
|
||||
<DialogDescription className="text-sm text-muted-foreground">
|
||||
You've reached the maximum number of agents allowed on your current plan.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="[&_.grid]:!grid-cols-4 [&_.grid]:gap-3 mt-8">
|
||||
<PricingSection
|
||||
returnUrl={returnUrl}
|
||||
showTitleAndTabs={false}
|
||||
insideDialog={true}
|
||||
showInfo={false}
|
||||
noPadding={true}
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
|
@ -11,6 +11,8 @@ import { ProfileConnector } from './installation/streamlined-profile-connector';
|
|||
import { CustomServerStep } from './installation/custom-server-step';
|
||||
import type { SetupStep } from './installation/types';
|
||||
import { useAnalyzeJsonForImport, useImportAgentFromJson, type JsonAnalysisResult, type JsonImportResult } from '@/hooks/react-query/agents/use-json-import';
|
||||
import { AgentCountLimitDialog } from './agent-count-limit-dialog';
|
||||
import { AgentCountLimitError } from '@/lib/api';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface JsonImportDialogProps {
|
||||
|
@ -34,6 +36,8 @@ export const JsonImportDialog: React.FC<JsonImportDialogProps> = ({
|
|||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [profileMappings, setProfileMappings] = useState<Record<string, string>>({});
|
||||
const [customMcpConfigs, setCustomMcpConfigs] = useState<Record<string, Record<string, any>>>({});
|
||||
const [showAgentLimitDialog, setShowAgentLimitDialog] = useState(false);
|
||||
const [agentLimitError, setAgentLimitError] = useState<AgentCountLimitError | null>(null);
|
||||
|
||||
const analyzeJsonMutation = useAnalyzeJsonForImport();
|
||||
const importJsonMutation = useImportAgentFromJson();
|
||||
|
@ -47,6 +51,8 @@ export const JsonImportDialog: React.FC<JsonImportDialogProps> = ({
|
|||
setCurrentStep(0);
|
||||
setProfileMappings({});
|
||||
setCustomMcpConfigs({});
|
||||
setShowAgentLimitDialog(false);
|
||||
setAgentLimitError(null);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -215,6 +221,13 @@ export const JsonImportDialog: React.FC<JsonImportDialogProps> = ({
|
|||
}
|
||||
onOpenChange(false);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
if (error instanceof AgentCountLimitError) {
|
||||
setAgentLimitError(error);
|
||||
setShowAgentLimitDialog(true);
|
||||
onOpenChange(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -380,7 +393,6 @@ export const JsonImportDialog: React.FC<JsonImportDialogProps> = ({
|
|||
Import Agent from JSON
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{analysis && !analysis.requires_setup && step === 'paste' && (
|
||||
<Alert>
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
|
@ -389,7 +401,6 @@ export const JsonImportDialog: React.FC<JsonImportDialogProps> = ({
|
|||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{analysis && analysis.requires_setup && step === 'paste' && (
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
|
@ -400,7 +411,6 @@ export const JsonImportDialog: React.FC<JsonImportDialogProps> = ({
|
|||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{analyzeJsonMutation.isError && step === 'paste' && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
|
@ -409,7 +419,6 @@ export const JsonImportDialog: React.FC<JsonImportDialogProps> = ({
|
|||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{importJsonMutation.isError && (step === 'setup' || step === 'importing') && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
|
@ -418,11 +427,19 @@ export const JsonImportDialog: React.FC<JsonImportDialogProps> = ({
|
|||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{step === 'paste' && renderPasteStep()}
|
||||
{step === 'setup' && renderSetupStep()}
|
||||
{step === 'importing' && renderImportingStep()}
|
||||
</DialogContent>
|
||||
{agentLimitError && (
|
||||
<AgentCountLimitDialog
|
||||
open={showAgentLimitDialog}
|
||||
onOpenChange={setShowAgentLimitDialog}
|
||||
currentCount={agentLimitError.detail.current_count}
|
||||
limit={agentLimitError.detail.limit}
|
||||
tierName={agentLimitError.detail.tier_name}
|
||||
/>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState, useRef } from 'react';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Loader2, Plus, FileJson, Code } from 'lucide-react';
|
||||
import {
|
||||
AlertDialog,
|
||||
|
@ -14,6 +14,8 @@ import {
|
|||
} from '@/components/ui/alert-dialog';
|
||||
import { useCreateNewAgent } from '@/hooks/react-query/agents/use-agents';
|
||||
import { JsonImportDialog } from './json-import-dialog';
|
||||
import { AgentCountLimitDialog } from './agent-count-limit-dialog';
|
||||
import { AgentCountLimitError } from '@/lib/api';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface NewAgentDialogProps {
|
||||
|
@ -25,19 +27,43 @@ interface NewAgentDialogProps {
|
|||
export function NewAgentDialog({ open, onOpenChange, onSuccess }: NewAgentDialogProps) {
|
||||
const [showJsonImport, setShowJsonImport] = useState(false);
|
||||
const [jsonImportText, setJsonImportText] = useState('');
|
||||
const [showAgentLimitDialog, setShowAgentLimitDialog] = useState(false);
|
||||
const [agentLimitError, setAgentLimitError] = useState<AgentCountLimitError | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const createNewAgentMutation = useCreateNewAgent();
|
||||
|
||||
useEffect(() => {
|
||||
console.log('[DEBUG] NewAgentDialog state:', {
|
||||
agentLimitError: !!agentLimitError,
|
||||
showAgentLimitDialog,
|
||||
errorDetail: agentLimitError?.detail
|
||||
});
|
||||
}, [agentLimitError, showAgentLimitDialog]);
|
||||
|
||||
const handleCreateNewAgent = () => {
|
||||
console.log('[DEBUG] Creating new agent...');
|
||||
createNewAgentMutation.mutate(undefined, {
|
||||
onSuccess: () => {
|
||||
console.log('[DEBUG] Agent created successfully');
|
||||
onOpenChange(false);
|
||||
onSuccess?.();
|
||||
},
|
||||
onError: () => {
|
||||
// Keep dialog open on error so user can see the error and try again
|
||||
// The useCreateNewAgent hook already shows error toasts
|
||||
onError: (error) => {
|
||||
console.log('[DEBUG] Error creating agent:', error);
|
||||
console.log('[DEBUG] Error type:', typeof error);
|
||||
console.log('[DEBUG] Error constructor:', error.constructor.name);
|
||||
console.log('[DEBUG] Is AgentCountLimitError?', error instanceof AgentCountLimitError);
|
||||
|
||||
if (error instanceof AgentCountLimitError) {
|
||||
console.log('[DEBUG] Setting agent limit error state');
|
||||
setAgentLimitError(error);
|
||||
setShowAgentLimitDialog(true);
|
||||
onOpenChange(false);
|
||||
} else {
|
||||
console.log('[DEBUG] Not an agent limit error, keeping dialog open');
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to create agent');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -147,6 +173,16 @@ export function NewAgentDialog({ open, onOpenChange, onSuccess }: NewAgentDialog
|
|||
onSuccess?.();
|
||||
}}
|
||||
/>
|
||||
|
||||
{agentLimitError && (
|
||||
<AgentCountLimitDialog
|
||||
open={showAgentLimitDialog}
|
||||
onOpenChange={setShowAgentLimitDialog}
|
||||
currentCount={agentLimitError.detail.current_count}
|
||||
limit={agentLimitError.detail.limit}
|
||||
tierName={agentLimitError.detail.tier_name}
|
||||
/>
|
||||
)}
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
|
@ -566,7 +566,7 @@ function PricingTier({
|
|||
variant={buttonVariant || 'default'}
|
||||
className={cn(
|
||||
'w-full font-medium transition-all duration-200',
|
||||
isCompact || insideDialog ? 'h-8 rounded-md text-xs' : 'h-10 rounded-full text-sm',
|
||||
isCompact || insideDialog ? 'h-8 text-xs' : 'h-10 rounded-full text-sm',
|
||||
buttonClassName,
|
||||
isPlanLoading && 'animate-pulse',
|
||||
)}
|
||||
|
@ -584,13 +584,17 @@ interface PricingSectionProps {
|
|||
showTitleAndTabs?: boolean;
|
||||
hideFree?: boolean;
|
||||
insideDialog?: boolean;
|
||||
showInfo?: boolean;
|
||||
noPadding?: boolean;
|
||||
}
|
||||
|
||||
export function PricingSection({
|
||||
returnUrl = typeof window !== 'undefined' ? window.location.href : '/',
|
||||
showTitleAndTabs = true,
|
||||
hideFree = false,
|
||||
insideDialog = false
|
||||
insideDialog = false,
|
||||
showInfo = true,
|
||||
noPadding = false,
|
||||
}: PricingSectionProps) {
|
||||
const [deploymentType, setDeploymentType] = useState<'cloud' | 'self-hosted'>(
|
||||
'cloud',
|
||||
|
@ -598,18 +602,14 @@ export function PricingSection({
|
|||
const { data: subscriptionData, isLoading: isFetchingPlan, error: subscriptionQueryError, refetch: refetchSubscription } = useSubscription();
|
||||
const subCommitmentQuery = useSubscriptionCommitment(subscriptionData?.subscription_id);
|
||||
|
||||
// Derive authentication and subscription status from the hook data
|
||||
const isAuthenticated = !!subscriptionData && subscriptionQueryError === null;
|
||||
const currentSubscription = subscriptionData || null;
|
||||
|
||||
// Determine default billing period based on user's current subscription
|
||||
const getDefaultBillingPeriod = useCallback((): 'monthly' | 'yearly' | 'yearly_commitment' => {
|
||||
if (!isAuthenticated || !currentSubscription) {
|
||||
// Default to yearly_commitment for non-authenticated users (the new yearly plans)
|
||||
return 'yearly_commitment';
|
||||
}
|
||||
|
||||
// Find current tier to determine if user is on monthly, yearly, or yearly commitment plan
|
||||
const currentTier = siteConfig.cloudPricingItems.find(
|
||||
(p) => p.stripePriceId === currentSubscription.price_id ||
|
||||
p.yearlyStripePriceId === currentSubscription.price_id ||
|
||||
|
@ -685,7 +685,7 @@ export function PricingSection({
|
|||
return (
|
||||
<section
|
||||
id="pricing"
|
||||
className={cn("flex flex-col items-center justify-center gap-10 w-full relative pb-12")}
|
||||
className={cn("flex flex-col items-center justify-center gap-10 w-full relative", noPadding ? "pb-0" : "pb-12")}
|
||||
>
|
||||
{showTitleAndTabs && (
|
||||
<>
|
||||
|
@ -747,14 +747,15 @@ export function PricingSection({
|
|||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-4 p-4 bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-800 rounded-lg max-w-2xl mx-auto">
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200 text-center">
|
||||
<strong>What are AI tokens?</strong> Tokens are units of text that AI models process.
|
||||
Your plan includes credits to spend on various AI models - the more complex the task,
|
||||
the more tokens used.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{showInfo && (
|
||||
<div className="mt-4 p-4 bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-800 rounded-lg max-w-2xl mx-auto">
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200 text-center">
|
||||
<strong>What are AI tokens?</strong> Tokens are units of text that AI models process.
|
||||
Your plan includes credits to spend on various AI models - the more complex the task,
|
||||
the more tokens used.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
);
|
||||
|
|
|
@ -42,6 +42,14 @@ export const useCreateAgent = () => {
|
|||
queryClient.setQueryData(agentKeys.detail(data.agent_id), data);
|
||||
toast.success('Agent created successfully');
|
||||
},
|
||||
onError: async (error) => {
|
||||
const { AgentCountLimitError } = await import('@/lib/api');
|
||||
if (error instanceof AgentCountLimitError) {
|
||||
return;
|
||||
}
|
||||
console.error('Error creating agent:', error);
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to create agent');
|
||||
},
|
||||
}
|
||||
)();
|
||||
};
|
||||
|
|
|
@ -73,7 +73,18 @@ export const useImportAgentFromJson = () => {
|
|||
const response = await backendApi.post('/agents/json/import', request);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
// Extract error message from different error formats
|
||||
const errorData = error.response?.data;
|
||||
const isAgentLimitError = (error.response?.status === 402) && (
|
||||
errorData?.error_code === 'AGENT_LIMIT_EXCEEDED' ||
|
||||
errorData?.detail?.error_code === 'AGENT_LIMIT_EXCEEDED'
|
||||
);
|
||||
|
||||
if (isAgentLimitError) {
|
||||
const { AgentCountLimitError } = await import('@/lib/api');
|
||||
const errorDetail = errorData?.detail || errorData;
|
||||
throw new AgentCountLimitError(error.response.status, errorDetail);
|
||||
}
|
||||
|
||||
const message = error.response?.data?.detail || error.message || 'Failed to import agent';
|
||||
throw new Error(message);
|
||||
}
|
||||
|
|
|
@ -256,6 +256,25 @@ export const createAgent = async (agentData: AgentCreateRequest): Promise<Agent>
|
|||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Unknown error' }));
|
||||
console.log('[DEBUG] Error response data:', errorData);
|
||||
console.log('[DEBUG] Response status:', response.status);
|
||||
console.log('[DEBUG] Error code check:', errorData.error_code);
|
||||
|
||||
// Check for agent limit error - handle both direct error_code and nested in detail
|
||||
const isAgentLimitError = (response.status === 402) && (
|
||||
errorData.error_code === 'AGENT_LIMIT_EXCEEDED' ||
|
||||
errorData.detail?.error_code === 'AGENT_LIMIT_EXCEEDED'
|
||||
);
|
||||
|
||||
if (isAgentLimitError) {
|
||||
console.log('[DEBUG] Converting to AgentCountLimitError');
|
||||
const { AgentCountLimitError } = await import('@/lib/api');
|
||||
// Use the nested detail if it exists, otherwise use the errorData directly
|
||||
const errorDetail = errorData.detail || errorData;
|
||||
throw new AgentCountLimitError(response.status, errorDetail);
|
||||
}
|
||||
|
||||
console.log('[DEBUG] Throwing generic error');
|
||||
throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
|
|
|
@ -447,6 +447,22 @@ export function useInstallTemplate() {
|
|||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: 'Unknown error' }));
|
||||
console.log('[DEBUG] Template install error data:', errorData);
|
||||
|
||||
// Check for agent limit error - handle both direct error_code and nested in detail
|
||||
const isAgentLimitError = (response.status === 402) && (
|
||||
errorData.error_code === 'AGENT_LIMIT_EXCEEDED' ||
|
||||
errorData.detail?.error_code === 'AGENT_LIMIT_EXCEEDED'
|
||||
);
|
||||
|
||||
if (isAgentLimitError) {
|
||||
console.log('[DEBUG] Converting template install to AgentCountLimitError');
|
||||
const { AgentCountLimitError } = await import('@/lib/api');
|
||||
// Use the nested detail if it exists, otherwise use the errorData directly
|
||||
const errorDetail = errorData.detail || errorData;
|
||||
throw new AgentCountLimitError(response.status, errorDetail);
|
||||
}
|
||||
|
||||
throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
|
|
|
@ -59,6 +59,36 @@ export class AgentRunLimitError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
export class AgentCountLimitError extends Error {
|
||||
status: number;
|
||||
detail: {
|
||||
message: string;
|
||||
current_count: number;
|
||||
limit: number;
|
||||
tier_name: string;
|
||||
error_code: string;
|
||||
};
|
||||
|
||||
constructor(
|
||||
status: number,
|
||||
detail: {
|
||||
message: string;
|
||||
current_count: number;
|
||||
limit: number;
|
||||
tier_name: string;
|
||||
error_code: string;
|
||||
[key: string]: any;
|
||||
},
|
||||
message?: string,
|
||||
) {
|
||||
super(message || detail.message || `Agent Count Limit Exceeded: ${status}`);
|
||||
this.name = 'AgentCountLimitError';
|
||||
this.status = status;
|
||||
this.detail = detail;
|
||||
Object.setPrototypeOf(this, AgentCountLimitError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export class NoAccessTokenAvailableError extends Error {
|
||||
constructor(message?: string, options?: { cause?: Error }) {
|
||||
super(message || 'No access token available', options);
|
||||
|
|
|
@ -124,6 +124,7 @@ export const siteConfig = {
|
|||
hours: '60 min',
|
||||
features: [
|
||||
'$5 free AI tokens included',
|
||||
'2 custom agents',
|
||||
'Public projects',
|
||||
'Basic Models',
|
||||
'Community support',
|
||||
|
@ -145,6 +146,7 @@ export const siteConfig = {
|
|||
hours: '2 hours',
|
||||
features: [
|
||||
'$20 AI token credits/month',
|
||||
'5 custom agents',
|
||||
'Private projects',
|
||||
'Premium AI Models',
|
||||
'Community support',
|
||||
|
@ -168,6 +170,7 @@ export const siteConfig = {
|
|||
hours: '6 hours',
|
||||
features: [
|
||||
'$50 AI token credits/month',
|
||||
'20 custom agents',
|
||||
'Private projects',
|
||||
'Premium AI Models',
|
||||
'Community support',
|
||||
|
@ -190,6 +193,7 @@ export const siteConfig = {
|
|||
hours: '12 hours',
|
||||
features: [
|
||||
'$100 AI token credits/month',
|
||||
'20 custom agents',
|
||||
'Private projects',
|
||||
'Premium AI Models',
|
||||
'Community support',
|
||||
|
@ -212,6 +216,7 @@ export const siteConfig = {
|
|||
hours: '25 hours',
|
||||
features: [
|
||||
'$200 AI token credits/month',
|
||||
'100 custom agents',
|
||||
'Private projects',
|
||||
'Premium AI Models',
|
||||
'Priority support',
|
||||
|
|
Loading…
Reference in New Issue