oagi 0.4.1__tar.gz → 0.4.3__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.

Files changed (58) hide show
  1. {oagi-0.4.1 → oagi-0.4.3}/.github/workflows/release.yml +10 -3
  2. {oagi-0.4.1 → oagi-0.4.3}/PKG-INFO +3 -1
  3. {oagi-0.4.1 → oagi-0.4.3}/README.md +2 -0
  4. {oagi-0.4.1 → oagi-0.4.3}/pyproject.toml +1 -1
  5. {oagi-0.4.1 → oagi-0.4.3}/src/oagi/pyautogui_action_handler.py +87 -3
  6. oagi-0.4.3/tests/test_pyautogui_action_handler.py +321 -0
  7. {oagi-0.4.1 → oagi-0.4.3}/uv.lock +1 -1
  8. oagi-0.4.1/tests/test_pyautogui_action_handler.py +0 -144
  9. {oagi-0.4.1 → oagi-0.4.3}/.github/workflows/ci.yml +0 -0
  10. {oagi-0.4.1 → oagi-0.4.3}/.gitignore +0 -0
  11. {oagi-0.4.1 → oagi-0.4.3}/.python-version +0 -0
  12. {oagi-0.4.1 → oagi-0.4.3}/CONTRIBUTING.md +0 -0
  13. {oagi-0.4.1 → oagi-0.4.3}/LICENSE +0 -0
  14. {oagi-0.4.1 → oagi-0.4.3}/Makefile +0 -0
  15. {oagi-0.4.1 → oagi-0.4.3}/examples/async_google_weather.py +0 -0
  16. {oagi-0.4.1 → oagi-0.4.3}/examples/execute_task_auto.py +0 -0
  17. {oagi-0.4.1 → oagi-0.4.3}/examples/execute_task_manual.py +0 -0
  18. {oagi-0.4.1 → oagi-0.4.3}/examples/google_weather.py +0 -0
  19. {oagi-0.4.1 → oagi-0.4.3}/examples/hotel_booking.py +0 -0
  20. {oagi-0.4.1 → oagi-0.4.3}/examples/screenshot_with_config.py +0 -0
  21. {oagi-0.4.1 → oagi-0.4.3}/examples/single_step.py +0 -0
  22. {oagi-0.4.1 → oagi-0.4.3}/src/oagi/__init__.py +0 -0
  23. {oagi-0.4.1 → oagi-0.4.3}/src/oagi/async_client.py +0 -0
  24. {oagi-0.4.1 → oagi-0.4.3}/src/oagi/async_pyautogui_action_handler.py +0 -0
  25. {oagi-0.4.1 → oagi-0.4.3}/src/oagi/async_screenshot_maker.py +0 -0
  26. {oagi-0.4.1 → oagi-0.4.3}/src/oagi/async_short_task.py +0 -0
  27. {oagi-0.4.1 → oagi-0.4.3}/src/oagi/async_single_step.py +0 -0
  28. {oagi-0.4.1 → oagi-0.4.3}/src/oagi/async_task.py +0 -0
  29. {oagi-0.4.1 → oagi-0.4.3}/src/oagi/exceptions.py +0 -0
  30. {oagi-0.4.1 → oagi-0.4.3}/src/oagi/logging.py +0 -0
  31. {oagi-0.4.1 → oagi-0.4.3}/src/oagi/pil_image.py +0 -0
  32. {oagi-0.4.1 → oagi-0.4.3}/src/oagi/screenshot_maker.py +0 -0
  33. {oagi-0.4.1 → oagi-0.4.3}/src/oagi/short_task.py +0 -0
  34. {oagi-0.4.1 → oagi-0.4.3}/src/oagi/single_step.py +0 -0
  35. {oagi-0.4.1 → oagi-0.4.3}/src/oagi/sync_client.py +0 -0
  36. {oagi-0.4.1 → oagi-0.4.3}/src/oagi/task.py +0 -0
  37. {oagi-0.4.1 → oagi-0.4.3}/src/oagi/types/__init__.py +0 -0
  38. {oagi-0.4.1 → oagi-0.4.3}/src/oagi/types/action_handler.py +0 -0
  39. {oagi-0.4.1 → oagi-0.4.3}/src/oagi/types/async_action_handler.py +0 -0
  40. {oagi-0.4.1 → oagi-0.4.3}/src/oagi/types/async_image_provider.py +0 -0
  41. {oagi-0.4.1 → oagi-0.4.3}/src/oagi/types/image.py +0 -0
  42. {oagi-0.4.1 → oagi-0.4.3}/src/oagi/types/image_provider.py +0 -0
  43. {oagi-0.4.1 → oagi-0.4.3}/src/oagi/types/models/__init__.py +0 -0
  44. {oagi-0.4.1 → oagi-0.4.3}/src/oagi/types/models/action.py +0 -0
  45. {oagi-0.4.1 → oagi-0.4.3}/src/oagi/types/models/image_config.py +0 -0
  46. {oagi-0.4.1 → oagi-0.4.3}/src/oagi/types/models/step.py +0 -0
  47. {oagi-0.4.1 → oagi-0.4.3}/tests/__init__.py +0 -0
  48. {oagi-0.4.1 → oagi-0.4.3}/tests/conftest.py +0 -0
  49. {oagi-0.4.1 → oagi-0.4.3}/tests/test_async_client.py +0 -0
  50. {oagi-0.4.1 → oagi-0.4.3}/tests/test_async_handlers.py +0 -0
  51. {oagi-0.4.1 → oagi-0.4.3}/tests/test_async_task.py +0 -0
  52. {oagi-0.4.1 → oagi-0.4.3}/tests/test_logging.py +0 -0
  53. {oagi-0.4.1 → oagi-0.4.3}/tests/test_pil_image.py +0 -0
  54. {oagi-0.4.1 → oagi-0.4.3}/tests/test_screenshot_maker.py +0 -0
  55. {oagi-0.4.1 → oagi-0.4.3}/tests/test_short_task.py +0 -0
  56. {oagi-0.4.1 → oagi-0.4.3}/tests/test_single_step.py +0 -0
  57. {oagi-0.4.1 → oagi-0.4.3}/tests/test_sync_client.py +0 -0
  58. {oagi-0.4.1 → oagi-0.4.3}/tests/test_task.py +0 -0
@@ -32,10 +32,17 @@ jobs:
32
32
  password: ${{ secrets.PYPI_API_TOKEN }}
33
33
  # Another option: Use Trusted Publishing (recommended, no token needed)
34
34
  # Configure at: https://pypi.org/manage/project/oagi/settings/publishing/
35
-
35
+
36
+ - name: Get commit message
37
+ id: commit_message
38
+ run: |
39
+ # Get the commit message for the tagged commit
40
+ echo "message<<EOF" >> $GITHUB_OUTPUT
41
+ git log -1 --pretty=%B >> $GITHUB_OUTPUT
42
+ echo "EOF" >> $GITHUB_OUTPUT
36
43
 
37
44
  - name: Create GitHub Release
38
45
  uses: softprops/action-gh-release@v1
39
46
  with:
40
- files: dist/*
41
- generate_release_notes: true
47
+ body: ${{ steps.commit_message.outputs.message }}
48
+ files: dist/*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: oagi
3
- Version: 0.4.1
3
+ Version: 0.4.3
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)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "oagi"
7
- version = "0.4.1"
7
+ version = "0.4.3"
8
8
  description = "Official API of OpenAGI Foundation"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -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,11 +97,30 @@ 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
- """Convert coordinates from 0-1000 range to actual screen coordinates."""
104
+ """Convert coordinates from 0-1000 range to actual screen coordinates.
105
+
106
+ Also handles corner coordinates to prevent PyAutoGUI fail-safe trigger.
107
+ Corner coordinates (0,0), (0,max), (max,0), (max,max) are offset by 1 pixel.
108
+ """
60
109
  screen_x = int(x * self.screen_width / 1000)
61
110
  screen_y = int(y * self.screen_height / 1000)
111
+
112
+ # Prevent fail-safe by adjusting corner coordinates
113
+ # Check if coordinates are at screen corners (with small tolerance)
114
+ if screen_x < 1:
115
+ screen_x = 1
116
+ elif screen_x > self.screen_width - 1:
117
+ screen_x = self.screen_width - 1
118
+
119
+ if screen_y < 1:
120
+ screen_y = 1
121
+ elif screen_y > self.screen_height - 1:
122
+ screen_y = self.screen_height - 1
123
+
62
124
  return screen_x, screen_y
63
125
 
64
126
  def _parse_coords(self, args_str: str) -> tuple[int, int]:
@@ -94,12 +156,20 @@ class PyautoguiActionHandler:
94
156
  direction = match.group(3).lower()
95
157
  return x, y, direction
96
158
 
159
+ def _normalize_key(self, key: str) -> str:
160
+ """Normalize key names for consistency."""
161
+ key = key.strip().lower()
162
+ # Normalize caps lock variations
163
+ if key in ["caps_lock", "caps", "capslock"]:
164
+ return "capslock"
165
+ return key
166
+
97
167
  def _parse_hotkey(self, args_str: str) -> list[str]:
98
168
  """Parse hotkey string into list of keys."""
99
169
  # Remove parentheses if present
100
170
  args_str = args_str.strip("()")
101
171
  # Split by '+' to get individual keys
102
- keys = [key.strip() for key in args_str.split("+")]
172
+ keys = [self._normalize_key(key) for key in args_str.split("+")]
103
173
  return keys
104
174
 
105
175
  def _execute_single_action(self, action: Action) -> None:
@@ -132,11 +202,25 @@ class PyautoguiActionHandler:
132
202
 
133
203
  case ActionType.HOTKEY:
134
204
  keys = self._parse_hotkey(arg)
135
- pyautogui.hotkey(*keys)
205
+ # Check if this is a caps lock key press
206
+ if len(keys) == 1 and keys[0] == "capslock":
207
+ if self.caps_manager.should_use_system_capslock():
208
+ # System mode: use OS-level caps lock
209
+ pyautogui.hotkey(
210
+ "capslock", interval=self.config.hotkey_interval
211
+ )
212
+ else:
213
+ # Session mode: toggle internal state
214
+ self.caps_manager.toggle()
215
+ else:
216
+ # Regular hotkey combination
217
+ pyautogui.hotkey(*keys, interval=self.config.hotkey_interval)
136
218
 
137
219
  case ActionType.TYPE:
138
220
  # Remove quotes if present
139
221
  text = arg.strip("\"'")
222
+ # Apply caps lock transformation if needed
223
+ text = self.caps_manager.transform_text(text)
140
224
  pyautogui.typewrite(text)
141
225
 
142
226
  case ActionType.SCROLL:
@@ -0,0 +1,321 @@
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 TestCornerCoordinatesHandling:
200
+ """Test that corner coordinates are adjusted to prevent PyAutoGUI fail-safe."""
201
+
202
+ @pytest.mark.parametrize(
203
+ "input_coords,expected_coords",
204
+ [
205
+ # Top-left corner
206
+ ("0, 0", (1, 1)),
207
+ ("1, 1", (1, 1)),
208
+ # Top-right corner (assuming 1920x1080 screen)
209
+ ("1000, 0", (1919, 1)),
210
+ ("999, 1", (1918, 1)),
211
+ # Bottom-left corner
212
+ ("0, 1000", (1, 1079)),
213
+ ("1, 999", (1, 1078)),
214
+ # Bottom-right corner
215
+ ("1000, 1000", (1919, 1079)),
216
+ ("999, 999", (1918, 1078)),
217
+ # Middle coordinates should not be affected
218
+ ("500, 500", (960, 540)),
219
+ ("250, 750", (480, 810)),
220
+ ],
221
+ )
222
+ def test_corner_coordinate_adjustment(
223
+ self, mock_pyautogui, input_coords, expected_coords
224
+ ):
225
+ handler = PyautoguiActionHandler()
226
+ action = Action(type=ActionType.CLICK, argument=input_coords, count=1)
227
+ handler([action])
228
+ mock_pyautogui.click.assert_called_once_with(*expected_coords)
229
+
230
+ def test_drag_with_corner_coordinates(self, mock_pyautogui, config):
231
+ """Test drag operations with corner coordinates."""
232
+ handler = PyautoguiActionHandler()
233
+ # Drag from top-left corner to bottom-right corner
234
+ action = Action(type=ActionType.DRAG, argument="0, 0, 1000, 1000", count=1)
235
+ handler([action])
236
+
237
+ # Should adjust corner coordinates to prevent fail-safe
238
+ mock_pyautogui.moveTo.assert_called_once_with(1, 1)
239
+ mock_pyautogui.dragTo.assert_called_once_with(
240
+ 1919, 1079, duration=config.drag_duration, button="left"
241
+ )
242
+
243
+ def test_scroll_with_corner_coordinates(self, mock_pyautogui, config):
244
+ """Test scroll operations at corner coordinates."""
245
+ handler = PyautoguiActionHandler()
246
+ action = Action(type=ActionType.SCROLL, argument="0, 0, up", count=1)
247
+ handler([action])
248
+
249
+ # Should adjust corner coordinates
250
+ mock_pyautogui.moveTo.assert_called_once_with(1, 1)
251
+ mock_pyautogui.scroll.assert_called_once_with(config.scroll_amount)
252
+
253
+ def test_multiple_clicks_at_corners(self, mock_pyautogui):
254
+ """Test multiple clicks at corner positions."""
255
+ handler = PyautoguiActionHandler()
256
+ actions = [
257
+ Action(type=ActionType.LEFT_DOUBLE, argument="0, 0", count=1),
258
+ Action(type=ActionType.LEFT_TRIPLE, argument="1000, 0", count=1),
259
+ Action(type=ActionType.RIGHT_SINGLE, argument="0, 1000", count=1),
260
+ ]
261
+ handler(actions)
262
+
263
+ # All corner coordinates should be adjusted
264
+ mock_pyautogui.doubleClick.assert_called_once_with(1, 1)
265
+ mock_pyautogui.tripleClick.assert_called_once_with(1919, 1)
266
+ mock_pyautogui.rightClick.assert_called_once_with(1, 1079)
267
+
268
+
269
+ class TestCapsLockIntegration:
270
+ def test_caps_lock_key_normalization(self, mock_pyautogui):
271
+ handler = PyautoguiActionHandler()
272
+
273
+ # Test different caps lock variations
274
+ for variant in ["caps", "caps_lock", "capslock"]:
275
+ keys = handler._parse_hotkey(variant)
276
+ assert keys == ["capslock"]
277
+
278
+ def test_caps_lock_session_mode(self, mock_pyautogui):
279
+ config = PyautoguiConfig(capslock_mode="session")
280
+ handler = PyautoguiActionHandler(config=config)
281
+
282
+ # Type without caps
283
+ type_action = Action(type=ActionType.TYPE, argument="test", count=1)
284
+ handler([type_action])
285
+ mock_pyautogui.typewrite.assert_called_with("test")
286
+
287
+ # Toggle caps lock
288
+ caps_action = Action(type=ActionType.HOTKEY, argument="caps_lock", count=1)
289
+ handler([caps_action])
290
+ # In session mode, should not call pyautogui.hotkey for capslock
291
+ assert mock_pyautogui.hotkey.call_count == 0
292
+
293
+ # Type with caps enabled
294
+ mock_pyautogui.typewrite.reset_mock()
295
+ type_action = Action(type=ActionType.TYPE, argument="test", count=1)
296
+ handler([type_action])
297
+ mock_pyautogui.typewrite.assert_called_with("TEST")
298
+
299
+ def test_caps_lock_system_mode(self, mock_pyautogui):
300
+ config = PyautoguiConfig(capslock_mode="system")
301
+ handler = PyautoguiActionHandler(config=config)
302
+
303
+ # Toggle caps lock in system mode
304
+ caps_action = Action(type=ActionType.HOTKEY, argument="caps", count=1)
305
+ handler([caps_action])
306
+ # In system mode, should call pyautogui.hotkey
307
+ mock_pyautogui.hotkey.assert_called_once_with("capslock", interval=0.1)
308
+
309
+ # Type action should not transform text in system mode
310
+ mock_pyautogui.typewrite.reset_mock()
311
+ type_action = Action(type=ActionType.TYPE, argument="test", count=1)
312
+ handler([type_action])
313
+ mock_pyautogui.typewrite.assert_called_with("test")
314
+
315
+ def test_regular_hotkey_not_affected(self, mock_pyautogui):
316
+ handler = PyautoguiActionHandler()
317
+
318
+ # Regular hotkeys should work normally
319
+ action = Action(type=ActionType.HOTKEY, argument="ctrl+c", count=1)
320
+ handler([action])
321
+ mock_pyautogui.hotkey.assert_called_once_with("ctrl", "c", interval=0.1)
@@ -138,7 +138,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/28/fa/b2ba8229b9381e8f6
138
138
 
139
139
  [[package]]
140
140
  name = "oagi"
141
- version = "0.4.1"
141
+ version = "0.4.3"
142
142
  source = { editable = "." }
143
143
  dependencies = [
144
144
  { name = "httpx" },
@@ -1,144 +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.LEFT_TRIPLE, "350, 200", "tripleClick", (672, 216)),
40
- (ActionType.RIGHT_SINGLE, "600, 400", "rightClick", (1152, 432)),
41
- ],
42
- )
43
- def test_coordinate_based_actions(
44
- handler, mock_pyautogui, action_type, argument, expected_method, expected_coords
45
- ):
46
- action = Action(type=action_type, argument=argument, count=1)
47
- handler([action])
48
-
49
- getattr(mock_pyautogui, expected_method).assert_called_once_with(*expected_coords)
50
-
51
-
52
- def test_drag_action(handler, mock_pyautogui, config):
53
- action = Action(type=ActionType.DRAG, argument="100, 100, 500, 300", count=1)
54
- handler([action])
55
-
56
- mock_pyautogui.moveTo.assert_any_call(192, 108)
57
- mock_pyautogui.dragTo.assert_called_once_with(
58
- 960, 324, duration=config.drag_duration, button="left"
59
- )
60
-
61
-
62
- @pytest.mark.parametrize(
63
- "action_type,argument,expected_method,expected_args",
64
- [
65
- (ActionType.HOTKEY, "ctrl+c", "hotkey", ("ctrl", "c")),
66
- (ActionType.TYPE, "Hello World", "typewrite", ("Hello World",)),
67
- ],
68
- )
69
- def test_text_based_actions(
70
- handler, mock_pyautogui, action_type, argument, expected_method, expected_args
71
- ):
72
- action = Action(type=action_type, argument=argument, count=1)
73
- handler([action])
74
-
75
- getattr(mock_pyautogui, expected_method).assert_called_once_with(*expected_args)
76
-
77
-
78
- @pytest.mark.parametrize(
79
- "direction,expected_amount_multiplier",
80
- [("up", 1), ("down", -1)],
81
- )
82
- def test_scroll_actions(
83
- handler, mock_pyautogui, config, direction, expected_amount_multiplier
84
- ):
85
- action = Action(type=ActionType.SCROLL, argument=f"500, 300, {direction}", count=1)
86
- handler([action])
87
-
88
- mock_pyautogui.moveTo.assert_called_once_with(960, 324)
89
- expected_scroll_amount = config.scroll_amount * expected_amount_multiplier
90
- mock_pyautogui.scroll.assert_called_once_with(expected_scroll_amount)
91
-
92
-
93
- def test_wait_action(handler, mock_pyautogui, config):
94
- with patch("time.sleep") as mock_sleep:
95
- action = Action(type=ActionType.WAIT, argument="", count=1)
96
- handler([action])
97
- mock_sleep.assert_called_once_with(config.wait_duration)
98
-
99
-
100
- def test_finish_action(handler, mock_pyautogui):
101
- action = Action(type=ActionType.FINISH, argument="", count=1)
102
- handler([action])
103
-
104
-
105
- def test_call_user_action(handler, mock_pyautogui, capsys):
106
- action = Action(type=ActionType.CALL_USER, argument="", count=1)
107
- handler([action])
108
-
109
- captured = capsys.readouterr()
110
- assert "User intervention requested" in captured.out
111
-
112
-
113
- class TestActionExecution:
114
- def test_multiple_count(self, handler, mock_pyautogui):
115
- action = Action(type=ActionType.CLICK, argument="500, 300", count=3)
116
- handler([action])
117
-
118
- assert mock_pyautogui.click.call_count == 3
119
-
120
- def test_multiple_actions(self, handler, mock_pyautogui):
121
- actions = [
122
- Action(type=ActionType.CLICK, argument="100, 100", count=1),
123
- Action(type=ActionType.TYPE, argument="test", count=1),
124
- Action(type=ActionType.HOTKEY, argument="ctrl+s", count=1),
125
- ]
126
- handler(actions)
127
-
128
- mock_pyautogui.click.assert_called_once()
129
- mock_pyautogui.typewrite.assert_called_once_with("test")
130
- mock_pyautogui.hotkey.assert_called_once_with("ctrl", "s")
131
-
132
-
133
- class TestInputValidation:
134
- def test_invalid_coordinates_format(self, handler, mock_pyautogui):
135
- action = Action(type=ActionType.CLICK, argument="invalid", count=1)
136
-
137
- with pytest.raises(ValueError, match="Invalid coordinates format"):
138
- handler([action])
139
-
140
- def test_type_with_quotes(self, handler, mock_pyautogui):
141
- action = Action(type=ActionType.TYPE, argument='"Hello World"', count=1)
142
- handler([action])
143
-
144
- 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
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