autoglm-gui 1.4.1__py3-none-any.whl → 1.5.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.
Files changed (135) hide show
  1. AutoGLM_GUI/__init__.py +11 -0
  2. AutoGLM_GUI/__main__.py +26 -4
  3. AutoGLM_GUI/actions/__init__.py +6 -0
  4. phone_agent/actions/handler_ios.py → AutoGLM_GUI/actions/handler.py +30 -112
  5. AutoGLM_GUI/actions/types.py +15 -0
  6. {phone_agent → AutoGLM_GUI}/adb/__init__.py +25 -23
  7. {phone_agent → AutoGLM_GUI}/adb/connection.py +5 -40
  8. {phone_agent → AutoGLM_GUI}/adb/device.py +12 -94
  9. {phone_agent → AutoGLM_GUI}/adb/input.py +6 -47
  10. AutoGLM_GUI/adb/screenshot.py +11 -0
  11. {phone_agent/config → AutoGLM_GUI/adb}/timing.py +1 -1
  12. AutoGLM_GUI/adb_plus/keyboard_installer.py +4 -2
  13. AutoGLM_GUI/adb_plus/screenshot.py +22 -1
  14. AutoGLM_GUI/adb_plus/serial.py +38 -20
  15. AutoGLM_GUI/adb_plus/touch.py +4 -9
  16. AutoGLM_GUI/agents/__init__.py +43 -12
  17. AutoGLM_GUI/agents/events.py +19 -0
  18. AutoGLM_GUI/agents/factory.py +31 -38
  19. AutoGLM_GUI/agents/glm/__init__.py +7 -0
  20. AutoGLM_GUI/agents/glm/agent.py +297 -0
  21. AutoGLM_GUI/agents/glm/message_builder.py +81 -0
  22. AutoGLM_GUI/agents/glm/parser.py +110 -0
  23. {phone_agent/config → AutoGLM_GUI/agents/glm}/prompts_en.py +7 -9
  24. {phone_agent/config → AutoGLM_GUI/agents/glm}/prompts_zh.py +18 -25
  25. AutoGLM_GUI/agents/mai/__init__.py +28 -0
  26. AutoGLM_GUI/agents/mai/agent.py +408 -0
  27. AutoGLM_GUI/agents/mai/parser.py +254 -0
  28. AutoGLM_GUI/agents/mai/prompts.py +103 -0
  29. AutoGLM_GUI/agents/mai/traj_memory.py +91 -0
  30. AutoGLM_GUI/agents/protocols.py +12 -8
  31. AutoGLM_GUI/agents/stream_runner.py +193 -0
  32. AutoGLM_GUI/api/__init__.py +40 -21
  33. AutoGLM_GUI/api/agents.py +181 -239
  34. AutoGLM_GUI/api/control.py +9 -6
  35. AutoGLM_GUI/api/devices.py +102 -12
  36. AutoGLM_GUI/api/history.py +104 -0
  37. AutoGLM_GUI/api/layered_agent.py +67 -15
  38. AutoGLM_GUI/api/media.py +64 -1
  39. AutoGLM_GUI/api/scheduled_tasks.py +98 -0
  40. AutoGLM_GUI/config.py +81 -0
  41. AutoGLM_GUI/config_manager.py +68 -51
  42. AutoGLM_GUI/device_manager.py +248 -29
  43. AutoGLM_GUI/device_protocol.py +1 -1
  44. AutoGLM_GUI/devices/adb_device.py +5 -10
  45. AutoGLM_GUI/devices/mock_device.py +4 -2
  46. AutoGLM_GUI/devices/remote_device.py +8 -3
  47. AutoGLM_GUI/history_manager.py +164 -0
  48. AutoGLM_GUI/model/__init__.py +5 -0
  49. AutoGLM_GUI/model/message_builder.py +69 -0
  50. AutoGLM_GUI/model/types.py +24 -0
  51. AutoGLM_GUI/models/__init__.py +10 -0
  52. AutoGLM_GUI/models/history.py +140 -0
  53. AutoGLM_GUI/models/scheduled_task.py +71 -0
  54. AutoGLM_GUI/parsers/__init__.py +22 -0
  55. AutoGLM_GUI/parsers/base.py +50 -0
  56. AutoGLM_GUI/parsers/phone_parser.py +58 -0
  57. AutoGLM_GUI/phone_agent_manager.py +62 -396
  58. AutoGLM_GUI/platform_utils.py +26 -0
  59. AutoGLM_GUI/prompt_config.py +15 -0
  60. AutoGLM_GUI/prompts/__init__.py +32 -0
  61. AutoGLM_GUI/scheduler_manager.py +350 -0
  62. AutoGLM_GUI/schemas.py +246 -72
  63. AutoGLM_GUI/scrcpy_stream.py +142 -24
  64. AutoGLM_GUI/socketio_server.py +100 -27
  65. AutoGLM_GUI/static/assets/{about-_XNhzQZX.js → about-CfwX1Cmc.js} +1 -1
  66. AutoGLM_GUI/static/assets/alert-dialog-CtGlN2IJ.js +1 -0
  67. AutoGLM_GUI/static/assets/chat-BYa-foUI.js +129 -0
  68. AutoGLM_GUI/static/assets/circle-alert-t08bEMPO.js +1 -0
  69. AutoGLM_GUI/static/assets/dialog-FNwZJFwk.js +45 -0
  70. AutoGLM_GUI/static/assets/eye-D0UPWCWC.js +1 -0
  71. AutoGLM_GUI/static/assets/history-CRo95B7i.js +1 -0
  72. AutoGLM_GUI/static/assets/{index-Cy8TmmHV.js → index-BaLMSqd3.js} +1 -1
  73. AutoGLM_GUI/static/assets/index-CTHbFvKl.js +11 -0
  74. AutoGLM_GUI/static/assets/index-CV7jGxGm.css +1 -0
  75. AutoGLM_GUI/static/assets/label-DJFevVmr.js +1 -0
  76. AutoGLM_GUI/static/assets/logs-RW09DyYY.js +1 -0
  77. AutoGLM_GUI/static/assets/popover--JTJrE5v.js +1 -0
  78. AutoGLM_GUI/static/assets/scheduled-tasks-DTRKsQXF.js +1 -0
  79. AutoGLM_GUI/static/assets/square-pen-CPK_K680.js +1 -0
  80. AutoGLM_GUI/static/assets/textarea-PRmVnWq5.js +1 -0
  81. AutoGLM_GUI/static/assets/workflows-CdcsAoaT.js +1 -0
  82. AutoGLM_GUI/static/index.html +2 -2
  83. AutoGLM_GUI/types.py +17 -0
  84. {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.1.dist-info}/METADATA +179 -130
  85. autoglm_gui-1.5.1.dist-info/RECORD +118 -0
  86. AutoGLM_GUI/agents/mai_adapter.py +0 -627
  87. AutoGLM_GUI/api/dual_model.py +0 -317
  88. AutoGLM_GUI/device_adapter.py +0 -263
  89. AutoGLM_GUI/dual_model/__init__.py +0 -53
  90. AutoGLM_GUI/dual_model/decision_model.py +0 -664
  91. AutoGLM_GUI/dual_model/dual_agent.py +0 -917
  92. AutoGLM_GUI/dual_model/protocols.py +0 -354
  93. AutoGLM_GUI/dual_model/vision_model.py +0 -442
  94. AutoGLM_GUI/mai_ui_adapter/agent_wrapper.py +0 -291
  95. AutoGLM_GUI/phone_agent_patches.py +0 -147
  96. AutoGLM_GUI/static/assets/chat-DwJpiAWf.js +0 -126
  97. AutoGLM_GUI/static/assets/dialog-B3uW4T8V.js +0 -45
  98. AutoGLM_GUI/static/assets/index-Cpv2gSF1.css +0 -1
  99. AutoGLM_GUI/static/assets/index-UYYauTly.js +0 -12
  100. AutoGLM_GUI/static/assets/workflows-Du_de-dt.js +0 -1
  101. autoglm_gui-1.4.1.dist-info/RECORD +0 -117
  102. mai_agent/base.py +0 -137
  103. mai_agent/mai_grounding_agent.py +0 -263
  104. mai_agent/mai_naivigation_agent.py +0 -526
  105. mai_agent/prompt.py +0 -148
  106. mai_agent/unified_memory.py +0 -67
  107. mai_agent/utils.py +0 -73
  108. phone_agent/__init__.py +0 -12
  109. phone_agent/actions/__init__.py +0 -5
  110. phone_agent/actions/handler.py +0 -400
  111. phone_agent/adb/screenshot.py +0 -108
  112. phone_agent/agent.py +0 -253
  113. phone_agent/agent_ios.py +0 -277
  114. phone_agent/config/__init__.py +0 -53
  115. phone_agent/config/apps_harmonyos.py +0 -256
  116. phone_agent/config/apps_ios.py +0 -339
  117. phone_agent/config/prompts.py +0 -80
  118. phone_agent/device_factory.py +0 -166
  119. phone_agent/hdc/__init__.py +0 -53
  120. phone_agent/hdc/connection.py +0 -384
  121. phone_agent/hdc/device.py +0 -269
  122. phone_agent/hdc/input.py +0 -145
  123. phone_agent/hdc/screenshot.py +0 -127
  124. phone_agent/model/__init__.py +0 -5
  125. phone_agent/model/client.py +0 -290
  126. phone_agent/xctest/__init__.py +0 -47
  127. phone_agent/xctest/connection.py +0 -379
  128. phone_agent/xctest/device.py +0 -472
  129. phone_agent/xctest/input.py +0 -311
  130. phone_agent/xctest/screenshot.py +0 -226
  131. {phone_agent/config → AutoGLM_GUI/adb}/apps.py +0 -0
  132. {phone_agent/config → AutoGLM_GUI}/i18n.py +0 -0
  133. {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.1.dist-info}/WHEEL +0 -0
  134. {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.1.dist-info}/entry_points.txt +0 -0
  135. {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,290 +0,0 @@
1
- """Model client for AI inference using OpenAI-compatible API."""
2
-
3
- import json
4
- import time
5
- from dataclasses import dataclass, field
6
- from typing import Any
7
-
8
- from openai import OpenAI
9
-
10
- from phone_agent.config.i18n import get_message
11
-
12
-
13
- @dataclass
14
- class ModelConfig:
15
- """Configuration for the AI model."""
16
-
17
- base_url: str = "http://localhost:8000/v1"
18
- api_key: str = "EMPTY"
19
- model_name: str = "autoglm-phone-9b"
20
- max_tokens: int = 3000
21
- temperature: float = 0.0
22
- top_p: float = 0.85
23
- frequency_penalty: float = 0.2
24
- extra_body: dict[str, Any] = field(default_factory=dict)
25
- lang: str = "cn" # Language for UI messages: 'cn' or 'en'
26
-
27
-
28
- @dataclass
29
- class ModelResponse:
30
- """Response from the AI model."""
31
-
32
- thinking: str
33
- action: str
34
- raw_content: str
35
- # Performance metrics
36
- time_to_first_token: float | None = None # Time to first token (seconds)
37
- time_to_thinking_end: float | None = None # Time to thinking end (seconds)
38
- total_time: float | None = None # Total inference time (seconds)
39
-
40
-
41
- class ModelClient:
42
- """
43
- Client for interacting with OpenAI-compatible vision-language models.
44
-
45
- Args:
46
- config: Model configuration.
47
- """
48
-
49
- def __init__(self, config: ModelConfig | None = None):
50
- self.config = config or ModelConfig()
51
- self.client = OpenAI(base_url=self.config.base_url, api_key=self.config.api_key)
52
-
53
- def request(self, messages: list[dict[str, Any]]) -> ModelResponse:
54
- """
55
- Send a request to the model.
56
-
57
- Args:
58
- messages: List of message dictionaries in OpenAI format.
59
-
60
- Returns:
61
- ModelResponse containing thinking and action.
62
-
63
- Raises:
64
- ValueError: If the response cannot be parsed.
65
- """
66
- # Start timing
67
- start_time = time.time()
68
- time_to_first_token = None
69
- time_to_thinking_end = None
70
-
71
- stream = self.client.chat.completions.create(
72
- messages=messages,
73
- model=self.config.model_name,
74
- max_tokens=self.config.max_tokens,
75
- temperature=self.config.temperature,
76
- top_p=self.config.top_p,
77
- frequency_penalty=self.config.frequency_penalty,
78
- extra_body=self.config.extra_body,
79
- stream=True,
80
- )
81
-
82
- raw_content = ""
83
- buffer = "" # Buffer to hold content that might be part of a marker
84
- action_markers = ["finish(message=", "do(action="]
85
- in_action_phase = False # Track if we've entered the action phase
86
- first_token_received = False
87
-
88
- for chunk in stream:
89
- if len(chunk.choices) == 0:
90
- continue
91
- if chunk.choices[0].delta.content is not None:
92
- content = chunk.choices[0].delta.content
93
- raw_content += content
94
-
95
- # Record time to first token
96
- if not first_token_received:
97
- time_to_first_token = time.time() - start_time
98
- first_token_received = True
99
-
100
- if in_action_phase:
101
- # Already in action phase, just accumulate content without printing
102
- continue
103
-
104
- buffer += content
105
-
106
- # Check if any marker is fully present in buffer
107
- marker_found = False
108
- for marker in action_markers:
109
- if marker in buffer:
110
- # Marker found, print everything before it
111
- thinking_part = buffer.split(marker, 1)[0]
112
- print(thinking_part, end="", flush=True)
113
- print() # Print newline after thinking is complete
114
- in_action_phase = True
115
- marker_found = True
116
-
117
- # Record time to thinking end
118
- if time_to_thinking_end is None:
119
- time_to_thinking_end = time.time() - start_time
120
-
121
- break
122
-
123
- if marker_found:
124
- continue # Continue to collect remaining content
125
-
126
- # Check if buffer ends with a prefix of any marker
127
- # If so, don't print yet (wait for more content)
128
- is_potential_marker = False
129
- for marker in action_markers:
130
- for i in range(1, len(marker)):
131
- if buffer.endswith(marker[:i]):
132
- is_potential_marker = True
133
- break
134
- if is_potential_marker:
135
- break
136
-
137
- if not is_potential_marker:
138
- # Safe to print the buffer
139
- print(buffer, end="", flush=True)
140
- buffer = ""
141
-
142
- # Calculate total time
143
- total_time = time.time() - start_time
144
-
145
- # Parse thinking and action from response
146
- thinking, action = self._parse_response(raw_content)
147
-
148
- # Print performance metrics
149
- lang = self.config.lang
150
- print()
151
- print("=" * 50)
152
- print(f"⏱️ {get_message('performance_metrics', lang)}:")
153
- print("-" * 50)
154
- if time_to_first_token is not None:
155
- print(
156
- f"{get_message('time_to_first_token', lang)}: {time_to_first_token:.3f}s"
157
- )
158
- if time_to_thinking_end is not None:
159
- print(
160
- f"{get_message('time_to_thinking_end', lang)}: {time_to_thinking_end:.3f}s"
161
- )
162
- print(
163
- f"{get_message('total_inference_time', lang)}: {total_time:.3f}s"
164
- )
165
- print("=" * 50)
166
-
167
- return ModelResponse(
168
- thinking=thinking,
169
- action=action,
170
- raw_content=raw_content,
171
- time_to_first_token=time_to_first_token,
172
- time_to_thinking_end=time_to_thinking_end,
173
- total_time=total_time,
174
- )
175
-
176
- def _parse_response(self, content: str) -> tuple[str, str]:
177
- """
178
- Parse the model response into thinking and action parts.
179
-
180
- Parsing rules:
181
- 1. If content contains 'finish(message=', everything before is thinking,
182
- everything from 'finish(message=' onwards is action.
183
- 2. If rule 1 doesn't apply but content contains 'do(action=',
184
- everything before is thinking, everything from 'do(action=' onwards is action.
185
- 3. Fallback: If content contains '<answer>', use legacy parsing with XML tags.
186
- 4. Otherwise, return empty thinking and full content as action.
187
-
188
- Args:
189
- content: Raw response content.
190
-
191
- Returns:
192
- Tuple of (thinking, action).
193
- """
194
- # Rule 1: Check for finish(message=
195
- if "finish(message=" in content:
196
- parts = content.split("finish(message=", 1)
197
- thinking = parts[0].strip()
198
- action = "finish(message=" + parts[1]
199
- return thinking, action
200
-
201
- # Rule 2: Check for do(action=
202
- if "do(action=" in content:
203
- parts = content.split("do(action=", 1)
204
- thinking = parts[0].strip()
205
- action = "do(action=" + parts[1]
206
- return thinking, action
207
-
208
- # Rule 3: Fallback to legacy XML tag parsing
209
- if "<answer>" in content:
210
- parts = content.split("<answer>", 1)
211
- thinking = parts[0].replace("<think>", "").replace("</think>", "").strip()
212
- action = parts[1].replace("</answer>", "").strip()
213
- return thinking, action
214
-
215
- # Rule 4: No markers found, return content as action
216
- return "", content
217
-
218
-
219
- class MessageBuilder:
220
- """Helper class for building conversation messages."""
221
-
222
- @staticmethod
223
- def create_system_message(content: str) -> dict[str, Any]:
224
- """Create a system message."""
225
- return {"role": "system", "content": content}
226
-
227
- @staticmethod
228
- def create_user_message(
229
- text: str, image_base64: str | None = None
230
- ) -> dict[str, Any]:
231
- """
232
- Create a user message with optional image.
233
-
234
- Args:
235
- text: Text content.
236
- image_base64: Optional base64-encoded image.
237
-
238
- Returns:
239
- Message dictionary.
240
- """
241
- content = []
242
-
243
- if image_base64:
244
- content.append(
245
- {
246
- "type": "image_url",
247
- "image_url": {"url": f"data:image/png;base64,{image_base64}"},
248
- }
249
- )
250
-
251
- content.append({"type": "text", "text": text})
252
-
253
- return {"role": "user", "content": content}
254
-
255
- @staticmethod
256
- def create_assistant_message(content: str) -> dict[str, Any]:
257
- """Create an assistant message."""
258
- return {"role": "assistant", "content": content}
259
-
260
- @staticmethod
261
- def remove_images_from_message(message: dict[str, Any]) -> dict[str, Any]:
262
- """
263
- Remove image content from a message to save context space.
264
-
265
- Args:
266
- message: Message dictionary.
267
-
268
- Returns:
269
- Message with images removed.
270
- """
271
- if isinstance(message.get("content"), list):
272
- message["content"] = [
273
- item for item in message["content"] if item.get("type") == "text"
274
- ]
275
- return message
276
-
277
- @staticmethod
278
- def build_screen_info(current_app: str, **extra_info) -> str:
279
- """
280
- Build screen info string for the model.
281
-
282
- Args:
283
- current_app: Current app name.
284
- **extra_info: Additional info to include.
285
-
286
- Returns:
287
- JSON string with screen info.
288
- """
289
- info = {"current_app": current_app, **extra_info}
290
- return json.dumps(info, ensure_ascii=False)
@@ -1,47 +0,0 @@
1
- """XCTest utilities for iOS device interaction via WebDriverAgent/XCUITest."""
2
-
3
- from phone_agent.xctest.connection import (
4
- ConnectionType,
5
- DeviceInfo,
6
- XCTestConnection,
7
- list_devices,
8
- quick_connect,
9
- )
10
- from phone_agent.xctest.device import (
11
- back,
12
- double_tap,
13
- get_current_app,
14
- home,
15
- launch_app,
16
- long_press,
17
- swipe,
18
- tap,
19
- )
20
- from phone_agent.xctest.input import (
21
- clear_text,
22
- type_text,
23
- )
24
- from phone_agent.xctest.screenshot import get_screenshot
25
-
26
- __all__ = [
27
- # Screenshot
28
- "get_screenshot",
29
- # Input
30
- "type_text",
31
- "clear_text",
32
- # Device control
33
- "get_current_app",
34
- "tap",
35
- "swipe",
36
- "back",
37
- "home",
38
- "double_tap",
39
- "long_press",
40
- "launch_app",
41
- # Connection management
42
- "XCTestConnection",
43
- "DeviceInfo",
44
- "ConnectionType",
45
- "quick_connect",
46
- "list_devices",
47
- ]
@@ -1,379 +0,0 @@
1
- """iOS device connection management via idevice tools and WebDriverAgent."""
2
-
3
- import subprocess
4
- from dataclasses import dataclass
5
- from enum import Enum
6
-
7
-
8
- class ConnectionType(Enum):
9
- """Type of iOS connection."""
10
-
11
- USB = "usb"
12
- NETWORK = "network"
13
-
14
-
15
- @dataclass
16
- class DeviceInfo:
17
- """Information about a connected iOS device."""
18
-
19
- device_id: str # UDID
20
- status: str
21
- connection_type: ConnectionType
22
- model: str | None = None
23
- ios_version: str | None = None
24
- device_name: str | None = None
25
-
26
-
27
- class XCTestConnection:
28
- """
29
- Manages connections to iOS devices via libimobiledevice and WebDriverAgent.
30
-
31
- Requires:
32
- - libimobiledevice (idevice_id, ideviceinfo)
33
- - WebDriverAgent running on the iOS device
34
- - ios-deploy (optional, for app installation)
35
-
36
- Example:
37
- >>> conn = XCTestConnection()
38
- >>> # List connected devices
39
- >>> devices = conn.list_devices()
40
- >>> # Get device info
41
- >>> info = conn.get_device_info()
42
- >>> # Check if WDA is running
43
- >>> is_ready = conn.is_wda_ready()
44
- """
45
-
46
- def __init__(self, wda_url: str = "http://localhost:8100"):
47
- """
48
- Initialize iOS connection manager.
49
-
50
- Args:
51
- wda_url: WebDriverAgent URL (default: http://localhost:8100).
52
- For network devices, use http://<device-ip>:8100
53
- """
54
- self.wda_url = wda_url.rstrip("/")
55
-
56
- def list_devices(self) -> list[DeviceInfo]:
57
- """
58
- List all connected iOS devices.
59
-
60
- Returns:
61
- List of DeviceInfo objects.
62
-
63
- Note:
64
- Requires libimobiledevice to be installed.
65
- Install on macOS: brew install libimobiledevice
66
- """
67
- try:
68
- # Get list of device UDIDs
69
- result = subprocess.run(
70
- ["idevice_id", "-ln"],
71
- capture_output=True,
72
- text=True,
73
- timeout=5,
74
- )
75
-
76
- devices = []
77
- for line in result.stdout.strip().split("\n"):
78
- udid = line.strip()
79
- if not udid:
80
- continue
81
-
82
- # Determine connection type (network devices have specific format)
83
- conn_type = (
84
- ConnectionType.NETWORK
85
- if "-" in udid and len(udid) > 40
86
- else ConnectionType.USB
87
- )
88
-
89
- # Get detailed device info
90
- device_info = self._get_device_details(udid)
91
-
92
- devices.append(
93
- DeviceInfo(
94
- device_id=udid,
95
- status="connected",
96
- connection_type=conn_type,
97
- model=device_info.get("model"),
98
- ios_version=device_info.get("ios_version"),
99
- device_name=device_info.get("name"),
100
- )
101
- )
102
-
103
- return devices
104
-
105
- except FileNotFoundError:
106
- print(
107
- "Error: idevice_id not found. Install libimobiledevice: brew install libimobiledevice"
108
- )
109
- return []
110
- except Exception as e:
111
- print(f"Error listing devices: {e}")
112
- return []
113
-
114
- def _get_device_details(self, udid: str) -> dict[str, str]:
115
- """
116
- Get detailed information about a specific device.
117
-
118
- Args:
119
- udid: Device UDID.
120
-
121
- Returns:
122
- Dictionary with device details.
123
- """
124
- try:
125
- result = subprocess.run(
126
- ["ideviceinfo", "-u", udid],
127
- capture_output=True,
128
- text=True,
129
- timeout=5,
130
- )
131
-
132
- info = {}
133
- for line in result.stdout.split("\n"):
134
- if ": " in line:
135
- key, value = line.split(": ", 1)
136
- key = key.strip()
137
- value = value.strip()
138
-
139
- if key == "ProductType":
140
- info["model"] = value
141
- elif key == "ProductVersion":
142
- info["ios_version"] = value
143
- elif key == "DeviceName":
144
- info["name"] = value
145
-
146
- return info
147
-
148
- except Exception:
149
- return {}
150
-
151
- def get_device_info(self, device_id: str | None = None) -> DeviceInfo | None:
152
- """
153
- Get detailed information about a device.
154
-
155
- Args:
156
- device_id: Device UDID. If None, uses first available device.
157
-
158
- Returns:
159
- DeviceInfo or None if not found.
160
- """
161
- devices = self.list_devices()
162
-
163
- if not devices:
164
- return None
165
-
166
- if device_id is None:
167
- return devices[0]
168
-
169
- for device in devices:
170
- if device.device_id == device_id:
171
- return device
172
-
173
- return None
174
-
175
- def is_connected(self, device_id: str | None = None) -> bool:
176
- """
177
- Check if a device is connected.
178
-
179
- Args:
180
- device_id: Device UDID to check. If None, checks if any device is connected.
181
-
182
- Returns:
183
- True if connected, False otherwise.
184
- """
185
- devices = self.list_devices()
186
-
187
- if not devices:
188
- return False
189
-
190
- if device_id is None:
191
- return len(devices) > 0
192
-
193
- return any(d.device_id == device_id for d in devices)
194
-
195
- def is_wda_ready(self, timeout: int = 2) -> bool:
196
- """
197
- Check if WebDriverAgent is running and accessible.
198
-
199
- Args:
200
- timeout: Request timeout in seconds.
201
-
202
- Returns:
203
- True if WDA is ready, False otherwise.
204
- """
205
- try:
206
- import requests
207
-
208
- response = requests.get(
209
- f"{self.wda_url}/status", timeout=timeout, verify=False
210
- )
211
- return response.status_code == 200
212
- except ImportError:
213
- print("Error: requests library not found. Install it: pip install requests")
214
- return False
215
- except Exception:
216
- return False
217
-
218
- def start_wda_session(self) -> tuple[bool, str]:
219
- """
220
- Start a new WebDriverAgent session.
221
-
222
- Returns:
223
- Tuple of (success, session_id or error_message).
224
- """
225
- try:
226
- import requests
227
-
228
- response = requests.post(
229
- f"{self.wda_url}/session",
230
- json={"capabilities": {}},
231
- timeout=30,
232
- verify=False,
233
- )
234
-
235
- if response.status_code in (200, 201):
236
- data = response.json()
237
- session_id = data.get("sessionId") or data.get("value", {}).get(
238
- "sessionId"
239
- )
240
- return True, session_id or "session_started"
241
- else:
242
- return False, f"Failed to start session: {response.text}"
243
-
244
- except ImportError:
245
- return (
246
- False,
247
- "requests library not found. Install it: pip install requests",
248
- )
249
- except Exception as e:
250
- return False, f"Error starting WDA session: {e}"
251
-
252
- def get_wda_status(self) -> dict | None:
253
- """
254
- Get WebDriverAgent status information.
255
-
256
- Returns:
257
- Status dictionary or None if not available.
258
- """
259
- try:
260
- import requests
261
-
262
- response = requests.get(f"{self.wda_url}/status", timeout=5, verify=False)
263
-
264
- if response.status_code == 200:
265
- return response.json()
266
- return None
267
-
268
- except Exception:
269
- return None
270
-
271
- def pair_device(self, device_id: str | None = None) -> tuple[bool, str]:
272
- """
273
- Pair with an iOS device (required for some operations).
274
-
275
- Args:
276
- device_id: Device UDID. If None, uses first available device.
277
-
278
- Returns:
279
- Tuple of (success, message).
280
- """
281
- try:
282
- cmd = ["idevicepair"]
283
- if device_id:
284
- cmd.extend(["-u", device_id])
285
- cmd.append("pair")
286
-
287
- result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
288
-
289
- output = result.stdout + result.stderr
290
-
291
- if "SUCCESS" in output or "already paired" in output.lower():
292
- return True, "Device paired successfully"
293
- else:
294
- return False, output.strip()
295
-
296
- except FileNotFoundError:
297
- return (
298
- False,
299
- "idevicepair not found. Install libimobiledevice: brew install libimobiledevice",
300
- )
301
- except Exception as e:
302
- return False, f"Error pairing device: {e}"
303
-
304
- def get_device_name(self, device_id: str | None = None) -> str | None:
305
- """
306
- Get the device name.
307
-
308
- Args:
309
- device_id: Device UDID. If None, uses first available device.
310
-
311
- Returns:
312
- Device name string or None if not found.
313
- """
314
- try:
315
- cmd = ["ideviceinfo"]
316
- if device_id:
317
- cmd.extend(["-u", device_id])
318
- cmd.extend(["-k", "DeviceName"])
319
-
320
- result = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
321
-
322
- return result.stdout.strip() or None
323
-
324
- except Exception as e:
325
- print(f"Error getting device name: {e}")
326
- return None
327
-
328
- def restart_wda(self) -> tuple[bool, str]:
329
- """
330
- Restart WebDriverAgent (requires manual restart on device).
331
-
332
- Returns:
333
- Tuple of (success, message).
334
-
335
- Note:
336
- This method only checks if WDA needs restart.
337
- Actual restart requires re-running WDA on the device via Xcode or other means.
338
- """
339
- if self.is_wda_ready():
340
- return True, "WDA is already running"
341
- else:
342
- return (
343
- False,
344
- "WDA is not running. Please start it manually on the device.",
345
- )
346
-
347
-
348
- def quick_connect(wda_url: str = "http://localhost:8100") -> tuple[bool, str]:
349
- """
350
- Quick helper to check iOS device connection and WDA status.
351
-
352
- Args:
353
- wda_url: WebDriverAgent URL.
354
-
355
- Returns:
356
- Tuple of (success, message).
357
- """
358
- conn = XCTestConnection(wda_url=wda_url)
359
-
360
- # Check if device is connected
361
- if not conn.is_connected():
362
- return False, "No iOS device connected"
363
-
364
- # Check if WDA is ready
365
- if not conn.is_wda_ready():
366
- return False, "WebDriverAgent is not running"
367
-
368
- return True, "iOS device connected and WDA ready"
369
-
370
-
371
- def list_devices() -> list[DeviceInfo]:
372
- """
373
- Quick helper to list connected iOS devices.
374
-
375
- Returns:
376
- List of DeviceInfo objects.
377
- """
378
- conn = XCTestConnection()
379
- return conn.list_devices()