autoglm-gui 1.4.0__py3-none-any.whl → 1.4.1__py3-none-any.whl
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.
- AutoGLM_GUI/__main__.py +0 -4
- AutoGLM_GUI/adb_plus/qr_pair.py +8 -8
- AutoGLM_GUI/agents/__init__.py +20 -0
- AutoGLM_GUI/agents/factory.py +160 -0
- AutoGLM_GUI/agents/mai_adapter.py +627 -0
- AutoGLM_GUI/agents/protocols.py +23 -0
- AutoGLM_GUI/api/__init__.py +48 -7
- AutoGLM_GUI/api/agents.py +61 -17
- AutoGLM_GUI/api/devices.py +12 -18
- AutoGLM_GUI/api/dual_model.py +15 -9
- AutoGLM_GUI/api/health.py +13 -0
- AutoGLM_GUI/api/layered_agent.py +239 -166
- AutoGLM_GUI/api/mcp.py +11 -10
- AutoGLM_GUI/api/version.py +23 -10
- AutoGLM_GUI/api/workflows.py +2 -1
- AutoGLM_GUI/config_manager.py +55 -1
- AutoGLM_GUI/device_adapter.py +263 -0
- AutoGLM_GUI/device_protocol.py +266 -0
- AutoGLM_GUI/devices/__init__.py +49 -0
- AutoGLM_GUI/devices/adb_device.py +205 -0
- AutoGLM_GUI/devices/mock_device.py +183 -0
- AutoGLM_GUI/devices/remote_device.py +172 -0
- AutoGLM_GUI/dual_model/decision_model.py +4 -4
- AutoGLM_GUI/exceptions.py +3 -3
- AutoGLM_GUI/mai_ui_adapter/agent_wrapper.py +2 -2
- AutoGLM_GUI/metrics.py +13 -20
- AutoGLM_GUI/phone_agent_manager.py +219 -134
- AutoGLM_GUI/phone_agent_patches.py +2 -1
- AutoGLM_GUI/platform_utils.py +5 -2
- AutoGLM_GUI/schemas.py +47 -0
- AutoGLM_GUI/scrcpy_stream.py +17 -13
- AutoGLM_GUI/server.py +3 -1
- AutoGLM_GUI/socketio_server.py +16 -4
- AutoGLM_GUI/state.py +10 -30
- AutoGLM_GUI/static/assets/{about-DeclntHg.js → about-_XNhzQZX.js} +1 -1
- AutoGLM_GUI/static/assets/chat-DwJpiAWf.js +126 -0
- AutoGLM_GUI/static/assets/{dialog-BfdcBs1x.js → dialog-B3uW4T8V.js} +3 -3
- AutoGLM_GUI/static/assets/index-Cpv2gSF1.css +1 -0
- AutoGLM_GUI/static/assets/{index-zQ4KKDHt.js → index-Cy8TmmHV.js} +1 -1
- AutoGLM_GUI/static/assets/{index-DHF1NZh0.js → index-UYYauTly.js} +6 -6
- AutoGLM_GUI/static/assets/{workflows-xiplap-r.js → workflows-Du_de-dt.js} +1 -1
- AutoGLM_GUI/static/index.html +2 -2
- AutoGLM_GUI/types.py +125 -0
- {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.4.1.dist-info}/METADATA +83 -4
- {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.4.1.dist-info}/RECORD +54 -37
- mai_agent/base.py +137 -0
- mai_agent/mai_grounding_agent.py +263 -0
- mai_agent/mai_naivigation_agent.py +526 -0
- mai_agent/prompt.py +148 -0
- mai_agent/unified_memory.py +67 -0
- mai_agent/utils.py +73 -0
- AutoGLM_GUI/config.py +0 -23
- AutoGLM_GUI/static/assets/chat-Iut2yhSw.js +0 -125
- AutoGLM_GUI/static/assets/index-5hCCwHA7.css +0 -1
- {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.4.1.dist-info}/WHEEL +0 -0
- {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.4.1.dist-info}/entry_points.txt +0 -0
- {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.4.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Device implementations for the DeviceProtocol interface.
|
|
2
|
+
|
|
3
|
+
This package provides concrete implementations of DeviceProtocol:
|
|
4
|
+
- ADBDevice: Local ADB subprocess calls
|
|
5
|
+
- MockDevice: State machine driven mock for testing
|
|
6
|
+
- RemoteDevice: HTTP client for remote device agents
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
>>> from AutoGLM_GUI.devices import ADBDevice, RemoteDevice, get_device_manager
|
|
10
|
+
>>>
|
|
11
|
+
>>> # Local ADB device
|
|
12
|
+
>>> device = ADBDevice("emulator-5554")
|
|
13
|
+
>>> device.tap(100, 200)
|
|
14
|
+
>>>
|
|
15
|
+
>>> # Remote device via HTTP
|
|
16
|
+
>>> remote = RemoteDevice("phone_001", "http://device-agent:8001")
|
|
17
|
+
>>> remote.tap(100, 200)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from AutoGLM_GUI.devices.adb_device import ADBDevice, ADBDeviceManager
|
|
21
|
+
from AutoGLM_GUI.devices.mock_device import MockDevice
|
|
22
|
+
from AutoGLM_GUI.devices.remote_device import RemoteDevice, RemoteDeviceManager
|
|
23
|
+
|
|
24
|
+
_device_manager: "ADBDeviceManager | None" = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_device_manager() -> ADBDeviceManager:
|
|
28
|
+
"""Get the global device manager instance."""
|
|
29
|
+
global _device_manager
|
|
30
|
+
if _device_manager is None:
|
|
31
|
+
_device_manager = ADBDeviceManager()
|
|
32
|
+
return _device_manager
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def set_device_manager(manager: "ADBDeviceManager") -> None:
|
|
36
|
+
"""Set the global device manager instance (useful for testing)."""
|
|
37
|
+
global _device_manager
|
|
38
|
+
_device_manager = manager
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"ADBDevice",
|
|
43
|
+
"ADBDeviceManager",
|
|
44
|
+
"MockDevice",
|
|
45
|
+
"RemoteDevice",
|
|
46
|
+
"RemoteDeviceManager",
|
|
47
|
+
"get_device_manager",
|
|
48
|
+
"set_device_manager",
|
|
49
|
+
]
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""ADB Device implementation of DeviceProtocol.
|
|
2
|
+
|
|
3
|
+
This module wraps the existing phone_agent.adb module to provide
|
|
4
|
+
a DeviceProtocol-compliant implementation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from phone_agent import adb
|
|
8
|
+
from phone_agent.adb import ADBConnection
|
|
9
|
+
|
|
10
|
+
from AutoGLM_GUI.device_protocol import (
|
|
11
|
+
DeviceInfo,
|
|
12
|
+
DeviceManagerProtocol,
|
|
13
|
+
DeviceProtocol,
|
|
14
|
+
Screenshot,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ADBDevice:
|
|
19
|
+
"""
|
|
20
|
+
ADB device implementation using local subprocess calls.
|
|
21
|
+
|
|
22
|
+
Wraps the existing phone_agent.adb module to provide a clean
|
|
23
|
+
DeviceProtocol interface.
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
>>> device = ADBDevice("emulator-5554")
|
|
27
|
+
>>> screenshot = device.get_screenshot()
|
|
28
|
+
>>> device.tap(100, 200)
|
|
29
|
+
>>> device.swipe(100, 200, 300, 400)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, device_id: str):
|
|
33
|
+
"""
|
|
34
|
+
Initialize ADB device.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
device_id: ADB device ID (e.g., "emulator-5554", "192.168.1.100:5555").
|
|
38
|
+
"""
|
|
39
|
+
self._device_id = device_id
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def device_id(self) -> str:
|
|
43
|
+
"""Unique device identifier."""
|
|
44
|
+
return self._device_id
|
|
45
|
+
|
|
46
|
+
# === Screenshot ===
|
|
47
|
+
def get_screenshot(self, timeout: int = 10) -> Screenshot:
|
|
48
|
+
"""Capture current screen."""
|
|
49
|
+
result = adb.get_screenshot(self._device_id, timeout)
|
|
50
|
+
return Screenshot(
|
|
51
|
+
base64_data=result.base64_data,
|
|
52
|
+
width=result.width,
|
|
53
|
+
height=result.height,
|
|
54
|
+
is_sensitive=result.is_sensitive,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# === Input Operations ===
|
|
58
|
+
def tap(self, x: int, y: int, delay: float | None = None) -> None:
|
|
59
|
+
"""Tap at specified coordinates."""
|
|
60
|
+
adb.tap(x, y, self._device_id, delay)
|
|
61
|
+
|
|
62
|
+
def double_tap(self, x: int, y: int, delay: float | None = None) -> None:
|
|
63
|
+
"""Double tap at specified coordinates."""
|
|
64
|
+
adb.double_tap(x, y, self._device_id, delay)
|
|
65
|
+
|
|
66
|
+
def long_press(
|
|
67
|
+
self, x: int, y: int, duration_ms: int = 3000, delay: float | None = None
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Long press at specified coordinates."""
|
|
70
|
+
adb.long_press(x, y, duration_ms, self._device_id, delay)
|
|
71
|
+
|
|
72
|
+
def swipe(
|
|
73
|
+
self,
|
|
74
|
+
start_x: int,
|
|
75
|
+
start_y: int,
|
|
76
|
+
end_x: int,
|
|
77
|
+
end_y: int,
|
|
78
|
+
duration_ms: int | None = None,
|
|
79
|
+
delay: float | None = None,
|
|
80
|
+
) -> None:
|
|
81
|
+
"""Swipe from start to end coordinates."""
|
|
82
|
+
adb.swipe(start_x, start_y, end_x, end_y, duration_ms, self._device_id, delay)
|
|
83
|
+
|
|
84
|
+
def type_text(self, text: str) -> None:
|
|
85
|
+
"""Type text into the currently focused input field."""
|
|
86
|
+
adb.type_text(text, self._device_id)
|
|
87
|
+
|
|
88
|
+
def clear_text(self) -> None:
|
|
89
|
+
"""Clear text in the currently focused input field."""
|
|
90
|
+
adb.clear_text(self._device_id)
|
|
91
|
+
|
|
92
|
+
# === Navigation ===
|
|
93
|
+
def back(self, delay: float | None = None) -> None:
|
|
94
|
+
"""Press the back button."""
|
|
95
|
+
adb.back(self._device_id, delay)
|
|
96
|
+
|
|
97
|
+
def home(self, delay: float | None = None) -> None:
|
|
98
|
+
"""Press the home button."""
|
|
99
|
+
adb.home(self._device_id, delay)
|
|
100
|
+
|
|
101
|
+
def launch_app(self, app_name: str, delay: float | None = None) -> bool:
|
|
102
|
+
"""Launch an app by name."""
|
|
103
|
+
return adb.launch_app(app_name, self._device_id, delay)
|
|
104
|
+
|
|
105
|
+
# === State Query ===
|
|
106
|
+
def get_current_app(self) -> str:
|
|
107
|
+
"""Get the currently focused app name."""
|
|
108
|
+
return adb.get_current_app(self._device_id)
|
|
109
|
+
|
|
110
|
+
# === Keyboard Management ===
|
|
111
|
+
def detect_and_set_adb_keyboard(self) -> str:
|
|
112
|
+
"""Detect current keyboard and switch to ADB Keyboard if needed."""
|
|
113
|
+
return adb.detect_and_set_adb_keyboard(self._device_id)
|
|
114
|
+
|
|
115
|
+
def restore_keyboard(self, ime: str) -> None:
|
|
116
|
+
"""Restore the original keyboard IME."""
|
|
117
|
+
adb.restore_keyboard(ime, self._device_id)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# Verify ADBDevice implements DeviceProtocol
|
|
121
|
+
assert isinstance(ADBDevice("test"), DeviceProtocol)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ADBDeviceManager:
|
|
125
|
+
"""
|
|
126
|
+
ADB device manager implementation.
|
|
127
|
+
|
|
128
|
+
Manages multiple ADB devices and provides DeviceProtocol instances.
|
|
129
|
+
|
|
130
|
+
Example:
|
|
131
|
+
>>> manager = ADBDeviceManager()
|
|
132
|
+
>>> devices = manager.list_devices()
|
|
133
|
+
>>> device = manager.get_device("emulator-5554")
|
|
134
|
+
>>> device.tap(100, 200)
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
def __init__(self, adb_path: str = "adb"):
|
|
138
|
+
"""
|
|
139
|
+
Initialize the device manager.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
adb_path: Path to ADB executable.
|
|
143
|
+
"""
|
|
144
|
+
self._connection = ADBConnection(adb_path)
|
|
145
|
+
self._devices: dict[str, ADBDevice] = {}
|
|
146
|
+
|
|
147
|
+
def list_devices(self) -> list[DeviceInfo]:
|
|
148
|
+
"""List all available devices."""
|
|
149
|
+
adb_devices = self._connection.list_devices()
|
|
150
|
+
result = []
|
|
151
|
+
|
|
152
|
+
for dev in adb_devices:
|
|
153
|
+
result.append(
|
|
154
|
+
DeviceInfo(
|
|
155
|
+
device_id=dev.device_id,
|
|
156
|
+
status="online" if dev.status == "device" else dev.status,
|
|
157
|
+
model=dev.model,
|
|
158
|
+
platform="android",
|
|
159
|
+
connection_type=dev.connection_type.value,
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
return result
|
|
164
|
+
|
|
165
|
+
def get_device(self, device_id: str) -> ADBDevice:
|
|
166
|
+
"""
|
|
167
|
+
Get a device instance by ID.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
device_id: The device ID.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
ADBDevice instance.
|
|
174
|
+
|
|
175
|
+
Raises:
|
|
176
|
+
KeyError: If device not found or offline.
|
|
177
|
+
"""
|
|
178
|
+
# Check if device exists and is online
|
|
179
|
+
devices = self.list_devices()
|
|
180
|
+
device_info = next((d for d in devices if d.device_id == device_id), None)
|
|
181
|
+
|
|
182
|
+
if device_info is None:
|
|
183
|
+
raise KeyError(f"Device '{device_id}' not found")
|
|
184
|
+
if device_info.status != "online":
|
|
185
|
+
raise KeyError(f"Device '{device_id}' is {device_info.status}")
|
|
186
|
+
|
|
187
|
+
# Cache and return device instance
|
|
188
|
+
if device_id not in self._devices:
|
|
189
|
+
self._devices[device_id] = ADBDevice(device_id)
|
|
190
|
+
|
|
191
|
+
return self._devices[device_id]
|
|
192
|
+
|
|
193
|
+
def connect(self, address: str, timeout: int = 10) -> tuple[bool, str]:
|
|
194
|
+
"""Connect to a remote device."""
|
|
195
|
+
return self._connection.connect(address, timeout)
|
|
196
|
+
|
|
197
|
+
def disconnect(self, device_id: str) -> tuple[bool, str]:
|
|
198
|
+
"""Disconnect from a device."""
|
|
199
|
+
# Remove from cache
|
|
200
|
+
self._devices.pop(device_id, None)
|
|
201
|
+
return self._connection.disconnect(device_id)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# Verify ADBDeviceManager implements DeviceManagerProtocol
|
|
205
|
+
assert isinstance(ADBDeviceManager(), DeviceManagerProtocol)
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Mock Device implementation for testing.
|
|
2
|
+
|
|
3
|
+
This module provides a MockDevice that routes all operations through
|
|
4
|
+
a state machine, enabling controlled testing without real devices.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from AutoGLM_GUI.device_protocol import (
|
|
10
|
+
DeviceInfo,
|
|
11
|
+
Screenshot,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from tests.integration.state_machine import StateMachine
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MockDevice:
|
|
19
|
+
"""
|
|
20
|
+
Mock device implementation driven by a state machine.
|
|
21
|
+
|
|
22
|
+
All operations are routed through the state machine, which controls
|
|
23
|
+
screenshots and validates actions (tap coordinates, etc.).
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
>>> from tests.integration.state_machine import load_test_case
|
|
27
|
+
>>> state_machine, instruction, max_steps = load_test_case("scenario.yaml")
|
|
28
|
+
>>>
|
|
29
|
+
>>> device = MockDevice("mock_001", state_machine)
|
|
30
|
+
>>> screenshot = device.get_screenshot() # Returns current state's screenshot
|
|
31
|
+
>>> device.tap(100, 200) # State machine validates and transitions
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, device_id: str, state_machine: "StateMachine"):
|
|
35
|
+
"""
|
|
36
|
+
Initialize mock device.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
device_id: Mock device ID.
|
|
40
|
+
state_machine: State machine that controls test flow.
|
|
41
|
+
"""
|
|
42
|
+
self._device_id = device_id
|
|
43
|
+
self._state_machine = state_machine
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def device_id(self) -> str:
|
|
47
|
+
"""Unique device identifier."""
|
|
48
|
+
return self._device_id
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def state_machine(self) -> "StateMachine":
|
|
52
|
+
"""Get the underlying state machine."""
|
|
53
|
+
return self._state_machine
|
|
54
|
+
|
|
55
|
+
# === Screenshot ===
|
|
56
|
+
def get_screenshot(self, timeout: int = 10) -> Screenshot:
|
|
57
|
+
"""Get screenshot from current state."""
|
|
58
|
+
result = self._state_machine.get_current_screenshot()
|
|
59
|
+
return Screenshot(
|
|
60
|
+
base64_data=result.base64_data,
|
|
61
|
+
width=result.width,
|
|
62
|
+
height=result.height,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# === Input Operations ===
|
|
66
|
+
def tap(self, x: int, y: int, delay: float | None = None) -> None:
|
|
67
|
+
"""Handle tap action through state machine."""
|
|
68
|
+
self._state_machine.handle_tap(x, y)
|
|
69
|
+
|
|
70
|
+
def double_tap(self, x: int, y: int, delay: float | None = None) -> None:
|
|
71
|
+
"""Handle double tap (treated as single tap)."""
|
|
72
|
+
self._state_machine.handle_tap(x, y)
|
|
73
|
+
|
|
74
|
+
def long_press(
|
|
75
|
+
self, x: int, y: int, duration_ms: int = 3000, delay: float | None = None
|
|
76
|
+
) -> None:
|
|
77
|
+
"""Handle long press (treated as tap for testing)."""
|
|
78
|
+
self._state_machine.handle_tap(x, y)
|
|
79
|
+
|
|
80
|
+
def swipe(
|
|
81
|
+
self,
|
|
82
|
+
start_x: int,
|
|
83
|
+
start_y: int,
|
|
84
|
+
end_x: int,
|
|
85
|
+
end_y: int,
|
|
86
|
+
duration_ms: int | None = None,
|
|
87
|
+
delay: float | None = None,
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Handle swipe action."""
|
|
90
|
+
self._state_machine.handle_swipe(start_x, start_y, end_x, end_y)
|
|
91
|
+
|
|
92
|
+
def type_text(self, text: str) -> None:
|
|
93
|
+
"""Handle text input (no-op in testing)."""
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
def clear_text(self) -> None:
|
|
97
|
+
"""Handle text clear (no-op in testing)."""
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
# === Navigation ===
|
|
101
|
+
def back(self, delay: float | None = None) -> None:
|
|
102
|
+
"""Handle back button (no-op in testing)."""
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
def home(self, delay: float | None = None) -> None:
|
|
106
|
+
"""Handle home button (no-op in testing)."""
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
def launch_app(self, app_name: str, delay: float | None = None) -> bool:
|
|
110
|
+
"""Handle app launch (always succeeds in testing)."""
|
|
111
|
+
return True
|
|
112
|
+
|
|
113
|
+
# === State Query ===
|
|
114
|
+
def get_current_app(self) -> str:
|
|
115
|
+
"""Get current app name from the current state."""
|
|
116
|
+
return self._state_machine.current_state.current_app
|
|
117
|
+
|
|
118
|
+
# === Keyboard Management ===
|
|
119
|
+
def detect_and_set_adb_keyboard(self) -> str:
|
|
120
|
+
"""Mock keyboard detection."""
|
|
121
|
+
return "com.mock.keyboard"
|
|
122
|
+
|
|
123
|
+
def restore_keyboard(self, ime: str) -> None:
|
|
124
|
+
"""Mock keyboard restore."""
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class MockDeviceManager:
|
|
129
|
+
"""
|
|
130
|
+
Mock device manager for testing.
|
|
131
|
+
|
|
132
|
+
Provides a single mock device backed by a state machine.
|
|
133
|
+
|
|
134
|
+
Example:
|
|
135
|
+
>>> state_machine, _, _ = load_test_case("scenario.yaml")
|
|
136
|
+
>>> manager = MockDeviceManager(state_machine)
|
|
137
|
+
>>> device = manager.get_device("mock_001")
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
def __init__(
|
|
141
|
+
self,
|
|
142
|
+
state_machine: "StateMachine",
|
|
143
|
+
device_id: str = "mock_device_001",
|
|
144
|
+
):
|
|
145
|
+
"""
|
|
146
|
+
Initialize mock device manager.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
state_machine: State machine for the mock device.
|
|
150
|
+
device_id: ID for the mock device.
|
|
151
|
+
"""
|
|
152
|
+
self._device_id = device_id
|
|
153
|
+
self._device = MockDevice(device_id, state_machine)
|
|
154
|
+
|
|
155
|
+
def list_devices(self) -> list[DeviceInfo]:
|
|
156
|
+
"""List the mock device."""
|
|
157
|
+
return [
|
|
158
|
+
DeviceInfo(
|
|
159
|
+
device_id=self._device_id,
|
|
160
|
+
status="online",
|
|
161
|
+
model="MockPhone",
|
|
162
|
+
platform="android",
|
|
163
|
+
connection_type="mock",
|
|
164
|
+
)
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
def get_device(self, device_id: str) -> MockDevice:
|
|
168
|
+
"""Get the mock device."""
|
|
169
|
+
if device_id != self._device_id:
|
|
170
|
+
raise KeyError(f"Device '{device_id}' not found")
|
|
171
|
+
return self._device
|
|
172
|
+
|
|
173
|
+
def connect(self, address: str, timeout: int = 10) -> tuple[bool, str]:
|
|
174
|
+
"""Mock connect (always succeeds)."""
|
|
175
|
+
return True, f"Connected to {address}"
|
|
176
|
+
|
|
177
|
+
def disconnect(self, device_id: str) -> tuple[bool, str]:
|
|
178
|
+
"""Mock disconnect (always succeeds)."""
|
|
179
|
+
return True, f"Disconnected from {device_id}"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# Verify MockDeviceManager implements DeviceManagerProtocol
|
|
183
|
+
# Same issue as above - can't verify at import time
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Remote Device implementation using HTTP.
|
|
2
|
+
|
|
3
|
+
This module provides a RemoteDevice that connects to a Device Agent
|
|
4
|
+
via HTTP, allowing remote control of devices.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from AutoGLM_GUI.device_protocol import DeviceInfo, Screenshot
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RemoteDevice:
|
|
13
|
+
"""
|
|
14
|
+
Remote device implementation using HTTP.
|
|
15
|
+
|
|
16
|
+
Connects to a Device Agent server that handles actual device operations.
|
|
17
|
+
The server decides the implementation (ADB, Accessibility, Mock, etc.).
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
>>> device = RemoteDevice("phone_001", "http://localhost:8001")
|
|
21
|
+
>>> screenshot = device.get_screenshot()
|
|
22
|
+
>>> device.tap(100, 200)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, device_id: str, base_url: str, timeout: float = 30.0):
|
|
26
|
+
self._device_id = device_id
|
|
27
|
+
self._base_url = base_url.rstrip("/")
|
|
28
|
+
self._client = httpx.Client(timeout=timeout)
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def device_id(self) -> str:
|
|
32
|
+
return self._device_id
|
|
33
|
+
|
|
34
|
+
def _post(self, endpoint: str, json: dict | None = None) -> dict:
|
|
35
|
+
"""POST request helper."""
|
|
36
|
+
url = f"{self._base_url}/device/{self._device_id}{endpoint}"
|
|
37
|
+
resp = self._client.post(url, json=json or {})
|
|
38
|
+
resp.raise_for_status()
|
|
39
|
+
return resp.json()
|
|
40
|
+
|
|
41
|
+
def _get(self, endpoint: str) -> dict:
|
|
42
|
+
"""GET request helper."""
|
|
43
|
+
url = f"{self._base_url}/device/{self._device_id}{endpoint}"
|
|
44
|
+
resp = self._client.get(url)
|
|
45
|
+
resp.raise_for_status()
|
|
46
|
+
return resp.json()
|
|
47
|
+
|
|
48
|
+
def get_screenshot(self, timeout: int = 10) -> Screenshot:
|
|
49
|
+
data = self._post("/screenshot", {"timeout": timeout})
|
|
50
|
+
return Screenshot(
|
|
51
|
+
base64_data=data["base64_data"],
|
|
52
|
+
width=data["width"],
|
|
53
|
+
height=data["height"],
|
|
54
|
+
is_sensitive=data.get("is_sensitive", False),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def tap(self, x: int, y: int, delay: float | None = None) -> None:
|
|
58
|
+
self._post("/tap", {"x": x, "y": y, "delay": delay})
|
|
59
|
+
|
|
60
|
+
def double_tap(self, x: int, y: int, delay: float | None = None) -> None:
|
|
61
|
+
self._post("/double_tap", {"x": x, "y": y, "delay": delay})
|
|
62
|
+
|
|
63
|
+
def long_press(
|
|
64
|
+
self, x: int, y: int, duration_ms: int = 3000, delay: float | None = None
|
|
65
|
+
) -> None:
|
|
66
|
+
self._post(
|
|
67
|
+
"/long_press", {"x": x, "y": y, "duration_ms": duration_ms, "delay": delay}
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def swipe(
|
|
71
|
+
self,
|
|
72
|
+
start_x: int,
|
|
73
|
+
start_y: int,
|
|
74
|
+
end_x: int,
|
|
75
|
+
end_y: int,
|
|
76
|
+
duration_ms: int | None = None,
|
|
77
|
+
delay: float | None = None,
|
|
78
|
+
) -> None:
|
|
79
|
+
self._post(
|
|
80
|
+
"/swipe",
|
|
81
|
+
{
|
|
82
|
+
"start_x": start_x,
|
|
83
|
+
"start_y": start_y,
|
|
84
|
+
"end_x": end_x,
|
|
85
|
+
"end_y": end_y,
|
|
86
|
+
"duration_ms": duration_ms,
|
|
87
|
+
"delay": delay,
|
|
88
|
+
},
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def type_text(self, text: str) -> None:
|
|
92
|
+
self._post("/type_text", {"text": text})
|
|
93
|
+
|
|
94
|
+
def clear_text(self) -> None:
|
|
95
|
+
self._post("/clear_text")
|
|
96
|
+
|
|
97
|
+
def back(self, delay: float | None = None) -> None:
|
|
98
|
+
self._post("/back", {"delay": delay})
|
|
99
|
+
|
|
100
|
+
def home(self, delay: float | None = None) -> None:
|
|
101
|
+
self._post("/home", {"delay": delay})
|
|
102
|
+
|
|
103
|
+
def launch_app(self, app_name: str, delay: float | None = None) -> bool:
|
|
104
|
+
data = self._post("/launch_app", {"app_name": app_name, "delay": delay})
|
|
105
|
+
return data.get("success", True)
|
|
106
|
+
|
|
107
|
+
def get_current_app(self) -> str:
|
|
108
|
+
data = self._get("/current_app")
|
|
109
|
+
return data["app_name"]
|
|
110
|
+
|
|
111
|
+
def detect_and_set_adb_keyboard(self) -> str:
|
|
112
|
+
data = self._post("/detect_keyboard")
|
|
113
|
+
return data.get("original_ime", "")
|
|
114
|
+
|
|
115
|
+
def restore_keyboard(self, ime: str) -> None:
|
|
116
|
+
self._post("/restore_keyboard", {"ime": ime})
|
|
117
|
+
|
|
118
|
+
def close(self) -> None:
|
|
119
|
+
"""Close the HTTP client."""
|
|
120
|
+
self._client.close()
|
|
121
|
+
|
|
122
|
+
def __enter__(self):
|
|
123
|
+
return self
|
|
124
|
+
|
|
125
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
126
|
+
self.close()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class RemoteDeviceManager:
|
|
130
|
+
"""
|
|
131
|
+
Remote device manager using HTTP.
|
|
132
|
+
|
|
133
|
+
Manages connections to a Device Agent server.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def __init__(self, base_url: str, timeout: float = 30.0):
|
|
137
|
+
self._base_url = base_url.rstrip("/")
|
|
138
|
+
self._timeout = timeout
|
|
139
|
+
self._client = httpx.Client(timeout=timeout)
|
|
140
|
+
self._devices: dict[str, RemoteDevice] = {}
|
|
141
|
+
|
|
142
|
+
def list_devices(self) -> list[DeviceInfo]:
|
|
143
|
+
resp = self._client.get(f"{self._base_url}/devices")
|
|
144
|
+
resp.raise_for_status()
|
|
145
|
+
return [DeviceInfo(**d) for d in resp.json()]
|
|
146
|
+
|
|
147
|
+
def get_device(self, device_id: str) -> RemoteDevice:
|
|
148
|
+
if device_id not in self._devices:
|
|
149
|
+
self._devices[device_id] = RemoteDevice(
|
|
150
|
+
device_id, self._base_url, self._timeout
|
|
151
|
+
)
|
|
152
|
+
return self._devices[device_id]
|
|
153
|
+
|
|
154
|
+
def connect(self, address: str, timeout: int = 10) -> tuple[bool, str]:
|
|
155
|
+
resp = self._client.post(
|
|
156
|
+
f"{self._base_url}/connect", json={"address": address, "timeout": timeout}
|
|
157
|
+
)
|
|
158
|
+
data = resp.json()
|
|
159
|
+
return data.get("success", False), data.get("message", "")
|
|
160
|
+
|
|
161
|
+
def disconnect(self, device_id: str) -> tuple[bool, str]:
|
|
162
|
+
self._devices.pop(device_id, None)
|
|
163
|
+
resp = self._client.post(
|
|
164
|
+
f"{self._base_url}/disconnect", json={"device_id": device_id}
|
|
165
|
+
)
|
|
166
|
+
data = resp.json()
|
|
167
|
+
return data.get("success", True), data.get("message", "Disconnected")
|
|
168
|
+
|
|
169
|
+
def close(self) -> None:
|
|
170
|
+
for device in self._devices.values():
|
|
171
|
+
device.close()
|
|
172
|
+
self._client.close()
|
|
@@ -49,8 +49,8 @@ class ActionStep:
|
|
|
49
49
|
need_generate: bool = False
|
|
50
50
|
direction: Optional[str] = None
|
|
51
51
|
|
|
52
|
-
def to_dict(self) -> dict:
|
|
53
|
-
result = {"action": self.action, "target": self.target}
|
|
52
|
+
def to_dict(self) -> dict[str, str | bool]:
|
|
53
|
+
result: dict[str, str | bool] = {"action": self.action, "target": self.target}
|
|
54
54
|
if self.content:
|
|
55
55
|
result["content"] = self.content
|
|
56
56
|
if self.need_generate:
|
|
@@ -127,7 +127,7 @@ class DecisionModel:
|
|
|
127
127
|
self.client = OpenAI(
|
|
128
128
|
base_url=config.base_url,
|
|
129
129
|
api_key=config.api_key,
|
|
130
|
-
)
|
|
130
|
+
) # type: ignore[call-arg]
|
|
131
131
|
self.model_name = config.model_name
|
|
132
132
|
self.conversation_history: list[dict] = []
|
|
133
133
|
self.current_task: str = ""
|
|
@@ -159,7 +159,7 @@ class DecisionModel:
|
|
|
159
159
|
try:
|
|
160
160
|
response = self.client.chat.completions.create(
|
|
161
161
|
model=self.model_name,
|
|
162
|
-
messages=messages,
|
|
162
|
+
messages=messages, # type: ignore[arg-type]
|
|
163
163
|
max_tokens=self.config.max_tokens,
|
|
164
164
|
temperature=self.config.temperature,
|
|
165
165
|
stream=True,
|
AutoGLM_GUI/exceptions.py
CHANGED
|
@@ -79,9 +79,9 @@ class AgentInitializationError(Exception):
|
|
|
79
79
|
How to fix:
|
|
80
80
|
1. Check configuration:
|
|
81
81
|
>>> from AutoGLM_GUI.config_manager import config_manager
|
|
82
|
-
>>>
|
|
83
|
-
>>> print(f"base_url: {
|
|
84
|
-
>>> print(f"model_name: {
|
|
82
|
+
>>> effective_config = config_manager.get_effective_config()
|
|
83
|
+
>>> print(f"base_url: {effective_config.base_url}")
|
|
84
|
+
>>> print(f"model_name: {effective_config.model_name}")
|
|
85
85
|
|
|
86
86
|
2. Set configuration:
|
|
87
87
|
>>> via API: POST /api/config {"base_url": "...", "model_name": "...", "api_key": "..."}
|
|
@@ -8,8 +8,8 @@ from phone_agent.actions.handler import ActionHandler
|
|
|
8
8
|
from phone_agent.model import ModelConfig
|
|
9
9
|
|
|
10
10
|
from AutoGLM_GUI.logger import logger
|
|
11
|
-
from AutoGLM_GUI.mai_ui.mai_navigation_agent import MAIUINaivigationAgent
|
|
12
|
-
from AutoGLM_GUI.mai_ui_adapter.action_adapter import MAIUIActionAdapter
|
|
11
|
+
from AutoGLM_GUI.mai_ui.mai_navigation_agent import MAIUINaivigationAgent # type: ignore[import-not-found]
|
|
12
|
+
from AutoGLM_GUI.mai_ui_adapter.action_adapter import MAIUIActionAdapter # type: ignore[import-not-found]
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
@dataclass
|