From 30f88aed997c811fc066943dbc426429606364a2 Mon Sep 17 00:00:00 2001 From: sharath <29162020+tnfssc@users.noreply.github.com> Date: Mon, 9 Jun 2025 01:38:50 +0000 Subject: [PATCH 1/7] feat(image-editing): introduce image generation and editing tool with updated documentation --- backend/agent/prompt.py | 26 +++- backend/agent/run.py | 2 + backend/agent/tools/sb_image_edit_tool.py | 167 ++++++++++++++++++++++ backend/poetry.lock | 11 +- backend/pyproject.toml | 2 +- backend/requirements.txt | 2 +- 6 files changed, 202 insertions(+), 8 deletions(-) create mode 100644 backend/agent/tools/sb_image_edit_tool.py diff --git a/backend/agent/prompt.py b/backend/agent/prompt.py index 9bd0dc22..1fae53c1 100644 --- a/backend/agent/prompt.py +++ b/backend/agent/prompt.py @@ -89,7 +89,31 @@ You have the ability to execute operations using both Python and CLI tools: * Supported formats include JPG, PNG, GIF, WEBP, and other common image formats. * Maximum file size limit is 10 MB. -### 2.2.7 DATA PROVIDERS +### 2.2.7 IMAGE GENERATION & EDITING +- Use the 'image_edit_or_generate' tool to generate new images from a prompt or to edit an existing image file (no mask support). + * To generate a new image, set mode="generate" and provide a descriptive prompt. + * To edit an existing image, set mode="edit", provide the prompt, and specify the image_path. + * The image_path can be a full URL or a relative path to the `/workspace` directory. + * Example (generate): + + + generate + A futuristic cityscape at sunset + + + * Example (edit): + + + edit + Add a red hat to the person in the image + http://example.com/images/person.png + + + * ALWAYS use this tool for any image creation or editing tasks. Do not attempt to generate or edit images by any other means. + * You must use edit mode when the user asks you to edit an image or change an existing image in any way. + * Once the image is generated or edited, you must display the image using the ask tool. + +### 2.2.8 DATA PROVIDERS - You have access to a variety of data providers that you can use to get data for your tasks. - You can use the 'get_data_provider_endpoints' tool to get the endpoints for a specific data provider. - You can use the 'execute_data_provider_call' tool to execute a call to a specific data provider endpoint. diff --git a/backend/agent/run.py b/backend/agent/run.py index e97ef729..365676a6 100644 --- a/backend/agent/run.py +++ b/backend/agent/run.py @@ -25,6 +25,7 @@ from utils.logger import logger from utils.auth_utils import get_account_id_from_thread from services.billing import check_billing_status from agent.tools.sb_vision_tool import SandboxVisionTool +from agent.tools.sb_image_edit_tool import SandboxImageEditTool from services.langfuse import langfuse from langfuse.client import StatefulTraceClient from services.langfuse import langfuse @@ -107,6 +108,7 @@ async def run_agent( thread_manager.add_tool(MessageTool) thread_manager.add_tool(SandboxWebSearchTool, project_id=project_id, thread_manager=thread_manager) thread_manager.add_tool(SandboxVisionTool, project_id=project_id, thread_id=thread_id, thread_manager=thread_manager) + thread_manager.add_tool(SandboxImageEditTool, project_id=project_id, thread_id=thread_id, thread_manager=thread_manager) if config.RAPID_API_KEY: thread_manager.add_tool(DataProvidersTool) else: diff --git a/backend/agent/tools/sb_image_edit_tool.py b/backend/agent/tools/sb_image_edit_tool.py new file mode 100644 index 00000000..c79596b5 --- /dev/null +++ b/backend/agent/tools/sb_image_edit_tool.py @@ -0,0 +1,167 @@ +from typing import Optional +from agentpress.tool import ToolResult, openapi_schema, xml_schema +from sandbox.tool_base import SandboxToolsBase +from agentpress.thread_manager import ThreadManager +from openai import OpenAI +import httpx +import os +from io import BytesIO +import uuid + + +class SandboxImageEditTool(SandboxToolsBase): + """Tool for generating or editing images using OpenAI DALL-E via OpenAI SDK (no mask support).""" + + def __init__(self, project_id: str, thread_id: str, thread_manager: ThreadManager): + super().__init__(project_id, thread_manager) + self.thread_id = thread_id + self.thread_manager = thread_manager + self.client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) + + @openapi_schema( + { + "type": "function", + "function": { + "name": "image_edit_or_generate", + "description": "Generate a new image from a prompt, or edit an existing image (no mask support) using OpenAI DALL-E via OpenAI SDK. Stores the result in the thread context.", + "parameters": { + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": ["generate", "edit"], + "description": "'generate' to create a new image from a prompt, 'edit' to edit an existing image.", + }, + "prompt": { + "type": "string", + "description": "Text prompt describing the desired image or edit.", + }, + "image_path": { + "type": "string", + "description": "(edit mode only) Path to the image file to edit, relative to /workspace. Required for 'edit'.", + }, + }, + "required": ["mode", "prompt"], + }, + }, + } + ) + @xml_schema( + tag_name="image-edit-or-generate", + mappings=[ + {"param_name": "mode", "node_type": "attribute", "path": "."}, + {"param_name": "prompt", "node_type": "attribute", "path": "."}, + {"param_name": "image_path", "node_type": "attribute", "path": "."}, + ], + example=""" + + + generate + A futuristic cityscape at sunset + + + """, + ) + async def image_edit_or_generate( + self, + mode: str, + prompt: str, + image_path: Optional[str] = None, + ) -> ToolResult: + """Generate or edit images using OpenAI DALL-E via OpenAI SDK (no mask support).""" + try: + await self._ensure_sandbox() + + if mode == "generate": + response = self.client.images.generate( + prompt=prompt, n=1, size="1024x1024" + ) + elif mode == "edit": + if not image_path: + return self.fail_response("'image_path' is required for edit mode.") + + image_bytes = await self._get_image_bytes(image_path) + if isinstance(image_bytes, ToolResult): # Error occurred + return image_bytes + + # Create BytesIO object with proper filename to set MIME type + image_io = BytesIO(image_bytes) + image_io.name = "image.png" # Set filename to ensure proper MIME type detection + + response = self.client.images.edit( + image=image_io, prompt=prompt, n=1, size="1024x1024" + ) + else: + return self.fail_response("Invalid mode. Use 'generate' or 'edit'.") + + # Download and save the generated image to sandbox + image_filename = await self._process_image_response(response) + if isinstance(image_filename, ToolResult): # Error occurred + return image_filename + + return self.success_response( + f"Successfully generated image using mode '{mode}'. Image saved as: {image_filename}. You can use the ask tool to display the image." + ) + + except Exception as e: + return self.fail_response( + f"An error occurred during image generation/editing: {str(e)}" + ) + + async def _get_image_bytes(self, image_path: str) -> bytes | ToolResult: + """Get image bytes from URL or local file path.""" + if image_path.startswith(("http://", "https://")): + return await self._download_image_from_url(image_path) + else: + return await self._read_image_from_sandbox(image_path) + + async def _download_image_from_url(self, url: str) -> bytes | ToolResult: + """Download image from URL.""" + try: + async with httpx.AsyncClient() as client: + response = await client.get(url) + response.raise_for_status() + return response.content + except Exception: + return self.fail_response(f"Could not download image from URL: {url}") + + async def _read_image_from_sandbox(self, image_path: str) -> bytes | ToolResult: + """Read image from sandbox filesystem.""" + try: + cleaned_path = self.clean_path(image_path) + full_path = f"{self.workspace_path}/{cleaned_path}" + + # Check if file exists and is not a directory + file_info = self.sandbox.fs.get_file_info(full_path) + if file_info.is_dir: + return self.fail_response( + f"Path '{cleaned_path}' is a directory, not an image file." + ) + + return self.sandbox.fs.download_file(full_path) + + except Exception as e: + return self.fail_response( + f"Could not read image file from sandbox: {image_path} - {str(e)}" + ) + + async def _process_image_response(self, response) -> str | ToolResult: + """Download generated image and save to sandbox with random name.""" + try: + original_url = response.data[0].url + + async with httpx.AsyncClient() as client: + img_response = await client.get(original_url) + img_response.raise_for_status() + + # Generate random filename + + random_filename = f"generated_image_{uuid.uuid4().hex[:8]}.png" + sandbox_path = f"{self.workspace_path}/{random_filename}" + + # Save image to sandbox + self.sandbox.fs.upload_file(sandbox_path, img_response.content) + return random_filename + + except Exception as e: + return self.fail_response(f"Failed to download and save image: {str(e)}") diff --git a/backend/poetry.lock b/backend/poetry.lock index dbf884d8..14202f37 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -1428,14 +1428,14 @@ openai = ["openai (>=0.27.8)"] [[package]] name = "litellm" -version = "1.66.1" +version = "1.72.2" description = "Library to easily interface with LLM API providers" optional = false python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8" groups = ["main"] files = [ - {file = "litellm-1.66.1-py3-none-any.whl", hash = "sha256:1f601fea3f086c1d2d91be60b9db115082a2f3a697e4e0def72f8b9c777c7232"}, - {file = "litellm-1.66.1.tar.gz", hash = "sha256:98f7add913e5eae2131dd412ee27532d9a309defd9dbb64f6c6c42ea8a2af068"}, + {file = "litellm-1.72.2-py3-none-any.whl", hash = "sha256:51e70f5cd98748a603d725ef29ede0ecad3d55e1a89cbbcec8d12d6fff55bff4"}, + {file = "litellm-1.72.2.tar.gz", hash = "sha256:b50c7f7a0df67117889479264a12b0dea9c566a02173d4c3159540a13760d38b"}, ] [package.dependencies] @@ -1453,7 +1453,8 @@ tokenizers = "*" [package.extras] extra-proxy = ["azure-identity (>=1.15.0,<2.0.0)", "azure-keyvault-secrets (>=4.8.0,<5.0.0)", "google-cloud-kms (>=2.21.3,<3.0.0)", "prisma (==0.11.0)", "redisvl (>=0.4.1,<0.5.0) ; python_version >= \"3.9\" and python_version < \"3.14\"", "resend (>=0.8.0,<0.9.0)"] -proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "backoff", "boto3 (==1.34.34)", "cryptography (>=43.0.1,<44.0.0)", "fastapi (>=0.115.5,<0.116.0)", "fastapi-sso (>=0.16.0,<0.17.0)", "gunicorn (>=23.0.0,<24.0.0)", "litellm-proxy-extras (==0.1.7)", "mcp (==1.5.0) ; python_version >= \"3.10\"", "orjson (>=3.9.7,<4.0.0)", "pynacl (>=1.5.0,<2.0.0)", "python-multipart (>=0.0.18,<0.0.19)", "pyyaml (>=6.0.1,<7.0.0)", "rq", "uvicorn (>=0.29.0,<0.30.0)", "uvloop (>=0.21.0,<0.22.0)", "websockets (>=13.1.0,<14.0.0)"] +proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "backoff", "boto3 (==1.34.34)", "cryptography (>=43.0.1,<44.0.0)", "fastapi (>=0.115.5,<0.116.0)", "fastapi-sso (>=0.16.0,<0.17.0)", "gunicorn (>=23.0.0,<24.0.0)", "litellm-enterprise (==0.1.7)", "litellm-proxy-extras (==0.2.3)", "mcp (==1.5.0) ; python_version >= \"3.10\"", "orjson (>=3.9.7,<4.0.0)", "pynacl (>=1.5.0,<2.0.0)", "python-multipart (>=0.0.18,<0.0.19)", "pyyaml (>=6.0.1,<7.0.0)", "rich (==13.7.1)", "rq", "uvicorn (>=0.29.0,<0.30.0)", "uvloop (>=0.21.0,<0.22.0) ; sys_platform != \"win32\"", "websockets (>=13.1.0,<14.0.0)"] +utils = ["numpydoc"] [[package]] name = "mailtrap" @@ -3904,4 +3905,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"] [metadata] lock-version = "2.1" python-versions = "^3.11" -content-hash = "09a851f3db2d0b1f130405a69c1661c453f82ce23e078256bc6749662af897a7" +content-hash = "3b983fbe8614f4e59280b2087fa4bcc574502d58fc75aa73a44426279f99e3d2" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 05af3385..029e05f3 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -19,7 +19,7 @@ classifiers = [ [tool.poetry.dependencies] python = "^3.11" python-dotenv = "1.0.1" -litellm = "1.66.1" +litellm = "1.72.2" click = "8.1.7" questionary = "2.0.1" requests = "^2.31.0" diff --git a/backend/requirements.txt b/backend/requirements.txt index 395edcae..2350d88e 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,5 +1,5 @@ python-dotenv==1.0.1 -litellm==1.66.1 +litellm==1.72.2 click==8.1.7 questionary==2.0.1 requests>=2.31.0 From c7d892fc9b6698f8986aa2cc154a954a7b812ae8 Mon Sep 17 00:00:00 2001 From: sharath <29162020+tnfssc@users.noreply.github.com> Date: Mon, 23 Jun 2025 17:36:31 +0000 Subject: [PATCH 2/7] Update server configuration and dependencies - Increased the number of workers from 1 to 4 in the server startup configuration for improved performance. - Upgraded the OpenAI dependency from version 1.72.0 to 1.90.0 in `pyproject.toml` and `uv.lock`. - Refactored image editing tool to utilize the new OpenAI GPT Image 1 model, updating method calls and documentation accordingly. --- backend/agent/tools/sb_image_edit_tool.py | 31 ++++++++++++++--------- backend/api.py | 2 +- backend/pyproject.toml | 2 +- backend/uv.lock | 8 +++--- 4 files changed, 25 insertions(+), 18 deletions(-) diff --git a/backend/agent/tools/sb_image_edit_tool.py b/backend/agent/tools/sb_image_edit_tool.py index c79596b5..36f34ecb 100644 --- a/backend/agent/tools/sb_image_edit_tool.py +++ b/backend/agent/tools/sb_image_edit_tool.py @@ -2,28 +2,26 @@ from typing import Optional from agentpress.tool import ToolResult, openapi_schema, xml_schema from sandbox.tool_base import SandboxToolsBase from agentpress.thread_manager import ThreadManager -from openai import OpenAI import httpx -import os from io import BytesIO import uuid +from litellm import aimage_generation, aimage_edit class SandboxImageEditTool(SandboxToolsBase): - """Tool for generating or editing images using OpenAI DALL-E via OpenAI SDK (no mask support).""" + """Tool for generating or editing images using OpenAI GPT Image 1 via OpenAI SDK (no mask support).""" def __init__(self, project_id: str, thread_id: str, thread_manager: ThreadManager): super().__init__(project_id, thread_manager) self.thread_id = thread_id self.thread_manager = thread_manager - self.client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) @openapi_schema( { "type": "function", "function": { "name": "image_edit_or_generate", - "description": "Generate a new image from a prompt, or edit an existing image (no mask support) using OpenAI DALL-E via OpenAI SDK. Stores the result in the thread context.", + "description": "Generate a new image from a prompt, or edit an existing image (no mask support) using OpenAI GPT Image 1 via OpenAI SDK. Stores the result in the thread context.", "parameters": { "type": "object", "properties": { @@ -68,13 +66,16 @@ class SandboxImageEditTool(SandboxToolsBase): prompt: str, image_path: Optional[str] = None, ) -> ToolResult: - """Generate or edit images using OpenAI DALL-E via OpenAI SDK (no mask support).""" + """Generate or edit images using OpenAI GPT Image 1 via OpenAI SDK (no mask support).""" try: await self._ensure_sandbox() if mode == "generate": - response = self.client.images.generate( - prompt=prompt, n=1, size="1024x1024" + response = await aimage_generation( + model="gpt-image-1", + prompt=prompt, + n=1, + size="1024x1024", ) elif mode == "edit": if not image_path: @@ -86,10 +87,16 @@ class SandboxImageEditTool(SandboxToolsBase): # Create BytesIO object with proper filename to set MIME type image_io = BytesIO(image_bytes) - image_io.name = "image.png" # Set filename to ensure proper MIME type detection - - response = self.client.images.edit( - image=image_io, prompt=prompt, n=1, size="1024x1024" + image_io.name = ( + "image.png" # Set filename to ensure proper MIME type detection + ) + + response = await aimage_edit( + image=image_io, + prompt=prompt, + model="gpt-image-1", + n=1, + size="1024x1024", ) else: return self.fail_response("Invalid mode. Use 'generate' or 'edit'.") diff --git a/backend/api.py b/backend/api.py index 0ec89e86..90e0968e 100644 --- a/backend/api.py +++ b/backend/api.py @@ -199,7 +199,7 @@ if __name__ == "__main__": if sys.platform == "win32": asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) - workers = 1 + workers = 4 logger.info(f"Starting server on 0.0.0.0:8000 with {workers} workers") uvicorn.run( diff --git a/backend/pyproject.toml b/backend/pyproject.toml index c63ed762..70dcfb73 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -38,7 +38,7 @@ dependencies = [ "daytona-api-client==0.21.0", "daytona-api-client-async==0.21.0", "boto3==1.37.3", - "openai==1.72.0", + "openai==1.90.0", "nest-asyncio==1.6.0", "vncdotool==1.2.0", "tavily-python==0.5.4", diff --git a/backend/uv.lock b/backend/uv.lock index 5ebe4fe9..547360a0 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -1369,7 +1369,7 @@ wheels = [ [[package]] name = "openai" -version = "1.72.0" +version = "1.90.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1381,9 +1381,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/41/56/41de36c0e9f787c406211552ecf2ca4fba3db900207c5c158c4dc67263fc/openai-1.72.0.tar.gz", hash = "sha256:f51de971448905cc90ed5175a5b19e92fd94e31f68cde4025762f9f5257150db", size = 426061 } +sdist = { url = "https://files.pythonhosted.org/packages/2d/30/0bdb712f5e25e823a76828136de6043f28bd69363886c417e05d7021420e/openai-1.90.0.tar.gz", hash = "sha256:9771982cdd5b6631af68c6a603da72ed44cd2caf73b49f717a72b71374bc565b", size = 471896 } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/1c/a0870f31bd71244c8c3a82e171677d9a148a8ea1cb157308cb9e06a41a37/openai-1.72.0-py3-none-any.whl", hash = "sha256:34f5496ba5c8cb06c592831d69e847e2d164526a2fb92afdc3b5cf2891c328c3", size = 643863 }, + { url = "https://files.pythonhosted.org/packages/bd/e3/0d7a2ee7ae7293e794e7945ffeda942ff5e3a94de24be27cc3eb5ba6c188/openai-1.90.0-py3-none-any.whl", hash = "sha256:e5dcb5498ea6b42fec47546d10f1bcc05fb854219a7d953a5ba766718b212a02", size = 734638 }, ] [[package]] @@ -2329,7 +2329,7 @@ requires-dist = [ { name = "mailtrap", specifier = "==2.0.1" }, { name = "mcp", specifier = "==1.9.4" }, { name = "nest-asyncio", specifier = "==1.6.0" }, - { name = "openai", specifier = "==1.72.0" }, + { name = "openai", specifier = "==1.90.0" }, { name = "packaging", specifier = "==24.1" }, { name = "pika", specifier = "==1.3.2" }, { name = "pillow", specifier = "==10.0.0" }, From 4e669a6b751c0a5900459d4a9c9d41280a63729a Mon Sep 17 00:00:00 2001 From: Krishav Raj Singh Date: Sat, 5 Jul 2025 02:26:08 +0530 Subject: [PATCH 3/7] fix icon theme if theme is system --- frontend/src/components/sidebar/kortix-logo.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/sidebar/kortix-logo.tsx b/frontend/src/components/sidebar/kortix-logo.tsx index efef9c83..28f9d318 100644 --- a/frontend/src/components/sidebar/kortix-logo.tsx +++ b/frontend/src/components/sidebar/kortix-logo.tsx @@ -8,7 +8,7 @@ interface KortixLogoProps { size?: number; } export function KortixLogo({ size = 24 }: KortixLogoProps) { - const { theme } = useTheme(); + const { theme, systemTheme } = useTheme(); const [mounted, setMounted] = useState(false); // After mount, we can access the theme @@ -16,13 +16,17 @@ export function KortixLogo({ size = 24 }: KortixLogoProps) { setMounted(true); }, []); + const shouldInvert = mounted && ( + theme === 'dark' || (theme === 'system' && systemTheme === 'dark') + ); + return ( Kortix ); } From 9cfaac080ce4d63e3b836306c7874dbcf129e7ba Mon Sep 17 00:00:00 2001 From: sharath <29162020+tnfssc@users.noreply.github.com> Date: Sat, 5 Jul 2025 11:00:00 +0000 Subject: [PATCH 4/7] refactor(sb_image_edit_tool): convert file operations to async for improved performance --- backend/agent/tools/sb_image_edit_tool.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/agent/tools/sb_image_edit_tool.py b/backend/agent/tools/sb_image_edit_tool.py index 36f34ecb..96daaa90 100644 --- a/backend/agent/tools/sb_image_edit_tool.py +++ b/backend/agent/tools/sb_image_edit_tool.py @@ -139,13 +139,13 @@ class SandboxImageEditTool(SandboxToolsBase): full_path = f"{self.workspace_path}/{cleaned_path}" # Check if file exists and is not a directory - file_info = self.sandbox.fs.get_file_info(full_path) + file_info = await self.sandbox.fs.get_file_info(full_path) if file_info.is_dir: return self.fail_response( f"Path '{cleaned_path}' is a directory, not an image file." ) - return self.sandbox.fs.download_file(full_path) + return await self.sandbox.fs.download_file(full_path) except Exception as e: return self.fail_response( @@ -167,7 +167,7 @@ class SandboxImageEditTool(SandboxToolsBase): sandbox_path = f"{self.workspace_path}/{random_filename}" # Save image to sandbox - self.sandbox.fs.upload_file(sandbox_path, img_response.content) + await self.sandbox.fs.upload_file(sandbox_path, img_response.content) return random_filename except Exception as e: From 3d287213508385304c48aa1a6652bb3b38647787 Mon Sep 17 00:00:00 2001 From: sharath <29162020+tnfssc@users.noreply.github.com> Date: Sat, 5 Jul 2025 12:39:55 +0000 Subject: [PATCH 5/7] fix(sb_image_edit_tool): update image processing to use base64 data instead of URL --- backend/agent/tools/sb_image_edit_tool.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/backend/agent/tools/sb_image_edit_tool.py b/backend/agent/tools/sb_image_edit_tool.py index 96daaa90..9f983e19 100644 --- a/backend/agent/tools/sb_image_edit_tool.py +++ b/backend/agent/tools/sb_image_edit_tool.py @@ -6,6 +6,7 @@ import httpx from io import BytesIO import uuid from litellm import aimage_generation, aimage_edit +import base64 class SandboxImageEditTool(SandboxToolsBase): @@ -92,7 +93,7 @@ class SandboxImageEditTool(SandboxToolsBase): ) response = await aimage_edit( - image=image_io, + image=[image_io], # Type in the LiteLLM SDK is wrong prompt=prompt, model="gpt-image-1", n=1, @@ -155,19 +156,16 @@ class SandboxImageEditTool(SandboxToolsBase): async def _process_image_response(self, response) -> str | ToolResult: """Download generated image and save to sandbox with random name.""" try: - original_url = response.data[0].url - - async with httpx.AsyncClient() as client: - img_response = await client.get(original_url) - img_response.raise_for_status() + original_b64_str = response.data[0].b64_json + # Decode base64 image data + image_data = base64.b64decode(original_b64_str) # Generate random filename - random_filename = f"generated_image_{uuid.uuid4().hex[:8]}.png" sandbox_path = f"{self.workspace_path}/{random_filename}" # Save image to sandbox - await self.sandbox.fs.upload_file(sandbox_path, img_response.content) + await self.sandbox.fs.upload_file(image_data, sandbox_path) return random_filename except Exception as e: From 6e229b383083489c9ec575354f6812f8152512c7 Mon Sep 17 00:00:00 2001 From: marko-kraemer Date: Sat, 5 Jul 2025 15:56:09 +0200 Subject: [PATCH 6/7] fe improvements --- backend/docker-compose.yml | 8 +- .../src/components/sidebar/nav-agents.tsx | 303 +++++++----------- .../src/components/sidebar/sidebar-left.tsx | 40 ++- .../thread/chat-input/chat-input.tsx | 5 +- .../components/thread/thread-site-header.tsx | 4 +- frontend/src/lib/home.tsx | 14 +- 6 files changed, 156 insertions(+), 218 deletions(-) diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index 729e93ea..5a403fd7 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -81,8 +81,8 @@ services: redis: image: redis:8-alpine - # ports: - # - "127.0.0.1:6379:6379" + ports: + - "127.0.0.1:6379:6379" volumes: - redis_data:/data - ./services/docker/redis.conf:/usr/local/etc/redis/redis.conf:ro @@ -104,8 +104,8 @@ services: rabbitmq: image: rabbitmq - # ports: - # - "127.0.0.1:5672:5672" + ports: + - "127.0.0.1:5672:5672" volumes: - rabbitmq_data:/var/lib/rabbitmq restart: unless-stopped diff --git a/frontend/src/components/sidebar/nav-agents.tsx b/frontend/src/components/sidebar/nav-agents.tsx index 3c0b25e4..88ddcf48 100644 --- a/frontend/src/components/sidebar/nav-agents.tsx +++ b/frontend/src/components/sidebar/nav-agents.tsx @@ -7,7 +7,6 @@ import { Link as LinkIcon, MoreHorizontal, Trash2, - Plus, MessagesSquare, Loader2, Share2, @@ -383,97 +382,37 @@ export function NavAgents() { - ) : ( - - -
- - - New Agent - -
-
- New Agent -
- )} + ) : null} ) : null} - {state === 'collapsed' && ( - - - -
- - - - New Agent - - -
-
- New Agent -
-
- )} - {isLoading ? ( - // Show skeleton loaders while loading - Array.from({ length: 3 }).map((_, index) => ( - - -
-
-
-
- )) - ) : combinedThreads.length > 0 ? ( - // Show all threads with project info + + {state !== 'collapsed' && ( <> - {combinedThreads.map((thread) => { - // Check if this thread is currently active - const isActive = pathname?.includes(thread.threadId) || false; - const isThreadLoading = loadingThreadId === thread.threadId; - const isSelected = selectedThreads.has(thread.threadId); + {isLoading ? ( + // Show skeleton loaders while loading + Array.from({ length: 3 }).map((_, index) => ( + + +
+
+
+
+ )) + ) : combinedThreads.length > 0 ? ( + // Show all threads with project info + <> + {combinedThreads.map((thread) => { + // Check if this thread is currently active + const isActive = pathname?.includes(thread.threadId) || false; + const isThreadLoading = loadingThreadId === thread.threadId; + const isSelected = selectedThreads.has(thread.threadId); - return ( - - {state === 'collapsed' ? ( - - -
- - - handleThreadClick(e, thread.threadId, thread.url) - } - > - {isThreadLoading ? ( - - ) : ( - - )} - {thread.projectName} - - -
-
- {thread.projectName} -
- ) : ( -
+ return ( + - - handleThreadClick(e, thread.threadId, thread.url) - } - className="flex items-center" - > -
- {/* Show checkbox on hover or when selected, otherwise show MessagesSquare */} - {isThreadLoading ? ( - - ) : ( - <> - {/* MessagesSquare icon - hidden on hover if not selected */} - - - {/* Checkbox - appears on hover or when selected */} -
toggleThreadSelection(thread.threadId, e)} - > -
- {isSelected && } -
-
- - )} -
- {thread.projectName} - -
-
- )} - {state !== 'collapsed' && !isSelected && ( - - - { - // Ensure pointer events are enabled when dropdown opens - document.body.style.pointerEvents = 'auto'; - }} - > - - More - - - - { - setSelectedItem({ threadId: thread?.threadId, projectId: thread?.projectId }) - setShowShareModal(true) - }}> - - Share Chat - - - + + handleThreadClick(e, thread.threadId, thread.url) + } + className="flex items-center flex-1 min-w-0" > - - Open in New Tab - - - - - handleDeleteThread( - thread.threadId, - thread.projectName, - ) - } - > - - Delete - - - - )} -
- ); - })} + {isThreadLoading ? ( + + ) : null} + {thread.projectName} + + + {/* Checkbox - only visible on hover of this specific area */} +
toggleThreadSelection(thread.threadId, e)} + > +
+ {isSelected && } +
+
+ + {/* Dropdown Menu - inline with content */} + + + + + + { + setSelectedItem({ threadId: thread?.threadId, projectId: thread?.projectId }) + setShowShareModal(true) + }}> + + Share Chat + + + + + Open in New Tab + + + + + handleDeleteThread( + thread.threadId, + thread.projectName, + ) + } + > + + Delete + + + + + + + ); + })} + + ) : ( + + + No tasks yet + + + )} - ) : ( - - - - No tasks yet - - )}
diff --git a/frontend/src/components/sidebar/sidebar-left.tsx b/frontend/src/components/sidebar/sidebar-left.tsx index 62c4d133..4ea08c1a 100644 --- a/frontend/src/components/sidebar/sidebar-left.tsx +++ b/frontend/src/components/sidebar/sidebar-left.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import Link from 'next/link'; -import { Bot, Menu, Store, Shield, Key, Workflow } from 'lucide-react'; +import { Bot, Menu, Store, Shield, Key, Workflow, Plus } from 'lucide-react'; import { NavAgents } from '@/components/sidebar/nav-agents'; import { NavUserWithTeams } from '@/components/sidebar/nav-user-with-teams'; @@ -132,22 +132,20 @@ export function SidebarLeft({ {!flagsLoading && (customAgentsEnabled || marketplaceEnabled) && ( - {customAgentsEnabled && ( - - - - - Agent Playground - - - - )} + + + + + New Task + + + {marketplaceEnabled && ( @@ -156,10 +154,22 @@ export function SidebarLeft({ )} + {customAgentsEnabled && ( + + + + + Agent Playground + + + + )} {customAgentsEnabled && ( diff --git a/frontend/src/components/thread/chat-input/chat-input.tsx b/frontend/src/components/thread/chat-input/chat-input.tsx index faac0c2a..339c3c05 100644 --- a/frontend/src/components/thread/chat-input/chat-input.tsx +++ b/frontend/src/components/thread/chat-input/chat-input.tsx @@ -311,7 +311,7 @@ export const ChatInput = forwardRef( - {isAgentRunning && ( + {/* {isAgentRunning && ( ( {agentName ? `${agentName} is working...` : 'Suna is working...'} - )} + )} */} + ); }, diff --git a/frontend/src/components/thread/thread-site-header.tsx b/frontend/src/components/thread/thread-site-header.tsx index d8c57aca..67044d36 100644 --- a/frontend/src/components/thread/thread-site-header.tsx +++ b/frontend/src/components/thread/thread-site-header.tsx @@ -264,7 +264,7 @@ export function SiteHeader({ - + {/*