oagi-core 0.14.2__tar.gz → 0.15.0__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.
- {oagi_core-0.14.2 → oagi_core-0.15.0}/PKG-INFO +1 -1
- {oagi_core-0.14.2 → oagi_core-0.15.0}/metapackage/pyproject.toml +2 -2
- {oagi_core-0.14.2 → oagi_core-0.15.0}/metapackage/uv.lock +5 -5
- {oagi_core-0.14.2 → oagi_core-0.15.0}/pyproject.toml +1 -1
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/converters/oagi.py +13 -9
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/handler/pyautogui_action_handler.py +2 -2
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/handler/ydotool_action_handler.py +2 -2
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/server/socketio_server.py +1 -1
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/types/models/action.py +1 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/utils/output_parser.py +2 -1
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/utils/prompt_builder.py +1 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/conftest.py +16 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_actor.py +21 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_oagi_action_converter.py +18 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_pyautogui_action_handler.py +19 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/utils/test_output_parser.py +25 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/uv.lock +1 -1
- {oagi_core-0.14.2 → oagi_core-0.15.0}/.github/ISSUE_TEMPLATE/bug-report.yml +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/.github/ISSUE_TEMPLATE/feature-request.yml +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/.github/ISSUE_TEMPLATE/question.yml +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/.github/workflows/ci.yml +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/.github/workflows/release.yml +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/.gitignore +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/.python-version +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/CONTRIBUTING.md +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/LICENSE +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/Makefile +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/README.md +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/examples/async_google_weather.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/examples/execute_task_auto.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/examples/execute_task_manual.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/examples/google_weather.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/examples/multi_screen_execution.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/examples/openai_agent_loop_example.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/examples/screenshot_with_config.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/examples/tasker_agent_example.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/__init__.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/actor/__init__.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/actor/async_.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/actor/async_short.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/actor/base.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/actor/short.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/actor/sync.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/__init__.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/default.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/factories.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/observer/__init__.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/observer/agent_observer.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/observer/events.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/observer/exporters.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/observer/protocol.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/observer/report_template.html +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/protocol.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/registry.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/tasker/__init__.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/tasker/memory.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/tasker/models.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/tasker/planner.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/tasker/taskee_agent.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/tasker/tasker_agent.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/cli/__init__.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/cli/agent.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/cli/display.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/cli/main.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/cli/server.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/cli/tracking.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/cli/utils.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/client/__init__.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/client/async_.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/client/base.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/client/sync.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/constants.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/converters/__init__.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/converters/base.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/exceptions.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/handler/__init__.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/handler/_macos.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/handler/_windows.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/handler/_ydotool.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/handler/async_pyautogui_action_handler.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/handler/async_screenshot_maker.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/handler/async_ydotool_action_handler.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/handler/capslock_manager.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/handler/pil_image.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/handler/screen_manager.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/handler/screenshot_maker.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/handler/utils.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/handler/wayland_support.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/logging.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/platform_info.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/server/__init__.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/server/agent_wrappers.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/server/config.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/server/main.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/server/models.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/server/session_store.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/task/__init__.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/types/__init__.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/types/action_handler.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/types/async_action_handler.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/types/async_image_provider.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/types/image.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/types/image_provider.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/types/models/__init__.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/types/models/client.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/types/models/image_config.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/types/models/step.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/types/step_observer.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/types/url.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/utils/__init__.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/__init__.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_action_parsing.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_agent/test_agent_wrappers.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_agent/test_default_agent.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_agent_registry.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_async_actor.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_async_client.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_async_handlers.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_cli.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_logging.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_mac_double_click.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_observer.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_pil_image.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_planner.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_planner_memory.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_screenshot_maker.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_server/__init__.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_server/test_config.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_server/test_session_store.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_server/test_socketio_integration.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_sync_client.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_taskee_agent.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_tasker_agent.py +0 -0
- {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/utils/__init__.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "oagi"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.15.0"
|
|
8
8
|
description = "Official API of OpenAGI Foundation (metapackage with all features)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -16,7 +16,7 @@ authors = [
|
|
|
16
16
|
requires-python = ">= 3.10"
|
|
17
17
|
|
|
18
18
|
dependencies = [
|
|
19
|
-
"oagi-core[desktop,server]==0.
|
|
19
|
+
"oagi-core[desktop,server]==0.15.0",
|
|
20
20
|
]
|
|
21
21
|
|
|
22
22
|
[project.urls]
|
|
@@ -527,18 +527,18 @@ wheels = [
|
|
|
527
527
|
|
|
528
528
|
[[package]]
|
|
529
529
|
name = "oagi"
|
|
530
|
-
version = "0.
|
|
530
|
+
version = "0.15.0"
|
|
531
531
|
source = { editable = "." }
|
|
532
532
|
dependencies = [
|
|
533
533
|
{ name = "oagi-core", extra = ["desktop", "server"] },
|
|
534
534
|
]
|
|
535
535
|
|
|
536
536
|
[package.metadata]
|
|
537
|
-
requires-dist = [{ name = "oagi-core", extras = ["desktop", "server"], specifier = "==0.14.
|
|
537
|
+
requires-dist = [{ name = "oagi-core", extras = ["desktop", "server"], specifier = "==0.14.2" }]
|
|
538
538
|
|
|
539
539
|
[[package]]
|
|
540
540
|
name = "oagi-core"
|
|
541
|
-
version = "0.14.
|
|
541
|
+
version = "0.14.2"
|
|
542
542
|
source = { registry = "https://pypi.org/simple" }
|
|
543
543
|
dependencies = [
|
|
544
544
|
{ name = "httpx" },
|
|
@@ -546,9 +546,9 @@ dependencies = [
|
|
|
546
546
|
{ name = "pydantic" },
|
|
547
547
|
{ name = "rich" },
|
|
548
548
|
]
|
|
549
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
549
|
+
sdist = { url = "https://files.pythonhosted.org/packages/7f/6e/02d64ba32075a3977e1037228580d29de7122e8d5dce2a1267c71d8c3d69/oagi_core-0.14.2.tar.gz", hash = "sha256:2eda6ef21276dc0c7256b6014f8db7045eaedf98a6f091f0aedf3f44b20321e6", size = 319639, upload-time = "2026-02-03T07:42:15.596Z" }
|
|
550
550
|
wheels = [
|
|
551
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
551
|
+
{ url = "https://files.pythonhosted.org/packages/0b/90/62215c6e46961d29d6e9e715d89483d108a811f5c1c854a5bb7b053eacab/oagi_core-0.14.2-py3-none-any.whl", hash = "sha256:ceaeabbe20bafef7c031f3932d6870b42e7406173030790a5a55965b8ebe5f73", size = 124410, upload-time = "2026-02-03T07:42:14.23Z" },
|
|
552
552
|
]
|
|
553
553
|
|
|
554
554
|
[package.optional-dependencies]
|
|
@@ -51,21 +51,21 @@ class OagiActionConverter(BaseActionConverter[Action]):
|
|
|
51
51
|
"""
|
|
52
52
|
converted: list[str] = []
|
|
53
53
|
failed: list[tuple[str, str]] = []
|
|
54
|
-
|
|
54
|
+
has_terminal = False
|
|
55
55
|
|
|
56
56
|
if not actions:
|
|
57
57
|
return converted
|
|
58
58
|
|
|
59
59
|
for action in actions:
|
|
60
|
-
# Check for duplicate finish() during iteration
|
|
61
|
-
|
|
62
|
-
if
|
|
63
|
-
if
|
|
60
|
+
# Check for duplicate finish()/fail() during iteration
|
|
61
|
+
is_terminal = action.type in (ActionType.FINISH, ActionType.FAIL)
|
|
62
|
+
if is_terminal:
|
|
63
|
+
if has_terminal:
|
|
64
64
|
raise ValueError(
|
|
65
|
-
"Duplicate finish() detected. "
|
|
66
|
-
"Only one finish() is allowed per action sequence."
|
|
65
|
+
"Duplicate finish()/fail() detected. "
|
|
66
|
+
"Only one finish() or fail() is allowed per action sequence."
|
|
67
67
|
)
|
|
68
|
-
|
|
68
|
+
has_terminal = True
|
|
69
69
|
|
|
70
70
|
try:
|
|
71
71
|
converted.extend(self._convert_action(action))
|
|
@@ -172,6 +172,10 @@ class OagiActionConverter(BaseActionConverter[Action]):
|
|
|
172
172
|
self._log_info("Task completion action -> DONE")
|
|
173
173
|
return ["DONE"]
|
|
174
174
|
|
|
175
|
+
if action_type == ActionType.FAIL.value:
|
|
176
|
+
self._log_info("Task infeasible action -> FAIL")
|
|
177
|
+
return ["FAIL"]
|
|
178
|
+
|
|
175
179
|
if action_type == ActionType.CALL_USER.value:
|
|
176
180
|
self._log_info("User intervention requested")
|
|
177
181
|
return []
|
|
@@ -179,7 +183,7 @@ class OagiActionConverter(BaseActionConverter[Action]):
|
|
|
179
183
|
raise ValueError(
|
|
180
184
|
f"Unknown action type: '{action_type}'. "
|
|
181
185
|
"Supported: click, left_double, left_triple, right_single, drag, "
|
|
182
|
-
"hotkey, type, scroll, wait, finish, call_user"
|
|
186
|
+
"hotkey, type, scroll, wait, finish, fail, call_user"
|
|
183
187
|
)
|
|
184
188
|
|
|
185
189
|
def serialize_actions(self, actions: list[Action]) -> list[dict[str, Any]]:
|
|
@@ -250,8 +250,8 @@ class PyautoguiActionHandler:
|
|
|
250
250
|
)
|
|
251
251
|
pyautogui.scroll(scroll_amount)
|
|
252
252
|
|
|
253
|
-
case ActionType.FINISH:
|
|
254
|
-
# Task completion - reset handler state
|
|
253
|
+
case ActionType.FINISH | ActionType.FAIL:
|
|
254
|
+
# Task completion or infeasible - reset handler state
|
|
255
255
|
self.reset()
|
|
256
256
|
|
|
257
257
|
case ActionType.WAIT:
|
|
@@ -164,8 +164,8 @@ class YdotoolActionHandler(Ydotool):
|
|
|
164
164
|
text = self.caps_manager.transform_text(text)
|
|
165
165
|
self._run_ydotool(["type", text], count=count)
|
|
166
166
|
|
|
167
|
-
case ActionType.FINISH:
|
|
168
|
-
# Task completion - reset handler state
|
|
167
|
+
case ActionType.FINISH | ActionType.FAIL:
|
|
168
|
+
# Task completion or infeasible - reset handler state
|
|
169
169
|
self.reset()
|
|
170
170
|
|
|
171
171
|
case ActionType.WAIT:
|
|
@@ -364,7 +364,7 @@ class SessionNamespace(socketio.AsyncNamespace):
|
|
|
364
364
|
timeout=self.config.socketio_timeout,
|
|
365
365
|
)
|
|
366
366
|
|
|
367
|
-
case ActionType.FINISH:
|
|
367
|
+
case ActionType.FINISH | ActionType.FAIL:
|
|
368
368
|
return await self.call(
|
|
369
369
|
"finish",
|
|
370
370
|
FinishEventData(**common).model_dump(),
|
|
@@ -45,7 +45,7 @@ def parse_raw_output(raw_output: str) -> Step:
|
|
|
45
45
|
parsed_action = _parse_action(action_text.strip())
|
|
46
46
|
if parsed_action:
|
|
47
47
|
actions.append(parsed_action)
|
|
48
|
-
if parsed_action.type
|
|
48
|
+
if parsed_action.type in (ActionType.FINISH, ActionType.FAIL):
|
|
49
49
|
stop = True
|
|
50
50
|
|
|
51
51
|
return Step(reason=reason, actions=actions, stop=stop)
|
|
@@ -105,6 +105,7 @@ def _parse_action(action_text: str) -> Action | None:
|
|
|
105
105
|
- scroll(x, y, direction, c) # scroll at position
|
|
106
106
|
- wait() # wait for a while
|
|
107
107
|
- finish() # indicate task is finished
|
|
108
|
+
- fail() # indicate task is infeasible
|
|
108
109
|
|
|
109
110
|
Args:
|
|
110
111
|
action_text: String representation of a single action
|
|
@@ -24,6 +24,7 @@ In the action field, you have the following action formats:
|
|
|
24
24
|
8. scroll(x, y, direction, c) # scroll the mouse at position (x, y) in the direction of up or down for c times, where x and y are integers normalized between 0 and 1000
|
|
25
25
|
9. wait() # wait for a while
|
|
26
26
|
10. finish() # indicate the task is finished
|
|
27
|
+
11. fail() # indicate the task is infeasible
|
|
27
28
|
|
|
28
29
|
Directly output the text beginning with <|think_start|>, no additional text is needed for this scenario.
|
|
29
30
|
|
|
@@ -132,6 +132,12 @@ def sample_raw_output_completed():
|
|
|
132
132
|
return "<|think_start|>The task has been completed successfully<|think_end|>\n<|action_start|>finish()<|action_end|>"
|
|
133
133
|
|
|
134
134
|
|
|
135
|
+
@pytest.fixture
|
|
136
|
+
def sample_raw_output_failed():
|
|
137
|
+
"""Sample raw output for infeasible task."""
|
|
138
|
+
return "<|think_start|>The task is infeasible<|think_end|>\n<|action_start|>fail()<|action_end|>"
|
|
139
|
+
|
|
140
|
+
|
|
135
141
|
@pytest.fixture
|
|
136
142
|
def sample_step(sample_action):
|
|
137
143
|
"""Sample Step object for testing."""
|
|
@@ -152,6 +158,16 @@ def completed_step():
|
|
|
152
158
|
)
|
|
153
159
|
|
|
154
160
|
|
|
161
|
+
@pytest.fixture
|
|
162
|
+
def failed_step():
|
|
163
|
+
"""Sample failed Step object for infeasible task."""
|
|
164
|
+
return Step(
|
|
165
|
+
reason="The task is infeasible",
|
|
166
|
+
actions=[Action(type=ActionType.FAIL, argument="", count=1)],
|
|
167
|
+
stop=True,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
155
171
|
@pytest.fixture
|
|
156
172
|
def mock_error_response():
|
|
157
173
|
"""Mock error HTTP response."""
|
|
@@ -219,6 +219,27 @@ class TestActorStep:
|
|
|
219
219
|
assert len(result.actions) == 1
|
|
220
220
|
assert result.actions[0].type == ActionType.FINISH
|
|
221
221
|
|
|
222
|
+
def test_step_with_failed_response(
|
|
223
|
+
self, actor, failed_step, sample_usage_obj, mock_upload_file_response
|
|
224
|
+
):
|
|
225
|
+
actor.task_description = "Test task"
|
|
226
|
+
actor.task_id = "task-789"
|
|
227
|
+
|
|
228
|
+
# Setup mocks
|
|
229
|
+
actor.client.put_s3_presigned_url.return_value = mock_upload_file_response
|
|
230
|
+
actor.client.chat_completion.return_value = (
|
|
231
|
+
failed_step,
|
|
232
|
+
"<|think_start|>infeasible<|think_end|>\n<|action_start|>fail()<|action_end|>",
|
|
233
|
+
sample_usage_obj,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
result = actor.step(b"image bytes")
|
|
237
|
+
|
|
238
|
+
assert result.stop is True
|
|
239
|
+
assert result.reason == "The task is infeasible"
|
|
240
|
+
assert len(result.actions) == 1
|
|
241
|
+
assert result.actions[0].type == ActionType.FAIL
|
|
242
|
+
|
|
222
243
|
def test_step_handles_exception(self, actor, mock_upload_file_response):
|
|
223
244
|
actor.task_description = "Test task"
|
|
224
245
|
actor.client.put_s3_presigned_url.return_value = mock_upload_file_response
|
|
@@ -97,6 +97,19 @@ class TestSpecialActions:
|
|
|
97
97
|
result = converter([action])
|
|
98
98
|
assert result[0] == "DONE"
|
|
99
99
|
|
|
100
|
+
def test_fail_action(self, converter):
|
|
101
|
+
action = Action(type=ActionType.FAIL, argument="", count=1)
|
|
102
|
+
result = converter([action])
|
|
103
|
+
assert result[0] == "FAIL"
|
|
104
|
+
|
|
105
|
+
def test_duplicate_terminal_actions_raises(self, converter):
|
|
106
|
+
actions = [
|
|
107
|
+
Action(type=ActionType.FINISH, argument="", count=1),
|
|
108
|
+
Action(type=ActionType.FAIL, argument="", count=1),
|
|
109
|
+
]
|
|
110
|
+
with pytest.raises(ValueError, match="Duplicate finish\\(\\)/fail\\(\\)"):
|
|
111
|
+
converter(actions)
|
|
112
|
+
|
|
100
113
|
|
|
101
114
|
class TestActionStringToStep:
|
|
102
115
|
def test_pyautogui_command(self, converter):
|
|
@@ -114,6 +127,11 @@ class TestActionStringToStep:
|
|
|
114
127
|
assert step["type"] == "sleep"
|
|
115
128
|
assert step["parameters"]["seconds"] == 0
|
|
116
129
|
|
|
130
|
+
def test_fail_command(self, converter):
|
|
131
|
+
step = converter.action_string_to_step("FAIL")
|
|
132
|
+
assert step["type"] == "sleep"
|
|
133
|
+
assert step["parameters"]["seconds"] == 0
|
|
134
|
+
|
|
117
135
|
|
|
118
136
|
class TestMultipleActions:
|
|
119
137
|
def test_action_count(self, converter):
|
|
@@ -131,6 +131,11 @@ def test_finish_action(handler, mock_pyautogui):
|
|
|
131
131
|
handler([action])
|
|
132
132
|
|
|
133
133
|
|
|
134
|
+
def test_fail_action(handler, mock_pyautogui):
|
|
135
|
+
action = Action(type=ActionType.FAIL, argument="", count=1)
|
|
136
|
+
handler([action])
|
|
137
|
+
|
|
138
|
+
|
|
134
139
|
def test_call_user_action(handler, mock_pyautogui, capsys):
|
|
135
140
|
action = Action(type=ActionType.CALL_USER, argument="", count=1)
|
|
136
141
|
handler([action])
|
|
@@ -453,6 +458,20 @@ class TestHandlerReset:
|
|
|
453
458
|
handler([finish_action])
|
|
454
459
|
assert handler.caps_manager.caps_enabled is False
|
|
455
460
|
|
|
461
|
+
def test_fail_action_resets_handler(self, mock_pyautogui):
|
|
462
|
+
config = PyautoguiConfig(capslock_mode="session", post_batch_delay=0)
|
|
463
|
+
handler = PyautoguiActionHandler(config=config)
|
|
464
|
+
|
|
465
|
+
# Enable caps lock
|
|
466
|
+
caps_action = Action(type=ActionType.HOTKEY, argument="capslock", count=1)
|
|
467
|
+
handler([caps_action])
|
|
468
|
+
assert handler.caps_manager.caps_enabled is True
|
|
469
|
+
|
|
470
|
+
# FAIL action should also reset handler
|
|
471
|
+
fail_action = Action(type=ActionType.FAIL, argument="", count=1)
|
|
472
|
+
handler([fail_action])
|
|
473
|
+
assert handler.caps_manager.caps_enabled is False
|
|
474
|
+
|
|
456
475
|
|
|
457
476
|
class TestAsyncHandlerReset:
|
|
458
477
|
def test_async_handler_reset_delegates_to_sync_handler(self, mock_pyautogui):
|
|
@@ -34,6 +34,15 @@ class TestParseRawOutput:
|
|
|
34
34
|
assert step.actions[0].type == ActionType.FINISH
|
|
35
35
|
assert step.stop is True
|
|
36
36
|
|
|
37
|
+
def test_parse_fail_action_sets_stop(self):
|
|
38
|
+
raw = "<|think_start|>Task is infeasible<|think_end|>\n<|action_start|>fail()<|action_end|>"
|
|
39
|
+
step = parse_raw_output(raw)
|
|
40
|
+
|
|
41
|
+
assert step.reason == "Task is infeasible"
|
|
42
|
+
assert len(step.actions) == 1
|
|
43
|
+
assert step.actions[0].type == ActionType.FAIL
|
|
44
|
+
assert step.stop is True
|
|
45
|
+
|
|
37
46
|
def test_parse_multiple_actions_with_ampersand(self):
|
|
38
47
|
raw = "<|think_start|>Do two things<|think_end|>\n<|action_start|>click(100, 200) & type(hello)<|action_end|>"
|
|
39
48
|
step = parse_raw_output(raw)
|
|
@@ -147,6 +156,7 @@ class TestParseAction:
|
|
|
147
156
|
("type(hello world)", ActionType.TYPE, "hello world"),
|
|
148
157
|
("wait()", ActionType.WAIT, ""),
|
|
149
158
|
("finish()", ActionType.FINISH, ""),
|
|
159
|
+
("fail()", ActionType.FAIL, ""),
|
|
150
160
|
],
|
|
151
161
|
)
|
|
152
162
|
def test_parse_basic_actions(self, action_text, expected_type, expected_arg):
|
|
@@ -254,3 +264,18 @@ class TestEdgeCases:
|
|
|
254
264
|
|
|
255
265
|
assert step.stop is True
|
|
256
266
|
assert len(step.actions) == 2
|
|
267
|
+
|
|
268
|
+
def test_fail_with_other_actions(self):
|
|
269
|
+
raw = "<|think_start|>Test<|think_end|>\n<|action_start|>click(100, 200) & fail()<|action_end|>"
|
|
270
|
+
step = parse_raw_output(raw)
|
|
271
|
+
|
|
272
|
+
assert step.stop is True
|
|
273
|
+
assert len(step.actions) == 2
|
|
274
|
+
assert step.actions[1].type == ActionType.FAIL
|
|
275
|
+
|
|
276
|
+
def test_multiple_fail_actions_stop_true(self):
|
|
277
|
+
raw = "<|think_start|>Test<|think_end|>\n<|action_start|>fail() & fail()<|action_end|>"
|
|
278
|
+
step = parse_raw_output(raw)
|
|
279
|
+
|
|
280
|
+
assert step.stop is True
|
|
281
|
+
assert len(step.actions) == 2
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|