Skip to content

Commit 4a96485

Browse files
Merge pull request #105 from askui/feat/set-controller-path-directly
feat: set askui controller path directly
2 parents e894b95 + 8a5b0c3 commit 4a96485

File tree

7 files changed

+559
-143
lines changed

7 files changed

+559
-143
lines changed

src/askui/models/shared/tools.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from abc import ABC, abstractmethod
2-
from typing import Any, cast
2+
from typing import Any
33

44
from anthropic.types.beta import BetaToolParam, BetaToolUnionParam
55
from anthropic.types.beta.beta_tool_param import InputSchema
@@ -147,10 +147,7 @@ def _run_tool(
147147
tool_use_id=tool_use_block_param.id,
148148
)
149149
try:
150-
tool_result: ToolCallResult = cast(
151-
"ToolCallResult",
152-
tool(**tool_use_block_param.input), # type: ignore
153-
)
150+
tool_result: ToolCallResult = tool(**tool_use_block_param.input) # type: ignore
154151
return ToolResultBlockParam(
155152
content=_convert_to_content(tool_result),
156153
tool_use_id=tool_use_block_param.id,

src/askui/tools/askui/askui_controller.py

Lines changed: 9 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,13 @@
88

99
import grpc
1010
from PIL import Image
11-
from pydantic import BaseModel, Field, model_validator
12-
from pydantic_settings import BaseSettings, SettingsConfigDict
1311
from typing_extensions import Self, override
1412

1513
from askui.container import telemetry
1614
from askui.logger import logger
1715
from askui.reporting import Reporter
1816
from askui.tools.agent_os import AgentOs, Coordinate, ModifierKey, PcKey
17+
from askui.tools.askui.askui_controller_settings import AskUiControllerSettings
1918
from askui.tools.askui.askui_ui_controller_grpc.generated import (
2019
Controller_V1_pb2 as controller_v1_pbs,
2120
)
@@ -48,137 +47,19 @@
4847
)
4948

5049

51-
class RemoteDeviceController(BaseModel):
52-
askui_remote_device_controller: pathlib.Path = Field(
53-
alias="AskUIRemoteDeviceController"
54-
)
55-
56-
57-
class Executables(BaseModel):
58-
executables: RemoteDeviceController = Field(alias="Executables")
59-
60-
61-
class InstalledPackages(BaseModel):
62-
remote_device_controller_uuid: Executables = Field(
63-
alias="{aed1b543-e856-43ad-b1bc-19365d35c33e}"
64-
)
65-
66-
67-
class AskUiComponentRegistry(BaseModel):
68-
definition_version: int = Field(alias="DefinitionVersion")
69-
installed_packages: InstalledPackages = Field(alias="InstalledPackages")
70-
71-
72-
class AskUiControllerSettings(BaseSettings):
73-
model_config = SettingsConfigDict(
74-
env_prefix="ASKUI_",
75-
)
76-
77-
component_registry_file: pathlib.Path | None = None
78-
installation_directory: pathlib.Path | None = None
79-
80-
@model_validator(mode="after")
81-
def validate_either_component_registry_or_installation_directory_is_set(
82-
self,
83-
) -> "AskUiControllerSettings":
84-
if self.component_registry_file is None and self.installation_directory is None:
85-
error_msg = (
86-
"Either ASKUI_COMPONENT_REGISTRY_FILE or "
87-
"ASKUI_INSTALLATION_DIRECTORY environment variable must be set"
88-
)
89-
raise ValueError(error_msg)
90-
return self
91-
92-
9350
class AskUiControllerServer:
9451
"""
9552
Concrete implementation of `ControllerServer` for managing the AskUI Remote Device
9653
Controller process.
9754
Handles process discovery, startup, and shutdown for the native controller binary.
55+
56+
Args:
57+
settings (AskUiControllerSettings | None, optional): Settings for the AskUI.
9858
"""
9959

100-
def __init__(self) -> None:
60+
def __init__(self, settings: AskUiControllerSettings | None = None) -> None:
10161
self._process: subprocess.Popen[bytes] | None = None
102-
self._settings = AskUiControllerSettings()
103-
104-
def _find_remote_device_controller(self) -> pathlib.Path:
105-
if (
106-
self._settings.installation_directory is not None
107-
and self._settings.component_registry_file is None
108-
):
109-
logger.warning(
110-
"Outdated AskUI Suite detected. Please update to the latest version."
111-
)
112-
askui_remote_device_controller_path = (
113-
self._find_remote_device_controller_by_legacy_path()
114-
)
115-
if not askui_remote_device_controller_path.is_file():
116-
error_msg = (
117-
"AskUIRemoteDeviceController executable does not exist under "
118-
f"'{askui_remote_device_controller_path}'"
119-
)
120-
raise FileNotFoundError(error_msg)
121-
return askui_remote_device_controller_path
122-
return self._find_remote_device_controller_by_component_registry()
123-
124-
def _find_remote_device_controller_by_component_registry(self) -> pathlib.Path:
125-
assert self._settings.component_registry_file is not None, (
126-
"Component registry file is not set"
127-
)
128-
component_registry = AskUiComponentRegistry.model_validate_json(
129-
self._settings.component_registry_file.read_text()
130-
)
131-
askui_remote_device_controller_path = (
132-
component_registry.installed_packages.remote_device_controller_uuid.executables.askui_remote_device_controller # noqa: E501
133-
)
134-
if not askui_remote_device_controller_path.is_file():
135-
error_msg = (
136-
"AskUIRemoteDeviceController executable does not exist under "
137-
f"'{askui_remote_device_controller_path}'"
138-
)
139-
raise FileNotFoundError(error_msg)
140-
return askui_remote_device_controller_path
141-
142-
def _find_remote_device_controller_by_legacy_path(self) -> pathlib.Path:
143-
assert self._settings.installation_directory is not None, (
144-
"Installation directory is not set"
145-
)
146-
match sys.platform:
147-
case "win32":
148-
return (
149-
self._settings.installation_directory
150-
/ "Binaries"
151-
/ "resources"
152-
/ "assets"
153-
/ "binaries"
154-
/ "AskuiRemoteDeviceController.exe"
155-
)
156-
case "darwin":
157-
return (
158-
self._settings.installation_directory
159-
/ "Binaries"
160-
/ "askui-ui-controller.app"
161-
/ "Contents"
162-
/ "Resources"
163-
/ "assets"
164-
/ "binaries"
165-
/ "AskuiRemoteDeviceController"
166-
)
167-
case "linux":
168-
return (
169-
self._settings.installation_directory
170-
/ "Binaries"
171-
/ "resources"
172-
/ "assets"
173-
/ "binaries"
174-
/ "AskuiRemoteDeviceController"
175-
)
176-
case _:
177-
error_msg = (
178-
f"Platform {sys.platform} not supported by "
179-
"AskUI Remote Device Controller"
180-
)
181-
raise NotImplementedError(error_msg)
62+
self._settings = settings or AskUiControllerSettings()
18263

18364
def _start_process(self, path: pathlib.Path) -> None:
18465
self._process = subprocess.Popen(path)
@@ -198,11 +79,11 @@ def start(self, clean_up: bool = False) -> None:
19879
and process_exists("AskuiRemoteDeviceController.exe")
19980
):
20081
self.clean_up()
201-
remote_device_controller_path = self._find_remote_device_controller()
20282
logger.debug(
203-
"Starting AskUI Remote Device Controller: %s", remote_device_controller_path
83+
"Starting AskUI Remote Device Controller: %s",
84+
self._settings.controller_path,
20485
)
205-
self._start_process(remote_device_controller_path)
86+
self._start_process(self._settings.controller_path)
20687
time.sleep(0.5)
20788

20889
def clean_up(self) -> None:
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import pathlib
2+
import sys
3+
from functools import cached_property
4+
5+
from pydantic import BaseModel, Field, model_validator
6+
from pydantic_settings import BaseSettings, SettingsConfigDict
7+
from typing_extensions import Self
8+
9+
10+
class RemoteDeviceController(BaseModel):
11+
askui_remote_device_controller: pathlib.Path = Field(
12+
alias="AskUIRemoteDeviceController"
13+
)
14+
15+
16+
class Executables(BaseModel):
17+
executables: RemoteDeviceController = Field(alias="Executables")
18+
19+
20+
class InstalledPackages(BaseModel):
21+
remote_device_controller_uuid: Executables = Field(
22+
alias="{aed1b543-e856-43ad-b1bc-19365d35c33e}"
23+
)
24+
25+
26+
class AskUiComponentRegistry(BaseModel):
27+
definition_version: int = Field(alias="DefinitionVersion")
28+
installed_packages: InstalledPackages = Field(alias="InstalledPackages")
29+
30+
31+
class AskUiControllerSettings(BaseSettings):
32+
model_config = SettingsConfigDict(
33+
env_prefix="ASKUI_",
34+
)
35+
36+
component_registry_file: pathlib.Path | None = None
37+
installation_directory: pathlib.Path | None = Field(
38+
None,
39+
deprecated="ASKUI_INSTALLATION_DIRECTORY has been deprecated in favor of "
40+
"ASKUI_COMPONENT_REGISTRY_FILE and ASKUI_CONTROLLER_PATH. You may be using an "
41+
"outdated AskUI Suite. If you think so, reinstall to upgrade the AskUI Suite "
42+
"(see https://docs.askui.com/01-tutorials/00-installation).",
43+
)
44+
controller_path_setting: pathlib.Path | None = Field(
45+
None,
46+
validation_alias="ASKUI_CONTROLLER_PATH",
47+
description="Path to the AskUI Remote Device Controller executable. Takes "
48+
"precedence over ASKUI_COMPONENT_REGISTRY_FILE and ASKUI_INSTALLATION_DIRECTORY"
49+
".",
50+
)
51+
52+
@model_validator(mode="after")
53+
def validate_either_component_registry_or_installation_directory_is_set(
54+
self,
55+
) -> "Self":
56+
if (
57+
self.component_registry_file is None
58+
and self.installation_directory is None
59+
and self.controller_path_setting is None
60+
):
61+
error_msg = (
62+
"Either ASKUI_COMPONENT_REGISTRY_FILE, ASKUI_INSTALLATION_DIRECTORY, "
63+
"or ASKUI_CONTROLLER_PATH environment variable must be set"
64+
)
65+
raise ValueError(error_msg)
66+
return self
67+
68+
def _find_remote_device_controller_by_installation_directory(
69+
self,
70+
) -> pathlib.Path | None:
71+
if self.installation_directory is None:
72+
return None
73+
74+
return self._build_controller_path(self.installation_directory)
75+
76+
def _build_controller_path(
77+
self, installation_directory: pathlib.Path
78+
) -> pathlib.Path:
79+
match sys.platform:
80+
case "win32":
81+
return (
82+
installation_directory
83+
/ "Binaries"
84+
/ "resources"
85+
/ "assets"
86+
/ "binaries"
87+
/ "AskuiRemoteDeviceController.exe"
88+
)
89+
case "darwin":
90+
return (
91+
installation_directory
92+
/ "Binaries"
93+
/ "askui-ui-controller.app"
94+
/ "Contents"
95+
/ "Resources"
96+
/ "assets"
97+
/ "binaries"
98+
/ "AskuiRemoteDeviceController"
99+
)
100+
case "linux":
101+
return (
102+
installation_directory
103+
/ "Binaries"
104+
/ "resources"
105+
/ "assets"
106+
/ "binaries"
107+
/ "AskuiRemoteDeviceController"
108+
)
109+
case _:
110+
error_msg = (
111+
f'Platform "{sys.platform}" not supported by '
112+
"AskUI Remote Device Controller"
113+
)
114+
raise NotImplementedError(error_msg)
115+
116+
def _find_remote_device_controller_by_component_registry_file(
117+
self,
118+
) -> pathlib.Path | None:
119+
if self.component_registry_file is None:
120+
return None
121+
122+
component_registry = AskUiComponentRegistry.model_validate_json(
123+
self.component_registry_file.read_text()
124+
)
125+
return (
126+
component_registry.installed_packages.remote_device_controller_uuid.executables.askui_remote_device_controller # noqa: E501
127+
)
128+
129+
@cached_property
130+
def controller_path(self) -> pathlib.Path:
131+
result = (
132+
self.controller_path_setting
133+
or self._find_remote_device_controller_by_component_registry_file()
134+
or self._find_remote_device_controller_by_installation_directory()
135+
)
136+
assert result is not None, (
137+
"No AskUI Remote Device Controller found. Please set the "
138+
"ASKUI_COMPONENT_REGISTRY_FILE, ASKUI_INSTALLATION_DIRECTORY, or "
139+
"ASKUI_CONTROLLER_PATH environment variable."
140+
)
141+
if not result.is_file():
142+
error_msg = (
143+
"AskUIRemoteDeviceController executable does not exist under "
144+
f"`{result}`"
145+
)
146+
raise FileNotFoundError(error_msg)
147+
return result
148+
149+
150+
__all__ = ["AskUiControllerSettings"]

tests/e2e/tools/askui/test_askui_controller.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import base64
22
import io
3-
from pathlib import Path
43
from typing import Literal
54

65
import pytest
@@ -31,15 +30,6 @@ def controller_client(
3130
)
3231

3332

34-
def test_find_remote_device_controller_by_component_registry(
35-
controller_server: AskUiControllerServer,
36-
) -> None:
37-
remote_device_controller_path = Path(
38-
controller_server._find_remote_device_controller_by_component_registry()
39-
)
40-
assert "AskuiRemoteDeviceController" == remote_device_controller_path.stem
41-
42-
4333
def test_actions(controller_client: AskUiControllerClient) -> None:
4434
with controller_client:
4535
controller_client.screenshot()

tests/unit/tools/__init__.py

Whitespace-only changes.

tests/unit/tools/askui/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)