-
Notifications
You must be signed in to change notification settings - Fork 4.4k
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
""" | ||
Clipboard manager. | ||
""" | ||
|
||
from .tool import Clipboardtool |
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 | ||||||||||||||||||||||||||||||||||||||||||||||||||
file_paths: list[str] | ||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+8
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The 📝 Committable Code Suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The 📝 Committable Code Suggestion
Suggested change
|
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)] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
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=[], | ||
) |
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 | ||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential resource leak: The
Comment on lines
+73
to
+79
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
|
||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
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="", | ||||||||||||||||||||||||||||
) |
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="", | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider using
bytes
type forimage_data
instead ofstr
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.