Merge branch 'PRODUCTION' of https://github.com/kortix-ai/suna into PRODUCTION

This commit is contained in:
Soumyadas15 2025-06-09 19:12:40 +05:30
commit c802fa5745
6 changed files with 210 additions and 116 deletions

View File

@ -178,7 +178,7 @@ class ThreadManager:
elif 'gemini' in llm_model.lower():
max_tokens = 1000 * 1000 - 300000
elif 'deepseek' in llm_model.lower():
max_tokens = 163 * 1000 - 32000
max_tokens = 128 * 1000 - 28000
else:
max_tokens = 41 * 1000 - 10000

View File

@ -181,7 +181,7 @@ def prepare_params(
item["cache_control"] = {"type": "ephemeral"}
break # Apply to the first text block only for system prompt
# 2. Find and process relevant user and assistant messages
# 2. Find and process relevant user and assistant messages (limit to 4 max)
last_user_idx = -1
second_last_user_idx = -1
last_assistant_idx = -1
@ -197,9 +197,10 @@ def prepare_params(
if last_assistant_idx == -1:
last_assistant_idx = i
# Stop searching if we've found all needed messages
if last_user_idx != -1 and second_last_user_idx != -1 and last_assistant_idx != -1:
break
# Stop searching if we've found all needed messages (system, last user, second last user, last assistant)
found_count = sum(idx != -1 for idx in [last_user_idx, second_last_user_idx, last_assistant_idx])
if found_count >= 3:
break
# Helper function to apply cache control
def apply_cache_control(message_idx: int, message_role: str):
@ -219,7 +220,9 @@ def prepare_params(
if "cache_control" not in item:
item["cache_control"] = {"type": "ephemeral"}
# Apply cache control to the identified messages
# Apply cache control to the identified messages (max 4: system, last user, second last user, last assistant)
# System message is always at index 0 if present
apply_cache_control(0, "system")
apply_cache_control(last_user_idx, "last user")
apply_cache_control(second_last_user_idx, "second last user")
apply_cache_control(last_assistant_idx, "last assistant")

View File

@ -4,7 +4,7 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Loader2 } from 'lucide-react';
import { Loader2, Save, Sparkles } from 'lucide-react';
import { useMCPServerDetails } from '@/hooks/react-query/mcp/use-mcp-servers';
import { cn } from '@/lib/utils';
import { MCPConfiguration } from './types';
@ -49,9 +49,27 @@ export const ConfigDialog: React.FC<ConfigDialogProps> = ({
};
return (
<DialogContent className="max-w-2xl max-h-[85vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle>Configure {server.displayName || server.name}</DialogTitle>
<DialogContent className="max-w-5xl max-h-[85vh] overflow-hidden flex flex-col">
<DialogHeader className="flex-shrink-0">
<div className="flex items-center gap-3 mb-2">
{server.iconUrl ? (
<div className="relative">
<img
src={server.iconUrl}
alt={server.displayName || server.name}
className="w-8 h-8 rounded-lg shadow-sm"
/>
<div className="absolute inset-0 bg-gradient-to-br from-white/20 to-transparent rounded-lg pointer-events-none" />
</div>
) : (
<div className="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center shadow-sm border border-primary/20">
<Sparkles className="h-4 w-4 text-primary" />
</div>
)}
<div>
<DialogTitle className="text-lg">Configure {server.displayName || server.name}</DialogTitle>
</div>
</div>
<DialogDescription>
Set up the connection and select which tools to enable for this MCP server.
</DialogDescription>
@ -62,75 +80,95 @@ export const ConfigDialog: React.FC<ConfigDialogProps> = ({
<Loader2 className="h-6 w-6 animate-spin" />
</div>
) : (
<ScrollArea className="flex-1 px-1">
<div className="space-y-6">
{serverDetails?.connections?.[0]?.configSchema?.properties && (
<div className="space-y-4">
<h3 className="text-sm font-semibold">Connection Settings</h3>
{Object.entries(serverDetails.connections[0].configSchema.properties).map(([key, schema]: [string, any]) => (
<div key={key} className="space-y-2">
<Label htmlFor={key}>
{schema.title || key}
{serverDetails.connections[0].configSchema.required?.includes(key) && (
<span className="text-destructive ml-1">*</span>
)}
</Label>
<Input
id={key}
type={schema.format === 'password' ? 'password' : 'text'}
placeholder={schema.description || `Enter ${key}`}
value={config[key] || ''}
onChange={(e) => setConfig({ ...config, [key]: e.target.value })}
/>
{schema.description && (
<p className="text-xs text-muted-foreground">{schema.description}</p>
)}
</div>
))}
</div>
)}
{serverDetails?.tools && serverDetails.tools.length > 0 && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold">Available Tools</h3>
<span className="text-xs text-muted-foreground">
{selectedTools.size} of {serverDetails.tools.length} selected
</span>
</div>
<div className="space-y-2 max-h-[300px] overflow-y-auto">
{serverDetails.tools.map((tool: any) => (
<div
key={tool.name}
className={cn(
"flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-colors",
selectedTools.has(tool.name)
? "bg-primary/5 border-primary"
: "hover:bg-muted/50"
)}
onClick={() => handleToolToggle(tool.name)}
>
<input
type="checkbox"
checked={selectedTools.has(tool.name)}
onChange={() => {}}
className="mt-1"
/>
<div className="flex-1">
<div className="font-medium text-sm">{tool.name}</div>
{tool.description && (
<div className="text-xs text-muted-foreground mt-1">
{tool.description}
</div>
<div className="flex-1 min-h-0 grid grid-cols-2 gap-6 px-1">
<div className="flex flex-col min-h-0">
<h3 className="text-sm font-semibold flex items-center gap-2 mb-4">
<div className="w-1 h-4 bg-primary rounded-full" />
Connection Settings
</h3>
{serverDetails?.connections?.[0]?.configSchema?.properties ? (
<ScrollArea className="border bg-muted/30 rounded-lg p-4 flex-1 min-h-0">
<div>
{Object.entries(serverDetails.connections[0].configSchema.properties).map(([key, schema]: [string, any]) => (
<div key={key} className="space-y-2">
<Label htmlFor={key} className="text-sm font-medium">
{schema.title || key}
{serverDetails.connections[0].configSchema.required?.includes(key) && (
<span className="text-destructive ml-1">*</span>
)}
</div>
</Label>
<Input
id={key}
type={schema.format === 'password' ? 'password' : 'text'}
placeholder={schema.description || `Enter ${key}`}
value={config[key] || ''}
onChange={(e) => setConfig({ ...config, [key]: e.target.value })}
className="bg-background"
/>
</div>
))}
</div>
</ScrollArea>
) : (
<div className="flex-1 flex items-center justify-center text-muted-foreground">
<p className="text-sm">No configuration required</p>
</div>
)}
</div>
</ScrollArea>
<div className="flex flex-col min-h-0">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold flex items-center gap-2">
<div className="w-1 h-4 bg-primary rounded-full" />
Available Tools
</h3>
{serverDetails?.tools && serverDetails.tools.length > 0 && (
<span className="text-xs text-muted-foreground bg-muted/50 px-2 py-1 rounded-full">
{selectedTools.size} of {serverDetails.tools.length} selected
</span>
)}
</div>
{serverDetails?.tools && serverDetails.tools.length > 0 ? (
<ScrollArea className="-mt-1 border bg-muted/30 rounded-lg p-4 flex-1 min-h-0">
<div>
<div className="space-y-2">
{serverDetails.tools.map((tool: any) => (
<div
key={tool.name}
className={cn(
"flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-all duration-200",
selectedTools.has(tool.name)
? "bg-primary/10 border-primary/30 shadow-sm"
: "hover:bg-muted/30 border-border/50 hover:border-border"
)}
onClick={() => handleToolToggle(tool.name)}
>
<input
type="checkbox"
checked={selectedTools.has(tool.name)}
onChange={() => {}}
className="mt-1 accent-primary"
/>
<div className="flex-1">
<div className="font-medium text-sm">{tool.name}</div>
{tool.description && (
<div className="text-xs text-muted-foreground mt-1">
{tool.description}
</div>
)}
</div>
</div>
))}
</div>
</div>
</ScrollArea>
) : (
<div className="flex-1 flex items-center justify-center text-muted-foreground">
<p className="text-sm">No tools available</p>
</div>
)}
</div>
</div>
)}
<DialogFooter>
@ -140,7 +178,9 @@ export const ConfigDialog: React.FC<ConfigDialogProps> = ({
<Button
onClick={handleSave}
disabled={isLoading}
className="bg-primary hover:bg-primary/90"
>
<Save className="h-4 w-4" />
Save Configuration
</Button>
</DialogFooter>

View File

@ -133,7 +133,7 @@ export function Navbar() {
width={140}
height={22}
priority
/>
/>
</Link>
<NavMenu />
@ -161,7 +161,7 @@ export function Navbar() {
className="bg-secondary h-8 hidden md:flex items-center justify-center text-sm font-normal tracking-wide rounded-full text-primary-foreground dark:text-secondary-foreground w-fit px-4 shadow-[inset_0_1px_2px_rgba(255,255,255,0.25),0_3px_3px_-1.5px_rgba(16,24,40,0.06),0_1px_1px_rgba(16,24,40,0.08)] border border-white/[0.12]"
href="/auth"
>
Signup
Get started
</Link>
)}
</div>
@ -273,7 +273,7 @@ export function Navbar() {
href="/auth"
className="bg-secondary h-8 flex items-center justify-center text-sm font-normal tracking-wide rounded-full text-primary-foreground dark:text-secondary-foreground w-full px-4 shadow-[inset_0_1px_2px_rgba(255,255,255,0.25),0_3px_3px_-1.5px_rgba(16,24,40,0.06),0_1px_1px_rgba(16,24,40,0.08)] border border-white/[0.12] hover:bg-secondary/80 transition-all ease-out active:scale-95"
>
Signup
Get Started
</Link>
)}
<div className="flex justify-between">
@ -286,5 +286,5 @@ export function Navbar() {
)}
</AnimatePresence>
</header>
);
);
}

View File

@ -116,34 +116,53 @@ export function renderMarkdownContent(
toolCalls.forEach((toolCall, index) => {
const toolName = toolCall.functionName.replace(/_/g, '-');
const IconComponent = getToolIcon(toolName);
// Extract primary parameter for display
let paramDisplay = '';
if (toolCall.parameters.file_path) {
paramDisplay = toolCall.parameters.file_path;
} else if (toolCall.parameters.command) {
paramDisplay = toolCall.parameters.command;
} else if (toolCall.parameters.query) {
paramDisplay = toolCall.parameters.query;
} else if (toolCall.parameters.url) {
paramDisplay = toolCall.parameters.url;
if (toolName === 'ask') {
// Handle ask tool specially - extract text and attachments
const askText = toolCall.parameters.text || '';
const attachments = toolCall.parameters.attachments || [];
// Convert single attachment to array for consistent handling
const attachmentArray = Array.isArray(attachments) ? attachments :
(typeof attachments === 'string' ? attachments.split(',').map(a => a.trim()) : []);
// Render ask tool content with attachment UI
contentParts.push(
<div key={`ask-${match.index}-${index}`} className="space-y-3">
<Markdown className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none break-words [&>:first-child]:mt-0 prose-headings:mt-3">{askText}</Markdown>
{renderAttachments(attachmentArray, fileViewerHandler, sandboxId, project)}
</div>
);
} else {
const IconComponent = getToolIcon(toolName);
// Extract primary parameter for display
let paramDisplay = '';
if (toolCall.parameters.file_path) {
paramDisplay = toolCall.parameters.file_path;
} else if (toolCall.parameters.command) {
paramDisplay = toolCall.parameters.command;
} else if (toolCall.parameters.query) {
paramDisplay = toolCall.parameters.query;
} else if (toolCall.parameters.url) {
paramDisplay = toolCall.parameters.url;
}
contentParts.push(
<div key={`tool-${match.index}-${index}`} className="my-1">
<button
onClick={() => handleToolClick(messageId, toolName)}
className="inline-flex items-center gap-1.5 py-1 px-1 text-xs text-muted-foreground bg-muted hover:bg-muted/80 rounded-md transition-colors cursor-pointer border border-neutral-200 dark:border-neutral-700/50"
>
<div className='border-2 bg-gradient-to-br from-neutral-200 to-neutral-300 dark:from-neutral-700 dark:to-neutral-800 flex items-center justify-center p-0.5 rounded-sm border-neutral-400/20 dark:border-neutral-600'>
<IconComponent className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
</div>
<span className="font-mono text-xs text-foreground">{getUserFriendlyToolName(toolName)}</span>
{paramDisplay && <span className="ml-1 text-muted-foreground truncate max-w-[200px]" title={paramDisplay}>{paramDisplay}</span>}
</button>
</div>
);
}
contentParts.push(
<div key={`tool-${match.index}-${index}`} className="my-1">
<button
onClick={() => handleToolClick(messageId, toolName)}
className="inline-flex items-center gap-1.5 py-1 px-1 text-xs text-muted-foreground bg-muted hover:bg-muted/80 rounded-md transition-colors cursor-pointer border border-neutral-200 dark:border-neutral-700/50"
>
<div className='border-2 bg-gradient-to-br from-neutral-200 to-neutral-300 dark:from-neutral-700 dark:to-neutral-800 flex items-center justify-center p-0.5 rounded-sm border-neutral-400/20 dark:border-neutral-600'>
<IconComponent className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
</div>
<span className="font-mono text-xs text-foreground">{getUserFriendlyToolName(toolName)}</span>
{paramDisplay && <span className="ml-1 text-muted-foreground truncate max-w-[200px]" title={paramDisplay}>{paramDisplay}</span>}
</button>
</div>
);
});
lastIndex = match.index + match[0].length;
@ -224,7 +243,7 @@ export function renderMarkdownContent(
{paramDisplay && <span className="ml-1 text-muted-foreground truncate max-w-[200px]" title={paramDisplay}>{paramDisplay}</span>}
</button>
</div>
);
);
}
lastIndex = xmlRegex.lastIndex;
}

View File

@ -29,6 +29,13 @@ export class BillingError extends Error {
}
}
export class NoAccessTokenAvailableError extends Error {
constructor(message?: string, options?: { cause?: Error }) {
super(message || 'No access token available', options);
}
name = 'NoAccessTokenAvailableError';
}
// Type Definitions (moved from potential separate file for clarity)
export type Project = {
id: string;
@ -541,7 +548,7 @@ export const startAgent = async (
} = await supabase.auth.getSession();
if (!session?.access_token) {
throw new Error('No access token available');
throw new NoAccessTokenAvailableError();
}
// Check if backend URL is configured
@ -633,6 +640,10 @@ export const startAgent = async (
throw error;
}
if (error instanceof NoAccessTokenAvailableError) {
throw error;
}
console.error('[API] Failed to start agent:', error);
// Handle different error types with appropriate user messages
@ -673,7 +684,7 @@ export const stopAgent = async (agentRunId: string): Promise<void> => {
} = await supabase.auth.getSession();
if (!session?.access_token) {
const authError = new Error('No access token available');
const authError = new NoAccessTokenAvailableError();
handleApiError(authError, { operation: 'stop agent', resource: 'AI assistant' });
throw authError;
}
@ -714,7 +725,7 @@ export const getAgentStatus = async (agentRunId: string): Promise<AgentRun> => {
if (!session?.access_token) {
console.error('[API] No access token available for getAgentStatus');
throw new Error('No access token available');
throw new NoAccessTokenAvailableError();
}
const url = `${API_URL}/agent-run/${agentRunId}`;
@ -771,7 +782,7 @@ export const getAgentRuns = async (threadId: string): Promise<AgentRun[]> => {
} = await supabase.auth.getSession();
if (!session?.access_token) {
throw new Error('No access token available');
throw new NoAccessTokenAvailableError();
}
const response = await fetch(`${API_URL}/thread/${threadId}/agent-runs`, {
@ -789,6 +800,10 @@ export const getAgentRuns = async (threadId: string): Promise<AgentRun[]> => {
const data = await response.json();
return data.agent_runs || [];
} catch (error) {
if (error instanceof NoAccessTokenAvailableError) {
throw error;
}
console.error('Failed to get agent runs:', error);
handleApiError(error, { operation: 'load agent runs', resource: 'conversation history' });
throw error;
@ -875,8 +890,9 @@ export const streamAgent = (
} = await supabase.auth.getSession();
if (!session?.access_token) {
const authError = new NoAccessTokenAvailableError();
console.error('[STREAM] No auth token available');
callbacks.onError(new Error('Authentication required'));
callbacks.onError(authError);
callbacks.onClose();
return;
}
@ -1398,7 +1414,7 @@ export const initiateAgent = async (
} = await supabase.auth.getSession();
if (!session?.access_token) {
throw new Error('No access token available');
throw new NoAccessTokenAvailableError();
}
if (!API_URL) {
@ -1570,7 +1586,7 @@ export const createCheckoutSession = async (
} = await supabase.auth.getSession();
if (!session?.access_token) {
throw new Error('No access token available');
throw new NoAccessTokenAvailableError();
}
const response = await fetch(`${API_URL}/billing/create-checkout-session`, {
@ -1637,7 +1653,7 @@ export const createPortalSession = async (
} = await supabase.auth.getSession();
if (!session?.access_token) {
throw new Error('No access token available');
throw new NoAccessTokenAvailableError();
}
const response = await fetch(`${API_URL}/billing/create-portal-session`, {
@ -1679,7 +1695,7 @@ export const getSubscription = async (): Promise<SubscriptionStatus> => {
} = await supabase.auth.getSession();
if (!session?.access_token) {
throw new Error('No access token available');
throw new NoAccessTokenAvailableError();
}
const response = await fetch(`${API_URL}/billing/subscription`, {
@ -1703,6 +1719,10 @@ export const getSubscription = async (): Promise<SubscriptionStatus> => {
return response.json();
} catch (error) {
if (error instanceof NoAccessTokenAvailableError) {
throw error;
}
console.error('Failed to get subscription:', error);
handleApiError(error, { operation: 'load subscription', resource: 'billing information' });
throw error;
@ -1717,7 +1737,7 @@ export const getAvailableModels = async (): Promise<AvailableModelsResponse> =>
} = await supabase.auth.getSession();
if (!session?.access_token) {
throw new Error('No access token available');
throw new NoAccessTokenAvailableError();
}
const response = await fetch(`${API_URL}/billing/available-models`, {
@ -1741,6 +1761,10 @@ export const getAvailableModels = async (): Promise<AvailableModelsResponse> =>
return response.json();
} catch (error) {
if (error instanceof NoAccessTokenAvailableError) {
throw error;
}
console.error('Failed to get available models:', error);
handleApiError(error, { operation: 'load available models', resource: 'AI models' });
throw error;
@ -1756,7 +1780,7 @@ export const checkBillingStatus = async (): Promise<BillingStatusResponse> => {
} = await supabase.auth.getSession();
if (!session?.access_token) {
throw new Error('No access token available');
throw new NoAccessTokenAvailableError();
}
const response = await fetch(`${API_URL}/billing/check-status`, {
@ -1780,6 +1804,10 @@ export const checkBillingStatus = async (): Promise<BillingStatusResponse> => {
return response.json();
} catch (error) {
if (error instanceof NoAccessTokenAvailableError) {
throw error;
}
console.error('Failed to check billing status:', error);
throw error;
}
@ -1799,7 +1827,7 @@ export const transcribeAudio = async (audioFile: File): Promise<TranscriptionRes
} = await supabase.auth.getSession();
if (!session?.access_token) {
throw new Error('No access token available');
throw new NoAccessTokenAvailableError();
}
const formData = new FormData();
@ -1828,6 +1856,10 @@ export const transcribeAudio = async (audioFile: File): Promise<TranscriptionRes
return response.json();
} catch (error) {
if (error instanceof NoAccessTokenAvailableError) {
throw error;
}
console.error('Failed to transcribe audio:', error);
handleApiError(error, { operation: 'transcribe audio', resource: 'speech-to-text' });
throw error;
@ -1841,7 +1873,7 @@ export const getAgentBuilderChatHistory = async (agentId: string): Promise<{mess
} = await supabase.auth.getSession();
if (!session?.access_token) {
throw new Error('No access token available');
throw new NoAccessTokenAvailableError();
}
const response = await fetch(`${API_URL}/agents/${agentId}/builder-chat-history`, {