oagi 0.4.0__tar.gz → 0.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of oagi might be problematic. Click here for more details.
- {oagi-0.4.0 → oagi-0.4.2}/PKG-INFO +3 -1
- {oagi-0.4.0 → oagi-0.4.2}/README.md +2 -0
- {oagi-0.4.0 → oagi-0.4.2}/pyproject.toml +1 -1
- {oagi-0.4.0 → oagi-0.4.2}/src/oagi/pyautogui_action_handler.py +73 -2
- {oagi-0.4.0 → oagi-0.4.2}/src/oagi/types/models/action.py +1 -0
- {oagi-0.4.0 → oagi-0.4.2}/tests/test_async_task.py +3 -1
- oagi-0.4.2/tests/test_pyautogui_action_handler.py +251 -0
- {oagi-0.4.0 → oagi-0.4.2}/tests/test_short_task.py +6 -2
- {oagi-0.4.0 → oagi-0.4.2}/uv.lock +1 -1
- oagi-0.4.0/tests/test_pyautogui_action_handler.py +0 -143
- {oagi-0.4.0 → oagi-0.4.2}/.github/workflows/ci.yml +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/.github/workflows/release.yml +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/.gitignore +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/.python-version +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/CONTRIBUTING.md +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/LICENSE +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/Makefile +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/examples/async_google_weather.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/examples/execute_task_auto.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/examples/execute_task_manual.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/examples/google_weather.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/examples/hotel_booking.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/examples/screenshot_with_config.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/examples/single_step.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/src/oagi/__init__.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/src/oagi/async_client.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/src/oagi/async_pyautogui_action_handler.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/src/oagi/async_screenshot_maker.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/src/oagi/async_short_task.py +3 -3
- {oagi-0.4.0 → oagi-0.4.2}/src/oagi/async_single_step.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/src/oagi/async_task.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/src/oagi/exceptions.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/src/oagi/logging.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/src/oagi/pil_image.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/src/oagi/screenshot_maker.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/src/oagi/short_task.py +3 -3
- {oagi-0.4.0 → oagi-0.4.2}/src/oagi/single_step.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/src/oagi/sync_client.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/src/oagi/task.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/src/oagi/types/__init__.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/src/oagi/types/action_handler.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/src/oagi/types/async_action_handler.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/src/oagi/types/async_image_provider.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/src/oagi/types/image.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/src/oagi/types/image_provider.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/src/oagi/types/models/__init__.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/src/oagi/types/models/image_config.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/src/oagi/types/models/step.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/tests/__init__.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/tests/conftest.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/tests/test_async_client.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/tests/test_async_handlers.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/tests/test_logging.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/tests/test_pil_image.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/tests/test_screenshot_maker.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/tests/test_single_step.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/tests/test_sync_client.py +0 -0
- {oagi-0.4.0 → oagi-0.4.2}/tests/test_task.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: oagi
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Official API of OpenAGI Foundation
|
|
5
5
|
Project-URL: Homepage, https://github.com/agiopen-org/oagi
|
|
6
6
|
Author-email: OpenAGI Foundation <contact@agiopen.org>
|
|
@@ -93,6 +93,8 @@ config = PyautoguiConfig(
|
|
|
93
93
|
scroll_amount=50, # Larger scroll steps (default: 30)
|
|
94
94
|
wait_duration=2.0, # Longer waits (default: 1.0)
|
|
95
95
|
action_pause=0.2, # More pause between actions (default: 0.1)
|
|
96
|
+
hotkey_interval=0.1, # Interval between keys in hotkey combinations (default: 0.1)
|
|
97
|
+
capslock_mode="session" # Caps lock mode: 'session' or 'system' (default: 'session')
|
|
96
98
|
)
|
|
97
99
|
|
|
98
100
|
executor = PyautoguiActionHandler(config=config)
|
|
@@ -59,6 +59,8 @@ config = PyautoguiConfig(
|
|
|
59
59
|
scroll_amount=50, # Larger scroll steps (default: 30)
|
|
60
60
|
wait_duration=2.0, # Longer waits (default: 1.0)
|
|
61
61
|
action_pause=0.2, # More pause between actions (default: 0.1)
|
|
62
|
+
hotkey_interval=0.1, # Interval between keys in hotkey combinations (default: 0.1)
|
|
63
|
+
capslock_mode="session" # Caps lock mode: 'session' or 'system' (default: 'session')
|
|
62
64
|
)
|
|
63
65
|
|
|
64
66
|
executor = PyautoguiActionHandler(config=config)
|
|
@@ -15,6 +15,42 @@ from pydantic import BaseModel, Field
|
|
|
15
15
|
from .types import Action, ActionType
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
class CapsLockManager:
|
|
19
|
+
"""Manages caps lock state for text transformation."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, mode: str = "session"):
|
|
22
|
+
"""Initialize caps lock manager.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
mode: Either "session" (internal state) or "system" (OS-level)
|
|
26
|
+
"""
|
|
27
|
+
self.mode = mode
|
|
28
|
+
self.caps_enabled = False
|
|
29
|
+
|
|
30
|
+
def toggle(self):
|
|
31
|
+
"""Toggle caps lock state in session mode."""
|
|
32
|
+
if self.mode == "session":
|
|
33
|
+
self.caps_enabled = not self.caps_enabled
|
|
34
|
+
|
|
35
|
+
def transform_text(self, text: str) -> str:
|
|
36
|
+
"""Transform text based on caps lock state.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
text: Input text to transform
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Transformed text (uppercase if caps enabled in session mode)
|
|
43
|
+
"""
|
|
44
|
+
if self.mode == "session" and self.caps_enabled:
|
|
45
|
+
# Transform letters to uppercase, preserve special characters
|
|
46
|
+
return "".join(c.upper() if c.isalpha() else c for c in text)
|
|
47
|
+
return text
|
|
48
|
+
|
|
49
|
+
def should_use_system_capslock(self) -> bool:
|
|
50
|
+
"""Check if system-level caps lock should be used."""
|
|
51
|
+
return self.mode == "system"
|
|
52
|
+
|
|
53
|
+
|
|
18
54
|
class PyautoguiConfig(BaseModel):
|
|
19
55
|
"""Configuration for PyautoguiActionHandler."""
|
|
20
56
|
|
|
@@ -30,6 +66,13 @@ class PyautoguiConfig(BaseModel):
|
|
|
30
66
|
action_pause: float = Field(
|
|
31
67
|
default=0.1, description="Pause between PyAutoGUI actions in seconds"
|
|
32
68
|
)
|
|
69
|
+
hotkey_interval: float = Field(
|
|
70
|
+
default=0.1, description="Interval between key presses in hotkey combinations"
|
|
71
|
+
)
|
|
72
|
+
capslock_mode: str = Field(
|
|
73
|
+
default="session",
|
|
74
|
+
description="Caps lock handling mode: 'session' (internal state) or 'system' (OS-level)",
|
|
75
|
+
)
|
|
33
76
|
|
|
34
77
|
|
|
35
78
|
class PyautoguiActionHandler:
|
|
@@ -54,6 +97,8 @@ class PyautoguiActionHandler:
|
|
|
54
97
|
self.screen_width, self.screen_height = pyautogui.size()
|
|
55
98
|
# Set default delay between actions
|
|
56
99
|
pyautogui.PAUSE = self.config.action_pause
|
|
100
|
+
# Initialize caps lock manager
|
|
101
|
+
self.caps_manager = CapsLockManager(mode=self.config.capslock_mode)
|
|
57
102
|
|
|
58
103
|
def _denormalize_coords(self, x: float, y: float) -> tuple[int, int]:
|
|
59
104
|
"""Convert coordinates from 0-1000 range to actual screen coordinates."""
|
|
@@ -94,12 +139,20 @@ class PyautoguiActionHandler:
|
|
|
94
139
|
direction = match.group(3).lower()
|
|
95
140
|
return x, y, direction
|
|
96
141
|
|
|
142
|
+
def _normalize_key(self, key: str) -> str:
|
|
143
|
+
"""Normalize key names for consistency."""
|
|
144
|
+
key = key.strip().lower()
|
|
145
|
+
# Normalize caps lock variations
|
|
146
|
+
if key in ["caps_lock", "caps", "capslock"]:
|
|
147
|
+
return "capslock"
|
|
148
|
+
return key
|
|
149
|
+
|
|
97
150
|
def _parse_hotkey(self, args_str: str) -> list[str]:
|
|
98
151
|
"""Parse hotkey string into list of keys."""
|
|
99
152
|
# Remove parentheses if present
|
|
100
153
|
args_str = args_str.strip("()")
|
|
101
154
|
# Split by '+' to get individual keys
|
|
102
|
-
keys = [
|
|
155
|
+
keys = [self._normalize_key(key) for key in args_str.split("+")]
|
|
103
156
|
return keys
|
|
104
157
|
|
|
105
158
|
def _execute_single_action(self, action: Action) -> None:
|
|
@@ -115,6 +168,10 @@ class PyautoguiActionHandler:
|
|
|
115
168
|
x, y = self._parse_coords(arg)
|
|
116
169
|
pyautogui.doubleClick(x, y)
|
|
117
170
|
|
|
171
|
+
case ActionType.LEFT_TRIPLE:
|
|
172
|
+
x, y = self._parse_coords(arg)
|
|
173
|
+
pyautogui.tripleClick(x, y)
|
|
174
|
+
|
|
118
175
|
case ActionType.RIGHT_SINGLE:
|
|
119
176
|
x, y = self._parse_coords(arg)
|
|
120
177
|
pyautogui.rightClick(x, y)
|
|
@@ -128,11 +185,25 @@ class PyautoguiActionHandler:
|
|
|
128
185
|
|
|
129
186
|
case ActionType.HOTKEY:
|
|
130
187
|
keys = self._parse_hotkey(arg)
|
|
131
|
-
|
|
188
|
+
# Check if this is a caps lock key press
|
|
189
|
+
if len(keys) == 1 and keys[0] == "capslock":
|
|
190
|
+
if self.caps_manager.should_use_system_capslock():
|
|
191
|
+
# System mode: use OS-level caps lock
|
|
192
|
+
pyautogui.hotkey(
|
|
193
|
+
"capslock", interval=self.config.hotkey_interval
|
|
194
|
+
)
|
|
195
|
+
else:
|
|
196
|
+
# Session mode: toggle internal state
|
|
197
|
+
self.caps_manager.toggle()
|
|
198
|
+
else:
|
|
199
|
+
# Regular hotkey combination
|
|
200
|
+
pyautogui.hotkey(*keys, interval=self.config.hotkey_interval)
|
|
132
201
|
|
|
133
202
|
case ActionType.TYPE:
|
|
134
203
|
# Remove quotes if present
|
|
135
204
|
text = arg.strip("\"'")
|
|
205
|
+
# Apply caps lock transformation if needed
|
|
206
|
+
text = self.caps_manager.transform_text(text)
|
|
136
207
|
pyautogui.typewrite(text)
|
|
137
208
|
|
|
138
209
|
case ActionType.SCROLL:
|
|
@@ -175,7 +175,9 @@ class TestAsyncShortTask:
|
|
|
175
175
|
)
|
|
176
176
|
|
|
177
177
|
assert result is True
|
|
178
|
-
assert
|
|
178
|
+
assert (
|
|
179
|
+
mock_executor.call_count == 2
|
|
180
|
+
) # Called for both steps including the completed one
|
|
179
181
|
assert mock_image_provider.call_count == 2
|
|
180
182
|
|
|
181
183
|
await task.close()
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# -----------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) OpenAGI Foundation
|
|
3
|
+
# All rights reserved.
|
|
4
|
+
#
|
|
5
|
+
# This file is part of the official API project.
|
|
6
|
+
# Licensed under the MIT License.
|
|
7
|
+
# -----------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
from unittest.mock import patch
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from oagi.pyautogui_action_handler import (
|
|
14
|
+
CapsLockManager,
|
|
15
|
+
PyautoguiActionHandler,
|
|
16
|
+
PyautoguiConfig,
|
|
17
|
+
)
|
|
18
|
+
from oagi.types import Action, ActionType
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def mock_pyautogui():
|
|
23
|
+
with patch("oagi.pyautogui_action_handler.pyautogui") as mock:
|
|
24
|
+
mock.size.return_value = (1920, 1080)
|
|
25
|
+
yield mock
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.fixture
|
|
29
|
+
def config():
|
|
30
|
+
return PyautoguiConfig()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.fixture
|
|
34
|
+
def handler(mock_pyautogui):
|
|
35
|
+
return PyautoguiActionHandler()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.mark.parametrize(
|
|
39
|
+
"action_type,argument,expected_method,expected_coords",
|
|
40
|
+
[
|
|
41
|
+
(ActionType.CLICK, "500, 300", "click", (960, 324)),
|
|
42
|
+
(ActionType.LEFT_DOUBLE, "400, 250", "doubleClick", (768, 270)),
|
|
43
|
+
(ActionType.LEFT_TRIPLE, "350, 200", "tripleClick", (672, 216)),
|
|
44
|
+
(ActionType.RIGHT_SINGLE, "600, 400", "rightClick", (1152, 432)),
|
|
45
|
+
],
|
|
46
|
+
)
|
|
47
|
+
def test_coordinate_based_actions(
|
|
48
|
+
handler, mock_pyautogui, action_type, argument, expected_method, expected_coords
|
|
49
|
+
):
|
|
50
|
+
action = Action(type=action_type, argument=argument, count=1)
|
|
51
|
+
handler([action])
|
|
52
|
+
|
|
53
|
+
getattr(mock_pyautogui, expected_method).assert_called_once_with(*expected_coords)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_drag_action(handler, mock_pyautogui, config):
|
|
57
|
+
action = Action(type=ActionType.DRAG, argument="100, 100, 500, 300", count=1)
|
|
58
|
+
handler([action])
|
|
59
|
+
|
|
60
|
+
mock_pyautogui.moveTo.assert_any_call(192, 108)
|
|
61
|
+
mock_pyautogui.dragTo.assert_called_once_with(
|
|
62
|
+
960, 324, duration=config.drag_duration, button="left"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_hotkey_action(handler, mock_pyautogui, config):
|
|
67
|
+
action = Action(type=ActionType.HOTKEY, argument="ctrl+c", count=1)
|
|
68
|
+
handler([action])
|
|
69
|
+
|
|
70
|
+
mock_pyautogui.hotkey.assert_called_once_with(
|
|
71
|
+
"ctrl", "c", interval=config.hotkey_interval
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_type_action(handler, mock_pyautogui):
|
|
76
|
+
action = Action(type=ActionType.TYPE, argument="Hello World", count=1)
|
|
77
|
+
handler([action])
|
|
78
|
+
|
|
79
|
+
mock_pyautogui.typewrite.assert_called_once_with("Hello World")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@pytest.mark.parametrize(
|
|
83
|
+
"direction,expected_amount_multiplier",
|
|
84
|
+
[("up", 1), ("down", -1)],
|
|
85
|
+
)
|
|
86
|
+
def test_scroll_actions(
|
|
87
|
+
handler, mock_pyautogui, config, direction, expected_amount_multiplier
|
|
88
|
+
):
|
|
89
|
+
action = Action(type=ActionType.SCROLL, argument=f"500, 300, {direction}", count=1)
|
|
90
|
+
handler([action])
|
|
91
|
+
|
|
92
|
+
mock_pyautogui.moveTo.assert_called_once_with(960, 324)
|
|
93
|
+
expected_scroll_amount = config.scroll_amount * expected_amount_multiplier
|
|
94
|
+
mock_pyautogui.scroll.assert_called_once_with(expected_scroll_amount)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_wait_action(handler, mock_pyautogui, config):
|
|
98
|
+
with patch("time.sleep") as mock_sleep:
|
|
99
|
+
action = Action(type=ActionType.WAIT, argument="", count=1)
|
|
100
|
+
handler([action])
|
|
101
|
+
mock_sleep.assert_called_once_with(config.wait_duration)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_hotkey_with_custom_interval(mock_pyautogui):
|
|
105
|
+
custom_config = PyautoguiConfig(hotkey_interval=0.5)
|
|
106
|
+
handler = PyautoguiActionHandler(config=custom_config)
|
|
107
|
+
|
|
108
|
+
action = Action(type=ActionType.HOTKEY, argument="cmd+shift+a", count=1)
|
|
109
|
+
handler([action])
|
|
110
|
+
|
|
111
|
+
mock_pyautogui.hotkey.assert_called_once_with("cmd", "shift", "a", interval=0.5)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_finish_action(handler, mock_pyautogui):
|
|
115
|
+
action = Action(type=ActionType.FINISH, argument="", count=1)
|
|
116
|
+
handler([action])
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_call_user_action(handler, mock_pyautogui, capsys):
|
|
120
|
+
action = Action(type=ActionType.CALL_USER, argument="", count=1)
|
|
121
|
+
handler([action])
|
|
122
|
+
|
|
123
|
+
captured = capsys.readouterr()
|
|
124
|
+
assert "User intervention requested" in captured.out
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TestActionExecution:
|
|
128
|
+
def test_multiple_count(self, handler, mock_pyautogui):
|
|
129
|
+
action = Action(type=ActionType.CLICK, argument="500, 300", count=3)
|
|
130
|
+
handler([action])
|
|
131
|
+
|
|
132
|
+
assert mock_pyautogui.click.call_count == 3
|
|
133
|
+
|
|
134
|
+
def test_multiple_actions(self, handler, mock_pyautogui):
|
|
135
|
+
actions = [
|
|
136
|
+
Action(type=ActionType.CLICK, argument="100, 100", count=1),
|
|
137
|
+
Action(type=ActionType.TYPE, argument="test", count=1),
|
|
138
|
+
Action(type=ActionType.HOTKEY, argument="ctrl+s", count=1),
|
|
139
|
+
]
|
|
140
|
+
handler(actions)
|
|
141
|
+
|
|
142
|
+
mock_pyautogui.click.assert_called_once()
|
|
143
|
+
mock_pyautogui.typewrite.assert_called_once_with("test")
|
|
144
|
+
mock_pyautogui.hotkey.assert_called_once_with("ctrl", "s", interval=0.1)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class TestInputValidation:
|
|
148
|
+
def test_invalid_coordinates_format(self, handler, mock_pyautogui):
|
|
149
|
+
action = Action(type=ActionType.CLICK, argument="invalid", count=1)
|
|
150
|
+
|
|
151
|
+
with pytest.raises(ValueError, match="Invalid coordinates format"):
|
|
152
|
+
handler([action])
|
|
153
|
+
|
|
154
|
+
def test_type_with_quotes(self, handler, mock_pyautogui):
|
|
155
|
+
action = Action(type=ActionType.TYPE, argument='"Hello World"', count=1)
|
|
156
|
+
handler([action])
|
|
157
|
+
|
|
158
|
+
mock_pyautogui.typewrite.assert_called_once_with("Hello World")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class TestCapsLockManager:
|
|
162
|
+
def test_session_mode_text_transformation(self):
|
|
163
|
+
manager = CapsLockManager(mode="session")
|
|
164
|
+
|
|
165
|
+
# Initially caps is off
|
|
166
|
+
assert manager.transform_text("Hello World") == "Hello World"
|
|
167
|
+
assert manager.transform_text("123!@#") == "123!@#"
|
|
168
|
+
|
|
169
|
+
# Toggle caps on
|
|
170
|
+
manager.toggle()
|
|
171
|
+
assert manager.caps_enabled is True
|
|
172
|
+
assert manager.transform_text("Hello World") == "HELLO WORLD"
|
|
173
|
+
assert manager.transform_text("test123!") == "TEST123!"
|
|
174
|
+
assert manager.transform_text("123!@#") == "123!@#"
|
|
175
|
+
|
|
176
|
+
# Toggle caps off
|
|
177
|
+
manager.toggle()
|
|
178
|
+
assert manager.caps_enabled is False
|
|
179
|
+
assert manager.transform_text("Hello World") == "Hello World"
|
|
180
|
+
|
|
181
|
+
def test_system_mode_no_transformation(self):
|
|
182
|
+
manager = CapsLockManager(mode="system")
|
|
183
|
+
|
|
184
|
+
# System mode doesn't transform text
|
|
185
|
+
assert manager.transform_text("Hello World") == "Hello World"
|
|
186
|
+
|
|
187
|
+
manager.toggle() # Should not affect state in system mode
|
|
188
|
+
assert manager.caps_enabled is False
|
|
189
|
+
assert manager.transform_text("Hello World") == "Hello World"
|
|
190
|
+
|
|
191
|
+
def test_should_use_system_capslock(self):
|
|
192
|
+
session_manager = CapsLockManager(mode="session")
|
|
193
|
+
system_manager = CapsLockManager(mode="system")
|
|
194
|
+
|
|
195
|
+
assert session_manager.should_use_system_capslock() is False
|
|
196
|
+
assert system_manager.should_use_system_capslock() is True
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class TestCapsLockIntegration:
|
|
200
|
+
def test_caps_lock_key_normalization(self, mock_pyautogui):
|
|
201
|
+
handler = PyautoguiActionHandler()
|
|
202
|
+
|
|
203
|
+
# Test different caps lock variations
|
|
204
|
+
for variant in ["caps", "caps_lock", "capslock"]:
|
|
205
|
+
keys = handler._parse_hotkey(variant)
|
|
206
|
+
assert keys == ["capslock"]
|
|
207
|
+
|
|
208
|
+
def test_caps_lock_session_mode(self, mock_pyautogui):
|
|
209
|
+
config = PyautoguiConfig(capslock_mode="session")
|
|
210
|
+
handler = PyautoguiActionHandler(config=config)
|
|
211
|
+
|
|
212
|
+
# Type without caps
|
|
213
|
+
type_action = Action(type=ActionType.TYPE, argument="test", count=1)
|
|
214
|
+
handler([type_action])
|
|
215
|
+
mock_pyautogui.typewrite.assert_called_with("test")
|
|
216
|
+
|
|
217
|
+
# Toggle caps lock
|
|
218
|
+
caps_action = Action(type=ActionType.HOTKEY, argument="caps_lock", count=1)
|
|
219
|
+
handler([caps_action])
|
|
220
|
+
# In session mode, should not call pyautogui.hotkey for capslock
|
|
221
|
+
assert mock_pyautogui.hotkey.call_count == 0
|
|
222
|
+
|
|
223
|
+
# Type with caps enabled
|
|
224
|
+
mock_pyautogui.typewrite.reset_mock()
|
|
225
|
+
type_action = Action(type=ActionType.TYPE, argument="test", count=1)
|
|
226
|
+
handler([type_action])
|
|
227
|
+
mock_pyautogui.typewrite.assert_called_with("TEST")
|
|
228
|
+
|
|
229
|
+
def test_caps_lock_system_mode(self, mock_pyautogui):
|
|
230
|
+
config = PyautoguiConfig(capslock_mode="system")
|
|
231
|
+
handler = PyautoguiActionHandler(config=config)
|
|
232
|
+
|
|
233
|
+
# Toggle caps lock in system mode
|
|
234
|
+
caps_action = Action(type=ActionType.HOTKEY, argument="caps", count=1)
|
|
235
|
+
handler([caps_action])
|
|
236
|
+
# In system mode, should call pyautogui.hotkey
|
|
237
|
+
mock_pyautogui.hotkey.assert_called_once_with("capslock", interval=0.1)
|
|
238
|
+
|
|
239
|
+
# Type action should not transform text in system mode
|
|
240
|
+
mock_pyautogui.typewrite.reset_mock()
|
|
241
|
+
type_action = Action(type=ActionType.TYPE, argument="test", count=1)
|
|
242
|
+
handler([type_action])
|
|
243
|
+
mock_pyautogui.typewrite.assert_called_with("test")
|
|
244
|
+
|
|
245
|
+
def test_regular_hotkey_not_affected(self, mock_pyautogui):
|
|
246
|
+
handler = PyautoguiActionHandler()
|
|
247
|
+
|
|
248
|
+
# Regular hotkeys should work normally
|
|
249
|
+
action = Action(type=ActionType.HOTKEY, argument="ctrl+c", count=1)
|
|
250
|
+
handler([action])
|
|
251
|
+
mock_pyautogui.hotkey.assert_called_once_with("ctrl", "c", interval=0.1)
|
|
@@ -60,7 +60,9 @@ class TestShortTaskAutoMode:
|
|
|
60
60
|
)
|
|
61
61
|
|
|
62
62
|
assert result is True
|
|
63
|
-
assert
|
|
63
|
+
assert (
|
|
64
|
+
mock_executor.call_count == 3
|
|
65
|
+
) # Called for all 3 steps including the completed one
|
|
64
66
|
assert mock_image_provider.call_count == 3 # Called for all 3 steps
|
|
65
67
|
|
|
66
68
|
def test_auto_mode_max_steps_reached(self, short_task, sample_llm_response):
|
|
@@ -133,7 +135,9 @@ class TestShortTaskAutoMode:
|
|
|
133
135
|
)
|
|
134
136
|
|
|
135
137
|
assert result is True
|
|
136
|
-
assert
|
|
138
|
+
assert (
|
|
139
|
+
mock_executor.call_count == 1
|
|
140
|
+
) # Actions are executed even when task is complete
|
|
137
141
|
assert mock_image_provider.call_count == 1
|
|
138
142
|
|
|
139
143
|
def test_auto_mode_with_default_parameters(
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
# -----------------------------------------------------------------------------
|
|
2
|
-
# Copyright (c) OpenAGI Foundation
|
|
3
|
-
# All rights reserved.
|
|
4
|
-
#
|
|
5
|
-
# This file is part of the official API project.
|
|
6
|
-
# Licensed under the MIT License.
|
|
7
|
-
# -----------------------------------------------------------------------------
|
|
8
|
-
|
|
9
|
-
from unittest.mock import patch
|
|
10
|
-
|
|
11
|
-
import pytest
|
|
12
|
-
|
|
13
|
-
from oagi.pyautogui_action_handler import PyautoguiActionHandler, PyautoguiConfig
|
|
14
|
-
from oagi.types import Action, ActionType
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@pytest.fixture
|
|
18
|
-
def mock_pyautogui():
|
|
19
|
-
with patch("oagi.pyautogui_action_handler.pyautogui") as mock:
|
|
20
|
-
mock.size.return_value = (1920, 1080)
|
|
21
|
-
yield mock
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@pytest.fixture
|
|
25
|
-
def config():
|
|
26
|
-
return PyautoguiConfig()
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
@pytest.fixture
|
|
30
|
-
def handler(mock_pyautogui):
|
|
31
|
-
return PyautoguiActionHandler()
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@pytest.mark.parametrize(
|
|
35
|
-
"action_type,argument,expected_method,expected_coords",
|
|
36
|
-
[
|
|
37
|
-
(ActionType.CLICK, "500, 300", "click", (960, 324)),
|
|
38
|
-
(ActionType.LEFT_DOUBLE, "400, 250", "doubleClick", (768, 270)),
|
|
39
|
-
(ActionType.RIGHT_SINGLE, "600, 400", "rightClick", (1152, 432)),
|
|
40
|
-
],
|
|
41
|
-
)
|
|
42
|
-
def test_coordinate_based_actions(
|
|
43
|
-
handler, mock_pyautogui, action_type, argument, expected_method, expected_coords
|
|
44
|
-
):
|
|
45
|
-
action = Action(type=action_type, argument=argument, count=1)
|
|
46
|
-
handler([action])
|
|
47
|
-
|
|
48
|
-
getattr(mock_pyautogui, expected_method).assert_called_once_with(*expected_coords)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def test_drag_action(handler, mock_pyautogui, config):
|
|
52
|
-
action = Action(type=ActionType.DRAG, argument="100, 100, 500, 300", count=1)
|
|
53
|
-
handler([action])
|
|
54
|
-
|
|
55
|
-
mock_pyautogui.moveTo.assert_any_call(192, 108)
|
|
56
|
-
mock_pyautogui.dragTo.assert_called_once_with(
|
|
57
|
-
960, 324, duration=config.drag_duration, button="left"
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
@pytest.mark.parametrize(
|
|
62
|
-
"action_type,argument,expected_method,expected_args",
|
|
63
|
-
[
|
|
64
|
-
(ActionType.HOTKEY, "ctrl+c", "hotkey", ("ctrl", "c")),
|
|
65
|
-
(ActionType.TYPE, "Hello World", "typewrite", ("Hello World",)),
|
|
66
|
-
],
|
|
67
|
-
)
|
|
68
|
-
def test_text_based_actions(
|
|
69
|
-
handler, mock_pyautogui, action_type, argument, expected_method, expected_args
|
|
70
|
-
):
|
|
71
|
-
action = Action(type=action_type, argument=argument, count=1)
|
|
72
|
-
handler([action])
|
|
73
|
-
|
|
74
|
-
getattr(mock_pyautogui, expected_method).assert_called_once_with(*expected_args)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
@pytest.mark.parametrize(
|
|
78
|
-
"direction,expected_amount_multiplier",
|
|
79
|
-
[("up", 1), ("down", -1)],
|
|
80
|
-
)
|
|
81
|
-
def test_scroll_actions(
|
|
82
|
-
handler, mock_pyautogui, config, direction, expected_amount_multiplier
|
|
83
|
-
):
|
|
84
|
-
action = Action(type=ActionType.SCROLL, argument=f"500, 300, {direction}", count=1)
|
|
85
|
-
handler([action])
|
|
86
|
-
|
|
87
|
-
mock_pyautogui.moveTo.assert_called_once_with(960, 324)
|
|
88
|
-
expected_scroll_amount = config.scroll_amount * expected_amount_multiplier
|
|
89
|
-
mock_pyautogui.scroll.assert_called_once_with(expected_scroll_amount)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def test_wait_action(handler, mock_pyautogui, config):
|
|
93
|
-
with patch("time.sleep") as mock_sleep:
|
|
94
|
-
action = Action(type=ActionType.WAIT, argument="", count=1)
|
|
95
|
-
handler([action])
|
|
96
|
-
mock_sleep.assert_called_once_with(config.wait_duration)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def test_finish_action(handler, mock_pyautogui):
|
|
100
|
-
action = Action(type=ActionType.FINISH, argument="", count=1)
|
|
101
|
-
handler([action])
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def test_call_user_action(handler, mock_pyautogui, capsys):
|
|
105
|
-
action = Action(type=ActionType.CALL_USER, argument="", count=1)
|
|
106
|
-
handler([action])
|
|
107
|
-
|
|
108
|
-
captured = capsys.readouterr()
|
|
109
|
-
assert "User intervention requested" in captured.out
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
class TestActionExecution:
|
|
113
|
-
def test_multiple_count(self, handler, mock_pyautogui):
|
|
114
|
-
action = Action(type=ActionType.CLICK, argument="500, 300", count=3)
|
|
115
|
-
handler([action])
|
|
116
|
-
|
|
117
|
-
assert mock_pyautogui.click.call_count == 3
|
|
118
|
-
|
|
119
|
-
def test_multiple_actions(self, handler, mock_pyautogui):
|
|
120
|
-
actions = [
|
|
121
|
-
Action(type=ActionType.CLICK, argument="100, 100", count=1),
|
|
122
|
-
Action(type=ActionType.TYPE, argument="test", count=1),
|
|
123
|
-
Action(type=ActionType.HOTKEY, argument="ctrl+s", count=1),
|
|
124
|
-
]
|
|
125
|
-
handler(actions)
|
|
126
|
-
|
|
127
|
-
mock_pyautogui.click.assert_called_once()
|
|
128
|
-
mock_pyautogui.typewrite.assert_called_once_with("test")
|
|
129
|
-
mock_pyautogui.hotkey.assert_called_once_with("ctrl", "s")
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
class TestInputValidation:
|
|
133
|
-
def test_invalid_coordinates_format(self, handler, mock_pyautogui):
|
|
134
|
-
action = Action(type=ActionType.CLICK, argument="invalid", count=1)
|
|
135
|
-
|
|
136
|
-
with pytest.raises(ValueError, match="Invalid coordinates format"):
|
|
137
|
-
handler([action])
|
|
138
|
-
|
|
139
|
-
def test_type_with_quotes(self, handler, mock_pyautogui):
|
|
140
|
-
action = Action(type=ActionType.TYPE, argument='"Hello World"', count=1)
|
|
141
|
-
handler([action])
|
|
142
|
-
|
|
143
|
-
mock_pyautogui.typewrite.assert_called_once_with("Hello World")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -41,14 +41,14 @@ class AsyncShortTask(AsyncTask):
|
|
|
41
41
|
logger.debug(f"Async auto mode step {i + 1}/{max_steps}")
|
|
42
42
|
image = await image_provider()
|
|
43
43
|
step = await self.step(image)
|
|
44
|
+
if executor:
|
|
45
|
+
logger.debug(f"Executing {len(step.actions)} actions asynchronously")
|
|
46
|
+
await executor(step.actions)
|
|
44
47
|
if step.stop:
|
|
45
48
|
logger.info(
|
|
46
49
|
f"Async auto mode completed successfully after {i + 1} steps"
|
|
47
50
|
)
|
|
48
51
|
return True
|
|
49
|
-
if executor:
|
|
50
|
-
logger.debug(f"Executing {len(step.actions)} actions asynchronously")
|
|
51
|
-
await executor(step.actions)
|
|
52
52
|
|
|
53
53
|
logger.warning(
|
|
54
54
|
f"Async auto mode reached max steps ({max_steps}) without completion"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -41,12 +41,12 @@ class ShortTask(Task):
|
|
|
41
41
|
logger.debug(f"Auto mode step {i + 1}/{max_steps}")
|
|
42
42
|
image = image_provider()
|
|
43
43
|
step = self.step(image)
|
|
44
|
-
if step.stop:
|
|
45
|
-
logger.info(f"Auto mode completed successfully after {i + 1} steps")
|
|
46
|
-
return True
|
|
47
44
|
if executor:
|
|
48
45
|
logger.debug(f"Executing {len(step.actions)} actions")
|
|
49
46
|
executor(step.actions)
|
|
47
|
+
if step.stop:
|
|
48
|
+
logger.info(f"Auto mode completed successfully after {i + 1} steps")
|
|
49
|
+
return True
|
|
50
50
|
|
|
51
51
|
logger.warning(f"Auto mode reached max steps ({max_steps}) without completion")
|
|
52
52
|
return False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|