Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: development to master #1490

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions python/composio/tools/local/clipboardtool/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""
Clipboard manager.
"""

from .tool import Clipboardtool
15 changes: 15 additions & 0 deletions python/composio/tools/local/clipboardtool/actions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Clipboard actions."""

from .files import CopyFilePaths, PasteFilePaths
from .image import CopyImage, PasteImage
from .text import CopyText, PasteText


__all__ = [
"CopyText",
"PasteText",
"CopyImage",
"PasteImage",
"CopyFilePaths",
"PasteFilePaths",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Base classes for clipboard actions."""

from typing import Any, Dict, TypedDict

from pydantic import BaseModel, Field


class ClipboardState(TypedDict, total=False):
"""Type definition for clipboard state."""

text_data: str
image_data: str
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using bytes type for image_data instead of str since it's storing binary image data that's base64 encoded. This would make the type hint more accurate and explicit about the expected data type.

file_paths: list[str]
Comment on lines +8 to +13
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ClipboardState TypedDict uses total=False but doesn't handle the case where keys might be missing when accessed. This could lead to KeyError exceptions when trying to access non-existent keys.

📝 Committable Code Suggestion

‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
class ClipboardState(TypedDict, total=False):
"""Type definition for clipboard state."""
text_data: str
image_data: str
file_paths: list[str]
class ClipboardState(TypedDict, total=False):
"""Type definition for clipboard state."""
text_data: Optional[str] = None
image_data: Optional[str] = None
file_paths: Optional[list[str]] = None



class BaseClipboardRequest(BaseModel):
"""Base request for clipboard actions."""

pass


class BaseClipboardResponse(BaseModel):
"""Base response for clipboard actions."""

message: str = Field(
default="",
description="Message describing the result of the action",
)
error: str = Field(
default="",
description="Error message if the action failed",
)


def get_clipboard_state(metadata: Dict[str, Any]) -> ClipboardState:
"""Get clipboard state from metadata.

Args:
metadata: The metadata dictionary containing clipboard state

Returns:
The clipboard state dictionary, initialized if it doesn't exist
"""
if "clipboard_state" not in metadata:
metadata["clipboard_state"] = {}
return metadata["clipboard_state"] # type: ignore
Comment on lines +35 to +46
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The get_clipboard_state function uses # type: ignore to suppress a type error instead of properly typing the return value. The function should explicitly cast the return value to ClipboardState.

📝 Committable Code Suggestion

‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
def get_clipboard_state(metadata: Dict[str, Any]) -> ClipboardState:
"""Get clipboard state from metadata.
Args:
metadata: The metadata dictionary containing clipboard state
Returns:
The clipboard state dictionary, initialized if it doesn't exist
"""
if "clipboard_state" not in metadata:
metadata["clipboard_state"] = {}
return metadata["clipboard_state"] # type: ignore
def get_clipboard_state(metadata: Dict[str, Any]) -> ClipboardState:
"""Get clipboard state from metadata.
Args:
metadata: The metadata dictionary containing clipboard state
Returns:
The clipboard state dictionary, initialized if it doesn't exist
"""
if "clipboard_state" not in metadata:
metadata["clipboard_state"] = {}
return ClipboardState(metadata["clipboard_state"])

101 changes: 101 additions & 0 deletions python/composio/tools/local/clipboardtool/actions/files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""File path clipboard actions."""

import os
from typing import Dict, List

from pydantic import Field

from composio.tools.base.local import LocalAction
from composio.tools.local.clipboardtool.actions.base_action import (
BaseClipboardRequest,
BaseClipboardResponse,
get_clipboard_state,
)


class CopyFilePathsRequest(BaseClipboardRequest):
"""Request to copy file paths to clipboard."""

paths: List[str] = Field(
...,
description="List of file paths to copy to clipboard",
)


class PasteFilePathsRequest(BaseClipboardRequest):
"""Request to paste file paths from clipboard."""

pass


class PasteFilePathsResponse(BaseClipboardResponse):
"""Response from pasting file paths from clipboard."""

paths: List[str] = Field(
default_factory=list,
description="List of file paths pasted from clipboard",
)


class CopyFilePaths(LocalAction[CopyFilePathsRequest, BaseClipboardResponse]):
"""Copy file paths to clipboard."""

def execute(
self, request: CopyFilePathsRequest, metadata: Dict
) -> BaseClipboardResponse:
"""Execute the action."""
try:
# Validate paths exist
valid_paths = [p for p in request.paths if os.path.exists(p)]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding path validation to prevent directory traversal attacks. You might want to use os.path.abspath() and ensure the paths don't escape a designated safe directory.


if not valid_paths:
return BaseClipboardResponse(
error="No valid files found to copy",
)

# Store paths in clipboard state
clipboard_state = get_clipboard_state(metadata)
clipboard_state["file_paths"] = valid_paths

return BaseClipboardResponse(
message="File paths copied to clipboard successfully"
)
except Exception as e:
return BaseClipboardResponse(error=f"Failed to copy file paths: {str(e)}")


class PasteFilePaths(LocalAction[PasteFilePathsRequest, PasteFilePathsResponse]):
"""Paste file paths from clipboard."""

def execute(
self, request: PasteFilePathsRequest, metadata: Dict
) -> PasteFilePathsResponse:
"""Execute the action."""
try:
clipboard_state = get_clipboard_state(metadata)
paths = clipboard_state.get("file_paths", [])

if not paths:
return PasteFilePathsResponse(
error="No files found in clipboard",
paths=[],
)

# Validate paths exist
valid_paths = [p for p in paths if os.path.exists(p)]

if not valid_paths:
return PasteFilePathsResponse(
error="No valid files found in clipboard",
paths=[],
)

return PasteFilePathsResponse(
message="File paths pasted from clipboard successfully",
paths=valid_paths,
)
except Exception as e:
return PasteFilePathsResponse(
error=f"Failed to paste file paths: {str(e)}",
paths=[],
)
133 changes: 133 additions & 0 deletions python/composio/tools/local/clipboardtool/actions/image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"""Image clipboard actions."""

import base64
import os
import logging
import tempfile
import typing as t
from pathlib import Path

from PIL import Image
from pydantic import ConfigDict, Field

from composio.tools.base.local import LocalAction
from composio.tools.local.clipboardtool.actions.base_action import (
BaseClipboardRequest,
BaseClipboardResponse,
get_clipboard_state,
)

logger = logging.getLogger(__name__)


class CopyImageRequest(BaseClipboardRequest):
"""Request to copy image to clipboard."""

model_config = ConfigDict(arbitrary_types_allowed=True)
image_path: str = Field(
default=...,
description="Path to image file to copy to clipboard",
)


class CopyImageResponse(BaseClipboardResponse):
"""Response from copying image to clipboard."""

pass


class PasteImageRequest(BaseClipboardRequest):
"""Request to paste image from clipboard."""

save_path: str = Field(
...,
description="Path to save the pasted image to",
)


class PasteImageResponse(BaseClipboardResponse):
"""Response from pasting image from clipboard."""

image_path: str = Field(
default="",
description="Path to the saved image file",
)


class CopyImage(LocalAction[CopyImageRequest, CopyImageResponse]):
"""Copy image to clipboard."""

def execute(self, request: CopyImageRequest, metadata: t.Dict) -> CopyImageResponse:
"""Execute the action."""
try:
logger.debug(f"Checking if image exists at {request.image_path}")
# Validate image exists
if not os.path.exists(request.image_path):
logger.error(f"Image not found at {request.image_path}")
return CopyImageResponse(
error="Image file not found",
)

logger.debug(f"Opening image from {request.image_path}")
# Store image data in clipboard state
image = Image.open(request.image_path)
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
temp_path = temp_file.name
temp_file.close()

logger.debug(f"Saving temp file to {temp_path}")
image.save(temp_path) # PIL needs a file to copy to clipboard
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential resource leak: The image object is not being closed after use. Consider using a context manager or explicitly calling image.close() after saving to prevent memory leaks, especially when handling large images.

Comment on lines +73 to +79
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CopyImage action creates a temporary PNG file regardless of the original image format, which could cause quality loss for certain image types. Should preserve the original format.


logger.debug("Reading temp file")
with open(temp_path, "rb") as f:
data = f.read()
logger.debug("Cleaning up temp file")
Path(temp_path).unlink() # Clean up temp file

logger.debug("Storing data in clipboard state")
clipboard_state = get_clipboard_state(metadata)
clipboard_state["image_data"] = base64.b64encode(data).decode()

return CopyImageResponse(message="Image copied to clipboard successfully")
except Exception as e:
logger.exception(f"Error occurred: {str(e)}")
return CopyImageResponse(error=f"Failed to copy image: {str(e)}")


class PasteImage(LocalAction[PasteImageRequest, PasteImageResponse]):
"""Paste image from clipboard."""

def execute(
self, request: PasteImageRequest, metadata: t.Dict
) -> PasteImageResponse:
"""Execute the action."""
try:
clipboard_state = get_clipboard_state(metadata)
image_data = clipboard_state.get("image_data")

if not image_data:
logger.warning("No valid image found in clipboard")
return PasteImageResponse(
error="No valid image found in clipboard",
image_path="",
)

# Create destination directory if needed
os.makedirs(os.path.dirname(request.save_path), exist_ok=True)

# Decode and save image
data = base64.b64decode(image_data)
with open(request.save_path, "wb") as f:
f.write(data)
Comment on lines +118 to +121
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code doesn't validate the image format when pasting. It should check if the decoded data is a valid image before writing to the file system.

📝 Committable Code Suggestion

‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
# Decode and save image
data = base64.b64decode(image_data)
with open(request.save_path, "wb") as f:
f.write(data)
# Decode and validate image before saving
data = base64.b64decode(image_data)
try:
# Validate image format using PIL
Image.open(io.BytesIO(data)).verify()
with open(request.save_path, "wb") as f:
f.write(data)
except Exception as e:
raise ValueError(f"Invalid image format: {str(e)}")


logger.debug(f"Image saved to {request.save_path}")
return PasteImageResponse(
message="Image pasted from clipboard successfully",
image_path=request.save_path,
)
except Exception as e:
logger.exception(f"Failed to paste image: {str(e)}")
return PasteImageResponse(
error=f"Failed to paste image: {str(e)}",
image_path="",
)
83 changes: 83 additions & 0 deletions python/composio/tools/local/clipboardtool/actions/text.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""Text clipboard actions."""

import typing as t

from pydantic import Field

from composio.tools.base.local import LocalAction
from composio.tools.local.clipboardtool.actions.base_action import (
BaseClipboardRequest,
BaseClipboardResponse,
get_clipboard_state,
)


class CopyTextRequest(BaseClipboardRequest):
"""Request to copy text to clipboard."""

text: str = Field(
...,
description="Text to copy to clipboard",
)


class CopyTextResponse(BaseClipboardResponse):
"""Response from copying text to clipboard."""

pass


class PasteTextRequest(BaseClipboardRequest):
"""Request to paste text from clipboard."""

pass


class PasteTextResponse(BaseClipboardResponse):
"""Response from pasting text from clipboard."""

text: str = Field(
default="",
description="Text pasted from clipboard",
)


class CopyText(LocalAction[CopyTextRequest, CopyTextResponse]):
"""Copy text to clipboard."""

def execute(self, request: CopyTextRequest, metadata: t.Dict) -> CopyTextResponse:
"""Execute the action."""
try:
# Store text in clipboard state
clipboard_state = get_clipboard_state(metadata)
clipboard_state["text_data"] = request.text

return CopyTextResponse(message="Text copied to clipboard successfully")
except Exception as e:
return CopyTextResponse(error=f"Failed to copy text: {str(e)}")


class PasteText(LocalAction[PasteTextRequest, PasteTextResponse]):
"""Paste text from clipboard."""

def execute(self, request: PasteTextRequest, metadata: t.Dict) -> PasteTextResponse:
"""Execute the action."""
try:
clipboard_state = get_clipboard_state(metadata)
text = clipboard_state.get("text_data", "")

if not text:
return PasteTextResponse(
error="No text found in clipboard",
text="",
)

return PasteTextResponse(
message="Text pasted from clipboard successfully",
text=text,
)
except Exception as e:
return PasteTextResponse(
error=f"Failed to paste text: {str(e)}",
text="",
)
Loading
Loading