mirror of https://github.com/kortix-ai/suna.git
commit
fb30938caa
|
@ -9,7 +9,7 @@ from core.ai_models import model_manager
|
|||
|
||||
from .api_models import (
|
||||
AgentUpdateRequest, AgentResponse, AgentVersionResponse, AgentsResponse,
|
||||
PaginationInfo, AgentCreateRequest
|
||||
PaginationInfo, AgentCreateRequest, AgentIconGenerationRequest, AgentIconGenerationResponse
|
||||
)
|
||||
from . import core_utils as utils
|
||||
from .core_utils import _get_version_service, merge_custom_mcps
|
||||
|
@ -813,4 +813,34 @@ async def create_agent(
|
|||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating agent for user {user_id}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to create agent: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to create agent: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/agents/generate-icon", response_model=AgentIconGenerationResponse)
|
||||
async def generate_agent_icon(
|
||||
request: AgentIconGenerationRequest,
|
||||
user_id: str = Depends(verify_and_get_user_id_from_jwt)
|
||||
):
|
||||
"""Generate an appropriate icon and colors for an agent based on its name and description."""
|
||||
logger.debug(f"Generating icon and colors for agent: {request.name}")
|
||||
|
||||
try:
|
||||
from .core_utils import generate_agent_icon_and_colors
|
||||
|
||||
result = await generate_agent_icon_and_colors(
|
||||
name=request.name,
|
||||
description=request.description
|
||||
)
|
||||
|
||||
response = AgentIconGenerationResponse(
|
||||
icon_name=result["icon_name"],
|
||||
icon_color=result["icon_color"],
|
||||
icon_background=result["icon_background"]
|
||||
)
|
||||
|
||||
logger.debug(f"Generated agent icon: {response.icon_name}, colors: {response.icon_color}/{response.icon_background}")
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating agent icon for user {user_id}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to generate agent icon: {str(e)}")
|
|
@ -18,6 +18,8 @@ from .agents import (
|
|||
ThreadAgentResponse,
|
||||
AgentExportData,
|
||||
AgentImportRequest,
|
||||
AgentIconGenerationRequest,
|
||||
AgentIconGenerationResponse,
|
||||
)
|
||||
|
||||
from .threads import (
|
||||
|
@ -46,6 +48,8 @@ __all__ = [
|
|||
"ThreadAgentResponse",
|
||||
"AgentExportData",
|
||||
"AgentImportRequest",
|
||||
"AgentIconGenerationRequest",
|
||||
"AgentIconGenerationResponse",
|
||||
|
||||
# Thread models
|
||||
"AgentStartRequest",
|
||||
|
|
|
@ -124,3 +124,16 @@ class AgentImportRequest(BaseModel):
|
|||
import_as_new: bool = True # Always true, only creating new agents is supported
|
||||
|
||||
|
||||
class AgentIconGenerationRequest(BaseModel):
|
||||
"""Request model for generating agent icon and colors."""
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class AgentIconGenerationResponse(BaseModel):
|
||||
"""Response model for generated agent icon and colors."""
|
||||
icon_name: str
|
||||
icon_color: str
|
||||
icon_background: str
|
||||
|
||||
|
||||
|
|
|
@ -248,7 +248,7 @@ async def generate_and_update_project_name(project_id: str, prompt: str):
|
|||
cleaned_content = raw_content.strip('\'" \n\t{}')
|
||||
if cleaned_content:
|
||||
generated_name = cleaned_content[:50] # Limit fallback title length
|
||||
selected_icon = "message-circle" # Default icon
|
||||
selected_icon = "message-circle" # Default icon
|
||||
else:
|
||||
logger.warning(f"Failed to get valid response from LLM for project {project_id} naming. Response: {response}")
|
||||
|
||||
|
@ -275,6 +275,111 @@ async def generate_and_update_project_name(project_id: str, prompt: str):
|
|||
# No need to disconnect DBConnection singleton instance here
|
||||
logger.debug(f"Finished background naming and icon selection task for project: {project_id}")
|
||||
|
||||
async def generate_agent_icon_and_colors(name: str, description: str = None) -> dict:
|
||||
"""Generates an agent icon and colors using an LLM based on agent name and description."""
|
||||
logger.debug(f"Generating icon and colors for agent: {name}")
|
||||
try:
|
||||
model_name = "openai/gpt-5-nano"
|
||||
|
||||
# Use pre-loaded Lucide React icons (loaded once at module level)
|
||||
relevant_icons = RELEVANT_ICONS
|
||||
|
||||
# Use exact colors from frontend presetColors array
|
||||
frontend_colors = [
|
||||
"#000000", "#FFFFFF", "#6366F1", "#10B981", "#F59E0B",
|
||||
"#EF4444", "#8B5CF6", "#EC4899", "#14B8A6", "#F97316",
|
||||
"#06B6D4", "#84CC16", "#F43F5E", "#A855F7", "#3B82F6"
|
||||
]
|
||||
|
||||
agent_context = f"Agent name: {name}"
|
||||
if description:
|
||||
agent_context += f"\nAgent description: {description}"
|
||||
|
||||
system_prompt = f"""You are a helpful assistant that selects appropriate icons and colors for AI agents based on their name and description.
|
||||
|
||||
Available Lucide React icons to choose from:
|
||||
{', '.join(relevant_icons)}
|
||||
|
||||
Available colors (hex codes):
|
||||
{', '.join(frontend_colors)}
|
||||
|
||||
Respond with a JSON object containing:
|
||||
- "icon": The most appropriate icon name from the available icons
|
||||
- "background_color": A background color hex code from the available colors
|
||||
- "text_color": A text color hex code from the available colors (choose one that contrasts well with the background)
|
||||
|
||||
Example response:
|
||||
{{"icon": "youtube", "background_color": "#EF4444", "text_color": "#FFFFFF"}}"""
|
||||
|
||||
user_message = f"Select the most appropriate icon and color scheme for this AI agent:\n{agent_context}"
|
||||
messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_message}]
|
||||
|
||||
logger.debug(f"Calling LLM ({model_name}) for agent icon and color generation.")
|
||||
response = await make_llm_api_call(
|
||||
messages=messages,
|
||||
model_name=model_name,
|
||||
max_tokens=4000,
|
||||
temperature=0.7,
|
||||
response_format={"type": "json_object"}
|
||||
)
|
||||
|
||||
# Default fallback values
|
||||
result = {
|
||||
"icon_name": "bot",
|
||||
"icon_color": "#FFFFFF",
|
||||
"icon_background": "#6366F1"
|
||||
}
|
||||
|
||||
if response and response.get('choices') and response['choices'][0].get('message'):
|
||||
raw_content = response['choices'][0]['message'].get('content', '').strip()
|
||||
try:
|
||||
parsed_response = json.loads(raw_content)
|
||||
|
||||
if isinstance(parsed_response, dict):
|
||||
# Extract and validate icon
|
||||
icon = parsed_response.get('icon', '').strip()
|
||||
if icon and icon in relevant_icons:
|
||||
result["icon_name"] = icon
|
||||
logger.debug(f"LLM selected icon: '{icon}'")
|
||||
else:
|
||||
logger.warning(f"LLM selected invalid icon '{icon}', using default 'bot'")
|
||||
|
||||
# Extract and validate colors
|
||||
bg_color = parsed_response.get('background_color', '').strip()
|
||||
text_color = parsed_response.get('text_color', '').strip()
|
||||
|
||||
if bg_color in frontend_colors:
|
||||
result["icon_background"] = bg_color
|
||||
logger.debug(f"LLM selected background color: '{bg_color}'")
|
||||
else:
|
||||
logger.warning(f"LLM selected invalid background color '{bg_color}', using default")
|
||||
|
||||
if text_color in frontend_colors:
|
||||
result["icon_color"] = text_color
|
||||
logger.debug(f"LLM selected text color: '{text_color}'")
|
||||
else:
|
||||
logger.warning(f"LLM selected invalid text color '{text_color}', using default")
|
||||
|
||||
else:
|
||||
logger.warning(f"LLM returned non-dict JSON: {parsed_response}")
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.warning(f"Failed to parse LLM JSON response: {e}. Raw content: {raw_content}")
|
||||
else:
|
||||
logger.warning(f"Failed to get valid response from LLM for agent icon generation. Response: {response}")
|
||||
|
||||
logger.debug(f"Generated agent styling: icon={result['icon_name']}, bg={result['icon_background']}, color={result['icon_color']}")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in agent icon generation: {str(e)}\n{traceback.format_exc()}")
|
||||
# Return safe defaults on error (using Indigo theme)
|
||||
return {
|
||||
"icon_name": "bot",
|
||||
"icon_color": "#FFFFFF",
|
||||
"icon_background": "#6366F1"
|
||||
}
|
||||
|
||||
def merge_custom_mcps(existing_mcps: List[Dict[str, Any]], new_mcps: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
if not new_mcps:
|
||||
return existing_mcps
|
||||
|
|
|
@ -242,6 +242,7 @@ export function AgentHeader({
|
|||
currentIconColor={displayData.icon_color}
|
||||
currentBackgroundColor={displayData.icon_background}
|
||||
agentName={displayData.name}
|
||||
agentDescription={displayData.description}
|
||||
onImageUpdate={handleImageUpdate}
|
||||
onIconUpdate={handleIconUpdate}
|
||||
/>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { Sparkles } from 'lucide-react';
|
||||
import { Sparkles, Wand2, Loader2 } from 'lucide-react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
@ -21,12 +21,14 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|||
import { cn } from '@/lib/utils';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { HexColorPicker } from 'react-colorful';
|
||||
import { useGenerateAgentIcon } from '@/hooks/react-query/agents/use-agent-icon-generation';
|
||||
|
||||
interface ProfilePictureDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
currentImageUrl?: string;
|
||||
agentName?: string;
|
||||
agentDescription?: string;
|
||||
onImageUpdate: (url: string | null) => void;
|
||||
currentIconName?: string;
|
||||
currentIconColor?: string;
|
||||
|
@ -39,6 +41,7 @@ export function ProfilePictureDialog({
|
|||
onClose,
|
||||
currentImageUrl,
|
||||
agentName,
|
||||
agentDescription,
|
||||
onImageUpdate,
|
||||
currentIconName,
|
||||
currentIconColor = '#000000',
|
||||
|
@ -48,6 +51,8 @@ export function ProfilePictureDialog({
|
|||
const [selectedIcon, setSelectedIcon] = useState(currentIconName || 'bot');
|
||||
const [iconColor, setIconColor] = useState(currentIconColor || '#000000');
|
||||
const [backgroundColor, setBackgroundColor] = useState(currentBackgroundColor || '#e5e5e5');
|
||||
|
||||
const generateIconMutation = useGenerateAgentIcon();
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
|
@ -66,6 +71,32 @@ export function ProfilePictureDialog({
|
|||
}
|
||||
}, [selectedIcon, iconColor, backgroundColor, onIconUpdate, onImageUpdate, onClose]);
|
||||
|
||||
const handleAutoGenerate = useCallback(() => {
|
||||
if (!agentName) {
|
||||
toast.error('Agent name is required for auto-generation');
|
||||
return;
|
||||
}
|
||||
|
||||
generateIconMutation.mutate(
|
||||
{
|
||||
name: agentName,
|
||||
description: agentDescription,
|
||||
},
|
||||
{
|
||||
onSuccess: (result) => {
|
||||
setSelectedIcon(result.icon_name);
|
||||
setIconColor(result.icon_color);
|
||||
setBackgroundColor(result.icon_background);
|
||||
toast.success('Agent icon auto-generated!');
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Auto-generation failed:', error);
|
||||
toast.error('Failed to auto-generate icon. Please try again.');
|
||||
},
|
||||
}
|
||||
);
|
||||
}, [agentName, agentDescription, generateIconMutation]);
|
||||
|
||||
const presetColors = [
|
||||
'#000000', '#FFFFFF', '#6366F1', '#10B981', '#F59E0B',
|
||||
'#EF4444', '#8B5CF6', '#EC4899', '#14B8A6', '#F97316',
|
||||
|
@ -324,6 +355,21 @@ export function ProfilePictureDialog({
|
|||
</Tabs>
|
||||
</div>
|
||||
<DialogFooter className="px-6 py-4 shrink-0 border-t">
|
||||
<div className="flex items-center gap-2 mr-auto">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleAutoGenerate}
|
||||
disabled={generateIconMutation.isPending || !agentName}
|
||||
className="gap-2"
|
||||
>
|
||||
{generateIconMutation.isPending ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Wand2 className="h-4 w-4" />
|
||||
)}
|
||||
Auto-generate
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import { useMutation } from '@tanstack/react-query';
|
||||
import { generateAgentIcon, AgentIconGenerationRequest, AgentIconGenerationResponse } from '@/lib/api';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export const useGenerateAgentIcon = () => {
|
||||
return useMutation<AgentIconGenerationResponse, Error, AgentIconGenerationRequest>({
|
||||
mutationFn: generateAgentIcon,
|
||||
onError: (error) => {
|
||||
console.error('Error generating agent icon:', error);
|
||||
toast.error('Failed to generate agent icon. Please try again.');
|
||||
},
|
||||
});
|
||||
};
|
|
@ -325,7 +325,8 @@ export const getProjects = async (): Promise<Project[]> => {
|
|||
const { data, error } = await supabase
|
||||
.from('projects')
|
||||
.select('*')
|
||||
.eq('account_id', userData.user.id);
|
||||
.eq('account_id', userData.user.id)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) {
|
||||
// Handle permission errors specifically
|
||||
|
@ -975,6 +976,60 @@ export const getAgentStatus = async (agentRunId: string): Promise<AgentRun> => {
|
|||
}
|
||||
};
|
||||
|
||||
export interface AgentIconGenerationRequest {
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface AgentIconGenerationResponse {
|
||||
icon_name: string;
|
||||
icon_color: string;
|
||||
icon_background: string;
|
||||
}
|
||||
|
||||
export const generateAgentIcon = async (request: AgentIconGenerationRequest): Promise<AgentIconGenerationResponse> => {
|
||||
try {
|
||||
const supabase = createClient();
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession();
|
||||
|
||||
if (!session?.access_token) {
|
||||
throw new NoAccessTokenAvailableError();
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_URL}/agents/generate-icon`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${session.access_token}`,
|
||||
},
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response
|
||||
.text()
|
||||
.catch(() => 'No error details available');
|
||||
console.error(
|
||||
`[API] Error generating agent icon: ${response.status} ${response.statusText}`,
|
||||
errorText,
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Error generating agent icon: ${response.statusText} (${response.status})`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('[API] Failed to generate agent icon:', error);
|
||||
handleApiError(error, { operation: 'generate agent icon', resource: 'agent icon generation' });
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getAgentRuns = async (threadId: string): Promise<AgentRun[]> => {
|
||||
try {
|
||||
const supabase = createClient();
|
||||
|
|
Loading…
Reference in New Issue