diff --git a/backend/agentpress/thread_manager.py b/backend/agentpress/thread_manager.py index 644a90d5..f9f19700 100644 --- a/backend/agentpress/thread_manager.py +++ b/backend/agentpress/thread_manager.py @@ -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 diff --git a/backend/services/llm.py b/backend/services/llm.py index 06d92d7e..ea49b59c 100644 --- a/backend/services/llm.py +++ b/backend/services/llm.py @@ -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") diff --git a/frontend/src/app/(dashboard)/agents/_components/mcp/config-dialog.tsx b/frontend/src/app/(dashboard)/agents/_components/mcp/config-dialog.tsx index 1104f3cd..0c865d3c 100644 --- a/frontend/src/app/(dashboard)/agents/_components/mcp/config-dialog.tsx +++ b/frontend/src/app/(dashboard)/agents/_components/mcp/config-dialog.tsx @@ -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 = ({ }; return ( - - - Configure {server.displayName || server.name} + + +
+ {server.iconUrl ? ( +
+ {server.displayName +
+
+ ) : ( +
+ +
+ )} +
+ Configure {server.displayName || server.name} +
+
Set up the connection and select which tools to enable for this MCP server. @@ -62,75 +80,95 @@ export const ConfigDialog: React.FC = ({
) : ( - -
- {serverDetails?.connections?.[0]?.configSchema?.properties && ( -
-

Connection Settings

- {Object.entries(serverDetails.connections[0].configSchema.properties).map(([key, schema]: [string, any]) => ( -
- - setConfig({ ...config, [key]: e.target.value })} - /> - {schema.description && ( -

{schema.description}

- )} -
- ))} -
- )} - - {serverDetails?.tools && serverDetails.tools.length > 0 && ( -
-
-

Available Tools

- - {selectedTools.size} of {serverDetails.tools.length} selected - -
-
- {serverDetails.tools.map((tool: any) => ( -
handleToolToggle(tool.name)} - > - {}} - className="mt-1" - /> -
-
{tool.name}
- {tool.description && ( -
- {tool.description} -
+
+
+

+
+ Connection Settings +

+ {serverDetails?.connections?.[0]?.configSchema?.properties ? ( + +
+ {Object.entries(serverDetails.connections[0].configSchema.properties).map(([key, schema]: [string, any]) => ( +
+
+ + setConfig({ ...config, [key]: e.target.value })} + className="bg-background" + />
))}
+ + ) : ( +
+

No configuration required

)}
- + +
+
+

+
+ Available Tools +

+ {serverDetails?.tools && serverDetails.tools.length > 0 && ( + + {selectedTools.size} of {serverDetails.tools.length} selected + + )} +
+ {serverDetails?.tools && serverDetails.tools.length > 0 ? ( + +
+
+ {serverDetails.tools.map((tool: any) => ( +
handleToolToggle(tool.name)} + > + {}} + className="mt-1 accent-primary" + /> +
+
{tool.name}
+ {tool.description && ( +
+ {tool.description} +
+ )} +
+
+ ))} +
+
+
+ ) : ( +
+

No tools available

+
+ )} +
+
)} @@ -140,7 +178,9 @@ export const ConfigDialog: React.FC = ({ diff --git a/frontend/src/components/home/sections/navbar.tsx b/frontend/src/components/home/sections/navbar.tsx index e9ad2c28..034a40d1 100644 --- a/frontend/src/components/home/sections/navbar.tsx +++ b/frontend/src/components/home/sections/navbar.tsx @@ -133,7 +133,7 @@ export function Navbar() { width={140} height={22} priority - /> + /> @@ -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 )}
@@ -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 )}
@@ -286,5 +286,5 @@ export function Navbar() { )} - ); + ); } diff --git a/frontend/src/components/thread/content/ThreadContent.tsx b/frontend/src/components/thread/content/ThreadContent.tsx index aa66837c..0bca2a65 100644 --- a/frontend/src/components/thread/content/ThreadContent.tsx +++ b/frontend/src/components/thread/content/ThreadContent.tsx @@ -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( +
+ {askText} + {renderAttachments(attachmentArray, fileViewerHandler, sandboxId, project)} +
+ ); + } 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( +
+ +
+ ); } - - contentParts.push( -
- -
- ); }); lastIndex = match.index + match[0].length; @@ -224,7 +243,7 @@ export function renderMarkdownContent( {paramDisplay && {paramDisplay}}
- ); + ); } lastIndex = xmlRegex.lastIndex; } diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index d9d1e5d0..7d6d1781 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -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 => { } = 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 => { 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 => { } = 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 => { 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 => { } = 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 => { 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 => } = 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 => 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 => { } = 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 => { 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