mirror of https://github.com/kortix-ai/suna.git
Merge pull request #590 from escapade-mckv/mcp-5a-agentbuilder
Mcp 5a agentbuilder
This commit is contained in:
commit
7940472da9
|
@ -1,130 +0,0 @@
|
||||||
# Pagination Implementation for Agents and Marketplace
|
|
||||||
|
|
||||||
This document outlines the implementation of server-side pagination, searching, sorting, and filtering for both the agents page and marketplace page.
|
|
||||||
|
|
||||||
## Backend Changes
|
|
||||||
|
|
||||||
### 1. Updated API Endpoints
|
|
||||||
|
|
||||||
#### Agents Endpoint (`/agents`)
|
|
||||||
- **New Parameters:**
|
|
||||||
- `page`: Page number (1-based, default: 1)
|
|
||||||
- `limit`: Items per page (1-100, default: 20)
|
|
||||||
- `search`: Search in name and description
|
|
||||||
- `sort_by`: Sort field (name, created_at, updated_at, tools_count)
|
|
||||||
- `sort_order`: Sort order (asc, desc)
|
|
||||||
- `has_default`: Filter by default agents
|
|
||||||
- `has_mcp_tools`: Filter by agents with MCP tools
|
|
||||||
- `has_agentpress_tools`: Filter by agents with AgentPress tools
|
|
||||||
- `tools`: Comma-separated list of tools to filter by
|
|
||||||
|
|
||||||
- **Response Format:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"agents": [...],
|
|
||||||
"pagination": {
|
|
||||||
"page": 1,
|
|
||||||
"limit": 20,
|
|
||||||
"total": 150,
|
|
||||||
"pages": 8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Marketplace Endpoint (`/marketplace/agents`)
|
|
||||||
- **New Parameters:**
|
|
||||||
- `page`: Page number (1-based, default: 1)
|
|
||||||
- `limit`: Items per page (1-100, default: 20)
|
|
||||||
- `search`: Search in name and description
|
|
||||||
- `tags`: Comma-separated string of tags
|
|
||||||
- `sort_by`: Sort by (newest, popular, most_downloaded, name)
|
|
||||||
- `creator`: Filter by creator name
|
|
||||||
|
|
||||||
- **Response Format:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"agents": [...],
|
|
||||||
"pagination": {
|
|
||||||
"page": 1,
|
|
||||||
"limit": 20,
|
|
||||||
"total": 75,
|
|
||||||
"pages": 4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Database Functions
|
|
||||||
|
|
||||||
#### Updated `get_marketplace_agents`
|
|
||||||
- Added `p_creator` parameter for filtering by creator name
|
|
||||||
- Enhanced search functionality
|
|
||||||
|
|
||||||
#### New `get_marketplace_agents_count`
|
|
||||||
- Returns total count of marketplace agents matching filters
|
|
||||||
- Used for pagination calculation
|
|
||||||
|
|
||||||
## Frontend Changes
|
|
||||||
|
|
||||||
### 1. New Components
|
|
||||||
|
|
||||||
#### Pagination Component
|
|
||||||
- Located at: `frontend/src/app/(dashboard)/agents/_components/pagination.tsx`
|
|
||||||
- Features:
|
|
||||||
- Smart page number display with ellipsis
|
|
||||||
- Previous/Next navigation
|
|
||||||
- Disabled state during loading
|
|
||||||
- Responsive design
|
|
||||||
|
|
||||||
### 2. Updated Hooks
|
|
||||||
|
|
||||||
#### useAgents Hook
|
|
||||||
- Now accepts `AgentsParams` for server-side filtering
|
|
||||||
- Returns `AgentsResponse` with pagination info
|
|
||||||
|
|
||||||
#### useMarketplaceAgents Hook
|
|
||||||
- Updated to support new pagination parameters
|
|
||||||
- Returns `MarketplaceAgentsResponse` with pagination info
|
|
||||||
|
|
||||||
### 3. Updated Pages
|
|
||||||
|
|
||||||
#### Agents Page
|
|
||||||
- Replaced client-side filtering with server-side parameters
|
|
||||||
- Added pagination component
|
|
||||||
- Automatic page reset when filters change
|
|
||||||
- Enhanced results display with pagination info
|
|
||||||
|
|
||||||
#### Marketplace Page
|
|
||||||
- Added pagination support
|
|
||||||
- Enhanced sorting options (added "Name A-Z")
|
|
||||||
- Improved results display
|
|
||||||
- Added pagination component
|
|
||||||
|
|
||||||
## Benefits
|
|
||||||
|
|
||||||
1. **Performance**: Only loads necessary data, reducing bandwidth and improving load times
|
|
||||||
2. **Scalability**: Can handle large datasets efficiently
|
|
||||||
3. **User Experience**: Faster page loads and responsive filtering
|
|
||||||
4. **Server Resources**: Reduced memory usage and database load
|
|
||||||
5. **Search**: Real-time search with backend optimization
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Agents Page
|
|
||||||
- Search agents by name or description
|
|
||||||
- Filter by default status, tool types, or specific tools
|
|
||||||
- Sort by name, creation date, update date, or tool count
|
|
||||||
- Navigate through pages with pagination controls
|
|
||||||
|
|
||||||
### Marketplace Page
|
|
||||||
- Search marketplace agents by name or description
|
|
||||||
- Filter by tags or creator name
|
|
||||||
- Sort by newest, popularity, downloads, or name
|
|
||||||
- Browse through paginated results
|
|
||||||
|
|
||||||
## Technical Notes
|
|
||||||
|
|
||||||
- Page size is limited to 100 items maximum for performance
|
|
||||||
- Search is case-insensitive and matches partial strings
|
|
||||||
- Tool filtering supports both MCP and AgentPress tools
|
|
||||||
- Sorting is handled both in database (for simple fields) and post-processing (for computed fields like tools_count)
|
|
||||||
- Pagination automatically resets to page 1 when filters change
|
|
|
@ -1,92 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""Test script for image compression functionality in SeeImage tool."""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from io import BytesIO
|
|
||||||
from PIL import Image
|
|
||||||
import base64
|
|
||||||
|
|
||||||
# Add the backend directory to the Python path
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
|
|
||||||
from agent.tools.sb_vision_tool import SandboxVisionTool
|
|
||||||
|
|
||||||
def create_test_image(width=3000, height=2000, format='PNG'):
|
|
||||||
"""Create a test image with specified dimensions."""
|
|
||||||
# Create a colorful test pattern
|
|
||||||
img = Image.new('RGB', (width, height))
|
|
||||||
pixels = img.load()
|
|
||||||
|
|
||||||
for x in range(width):
|
|
||||||
for y in range(height):
|
|
||||||
# Create a gradient pattern
|
|
||||||
r = int((x / width) * 255)
|
|
||||||
g = int((y / height) * 255)
|
|
||||||
b = int(((x + y) / (width + height)) * 255)
|
|
||||||
pixels[x, y] = (r, g, b)
|
|
||||||
|
|
||||||
# Save to bytes
|
|
||||||
output = BytesIO()
|
|
||||||
img.save(output, format=format)
|
|
||||||
return output.getvalue()
|
|
||||||
|
|
||||||
def test_compression():
|
|
||||||
"""Test the compression functionality."""
|
|
||||||
# Create a mock SandboxVisionTool instance
|
|
||||||
# We'll just test the compress_image method directly
|
|
||||||
tool = SandboxVisionTool(project_id="test", thread_id="test", thread_manager=None)
|
|
||||||
|
|
||||||
print("Testing image compression functionality...\n")
|
|
||||||
|
|
||||||
# Test 1: Large PNG image
|
|
||||||
print("Test 1: Large PNG image")
|
|
||||||
png_bytes = create_test_image(3000, 2000, 'PNG')
|
|
||||||
original_size = len(png_bytes)
|
|
||||||
print(f"Original PNG size: {original_size / 1024:.1f}KB")
|
|
||||||
|
|
||||||
compressed_bytes, mime_type = tool.compress_image(png_bytes, 'image/png', 'test.png')
|
|
||||||
compressed_size = len(compressed_bytes)
|
|
||||||
print(f"Compressed size: {compressed_size / 1024:.1f}KB")
|
|
||||||
print(f"Compression ratio: {(1 - compressed_size / original_size) * 100:.1f}%")
|
|
||||||
print(f"Output MIME type: {mime_type}\n")
|
|
||||||
|
|
||||||
# Test 2: JPEG image
|
|
||||||
print("Test 2: JPEG image")
|
|
||||||
jpeg_bytes = create_test_image(2000, 1500, 'JPEG')
|
|
||||||
original_size = len(jpeg_bytes)
|
|
||||||
print(f"Original JPEG size: {original_size / 1024:.1f}KB")
|
|
||||||
|
|
||||||
compressed_bytes, mime_type = tool.compress_image(jpeg_bytes, 'image/jpeg', 'test.jpg')
|
|
||||||
compressed_size = len(compressed_bytes)
|
|
||||||
print(f"Compressed size: {compressed_size / 1024:.1f}KB")
|
|
||||||
print(f"Compression ratio: {(1 - compressed_size / original_size) * 100:.1f}%")
|
|
||||||
print(f"Output MIME type: {mime_type}\n")
|
|
||||||
|
|
||||||
# Test 3: Small image (should not be resized)
|
|
||||||
print("Test 3: Small image (800x600)")
|
|
||||||
small_bytes = create_test_image(800, 600, 'PNG')
|
|
||||||
original_size = len(small_bytes)
|
|
||||||
print(f"Original size: {original_size / 1024:.1f}KB")
|
|
||||||
|
|
||||||
compressed_bytes, mime_type = tool.compress_image(small_bytes, 'image/png', 'small.png')
|
|
||||||
compressed_size = len(compressed_bytes)
|
|
||||||
print(f"Compressed size: {compressed_size / 1024:.1f}KB")
|
|
||||||
print(f"Compression ratio: {(1 - compressed_size / original_size) * 100:.1f}%")
|
|
||||||
print(f"Output MIME type: {mime_type}\n")
|
|
||||||
|
|
||||||
# Test 4: Verify image quality after compression
|
|
||||||
print("Test 4: Verifying image quality")
|
|
||||||
# Open the compressed image to check it's valid
|
|
||||||
try:
|
|
||||||
compressed_img = Image.open(BytesIO(compressed_bytes))
|
|
||||||
print(f"Compressed image dimensions: {compressed_img.size}")
|
|
||||||
print(f"Compressed image mode: {compressed_img.mode}")
|
|
||||||
print("✓ Compressed image is valid and can be opened")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Error opening compressed image: {e}")
|
|
||||||
|
|
||||||
print("\nAll tests completed!")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
test_compression()
|
|
|
@ -1,108 +0,0 @@
|
||||||
"""
|
|
||||||
Test script to list ONLY MCP tool OpenAI schema method names
|
|
||||||
"""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import os
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
from agentpress.thread_manager import ThreadManager
|
|
||||||
from agent.tools.mcp_tool_wrapper import MCPToolWrapper
|
|
||||||
from agentpress.tool import SchemaType
|
|
||||||
from utils.logger import logger
|
|
||||||
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
async def test_mcp_tools_only():
|
|
||||||
"""Test listing only MCP tools and their OpenAI schema method names"""
|
|
||||||
|
|
||||||
# Create thread manager
|
|
||||||
thread_manager = ThreadManager()
|
|
||||||
|
|
||||||
print("\n=== MCP Tools Test ===")
|
|
||||||
|
|
||||||
# MCP configuration with ALL tools enabled (empty enabledTools)
|
|
||||||
mcp_configs = [
|
|
||||||
{
|
|
||||||
"name": "Exa Search",
|
|
||||||
"qualifiedName": "exa",
|
|
||||||
"config": {"exaApiKey": os.getenv("EXA_API_KEY", "test-key")},
|
|
||||||
"enabledTools": [] # Empty to get ALL tools
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# Register MCP tool wrapper
|
|
||||||
logger.info("Registering MCP tool wrapper...")
|
|
||||||
thread_manager.add_tool(MCPToolWrapper, mcp_configs=mcp_configs)
|
|
||||||
|
|
||||||
# Get the tool instance
|
|
||||||
mcp_wrapper_instance = None
|
|
||||||
for tool_name, tool_info in thread_manager.tool_registry.tools.items():
|
|
||||||
if isinstance(tool_info['instance'], MCPToolWrapper):
|
|
||||||
mcp_wrapper_instance = tool_info['instance']
|
|
||||||
break
|
|
||||||
|
|
||||||
if not mcp_wrapper_instance:
|
|
||||||
logger.error("Failed to find MCP wrapper instance")
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Initialize MCP tools
|
|
||||||
logger.info("Initializing MCP tools...")
|
|
||||||
await mcp_wrapper_instance.initialize_and_register_tools()
|
|
||||||
|
|
||||||
# Get all available MCP tools from the server
|
|
||||||
available_mcp_tools = await mcp_wrapper_instance.get_available_tools()
|
|
||||||
print(f"\nTotal MCP tools available from server: {len(available_mcp_tools)}")
|
|
||||||
|
|
||||||
# Get the dynamically created schemas
|
|
||||||
updated_schemas = mcp_wrapper_instance.get_schemas()
|
|
||||||
mcp_method_schemas = {k: v for k, v in updated_schemas.items() if k != 'call_mcp_tool'}
|
|
||||||
|
|
||||||
print(f"\nDynamically created MCP methods: {len(mcp_method_schemas)}")
|
|
||||||
|
|
||||||
# List all MCP tool method names with descriptions
|
|
||||||
print("\n=== MCP Tool Method Names (Clean Names) ===")
|
|
||||||
for method_name, schema_list in sorted(mcp_method_schemas.items()):
|
|
||||||
for schema in schema_list:
|
|
||||||
if schema.schema_type == SchemaType.OPENAPI:
|
|
||||||
func_info = schema.schema.get('function', {})
|
|
||||||
func_desc = func_info.get('description', 'No description')
|
|
||||||
# Extract just the description part before "(MCP Server:"
|
|
||||||
desc_parts = func_desc.split(' (MCP Server:')
|
|
||||||
clean_desc = desc_parts[0] if desc_parts else func_desc
|
|
||||||
print(f"\n{method_name}")
|
|
||||||
print(f" Description: {clean_desc}")
|
|
||||||
|
|
||||||
# Show parameters
|
|
||||||
params = func_info.get('parameters', {})
|
|
||||||
props = params.get('properties', {})
|
|
||||||
required = params.get('required', [])
|
|
||||||
if props:
|
|
||||||
print(f" Parameters:")
|
|
||||||
for param_name, param_info in props.items():
|
|
||||||
param_type = param_info.get('type', 'any')
|
|
||||||
param_desc = param_info.get('description', 'No description')
|
|
||||||
is_required = param_name in required
|
|
||||||
req_marker = " (required)" if is_required else " (optional)"
|
|
||||||
print(f" - {param_name}: {param_type}{req_marker} - {param_desc}")
|
|
||||||
|
|
||||||
# Show the name mapping
|
|
||||||
print("\n\n=== MCP Tool Name Mapping (Original -> Clean) ===")
|
|
||||||
for original_name, tool_data in sorted(mcp_wrapper_instance._dynamic_tools.items()):
|
|
||||||
print(f"{original_name} -> {tool_data['method_name']}")
|
|
||||||
|
|
||||||
# Summary of callable method names
|
|
||||||
print("\n\n=== Summary: Callable MCP Method Names ===")
|
|
||||||
method_names = sorted(mcp_method_schemas.keys())
|
|
||||||
for name in method_names:
|
|
||||||
print(f"- {name}")
|
|
||||||
|
|
||||||
print(f"\nTotal callable MCP methods: {len(method_names)}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error during MCP initialization: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(test_mcp_tools_only())
|
|
|
@ -556,6 +556,7 @@ export default function ThreadPage({
|
||||||
debugMode={debugMode}
|
debugMode={debugMode}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
initialLoadCompleted={initialLoadCompleted}
|
initialLoadCompleted={initialLoadCompleted}
|
||||||
|
agentName={agent && agent.name}
|
||||||
>
|
>
|
||||||
<ThreadError error={error} />
|
<ThreadError error={error} />
|
||||||
</ThreadLayout>
|
</ThreadLayout>
|
||||||
|
@ -598,6 +599,7 @@ export default function ThreadPage({
|
||||||
debugMode={debugMode}
|
debugMode={debugMode}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
initialLoadCompleted={initialLoadCompleted}
|
initialLoadCompleted={initialLoadCompleted}
|
||||||
|
agentName={agent && agent.name}
|
||||||
>
|
>
|
||||||
<ThreadContent
|
<ThreadContent
|
||||||
messages={messages}
|
messages={messages}
|
||||||
|
@ -630,7 +632,7 @@ export default function ThreadPage({
|
||||||
value={newMessage}
|
value={newMessage}
|
||||||
onChange={setNewMessage}
|
onChange={setNewMessage}
|
||||||
onSubmit={handleSubmitMessage}
|
onSubmit={handleSubmitMessage}
|
||||||
placeholder="Ask Suna anything..."
|
placeholder={`Ask ${agent && agent.name} anything...`}
|
||||||
loading={isSending}
|
loading={isSending}
|
||||||
disabled={isSending || agentStatus === 'running' || agentStatus === 'connecting'}
|
disabled={isSending || agentStatus === 'running' || agentStatus === 'connecting'}
|
||||||
isAgentRunning={agentStatus === 'running' || agentStatus === 'connecting'}
|
isAgentRunning={agentStatus === 'running' || agentStatus === 'connecting'}
|
||||||
|
@ -639,6 +641,7 @@ export default function ThreadPage({
|
||||||
onFileBrowse={handleOpenFileViewer}
|
onFileBrowse={handleOpenFileViewer}
|
||||||
sandboxId={sandboxId || undefined}
|
sandboxId={sandboxId || undefined}
|
||||||
messages={messages}
|
messages={messages}
|
||||||
|
agentName={agent && agent.name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -38,6 +38,7 @@ interface ThreadLayoutProps {
|
||||||
debugMode: boolean;
|
debugMode: boolean;
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
initialLoadCompleted: boolean;
|
initialLoadCompleted: boolean;
|
||||||
|
agentName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ThreadLayout({
|
export function ThreadLayout({
|
||||||
|
@ -71,6 +72,7 @@ export function ThreadLayout({
|
||||||
debugMode,
|
debugMode,
|
||||||
isMobile,
|
isMobile,
|
||||||
initialLoadCompleted,
|
initialLoadCompleted,
|
||||||
|
agentName
|
||||||
}: ThreadLayoutProps) {
|
}: ThreadLayoutProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen">
|
<div className="flex h-screen">
|
||||||
|
@ -114,6 +116,7 @@ export function ThreadLayout({
|
||||||
renderToolResult={renderToolResult}
|
renderToolResult={renderToolResult}
|
||||||
isLoading={!initialLoadCompleted || isLoading}
|
isLoading={!initialLoadCompleted || isLoading}
|
||||||
onFileClick={onViewFiles}
|
onFileClick={onViewFiles}
|
||||||
|
agentName={agentName}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{sandboxId && (
|
{sandboxId && (
|
||||||
|
|
|
@ -443,7 +443,7 @@ export function ToolCallSidePanel({
|
||||||
<div className="ml-2 flex items-center gap-2">
|
<div className="ml-2 flex items-center gap-2">
|
||||||
<Computer className="h-4 w-4" />
|
<Computer className="h-4 w-4" />
|
||||||
<h2 className="text-md font-medium text-zinc-900 dark:text-zinc-100">
|
<h2 className="text-md font-medium text-zinc-900 dark:text-zinc-100">
|
||||||
Suna's Computer
|
{agentName ? `${agentName}'s Computer` : 'Suna\'s Computer'}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
@ -490,7 +490,7 @@ export function ToolCallSidePanel({
|
||||||
<div className="ml-2 flex items-center gap-2">
|
<div className="ml-2 flex items-center gap-2">
|
||||||
<Computer className="h-4 w-4" />
|
<Computer className="h-4 w-4" />
|
||||||
<h2 className="text-md font-medium text-zinc-900 dark:text-zinc-100">
|
<h2 className="text-md font-medium text-zinc-900 dark:text-zinc-100">
|
||||||
Suna's Computer
|
{agentName ? `${agentName}'s Computer` : 'Suna\'s Computer'}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
|
|
Loading…
Reference in New Issue