droidrun 0.3.2__py3-none-any.whl → 0.3.3__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.
- droidrun/__init__.py +6 -2
- droidrun/agent/codeact/codeact_agent.py +7 -6
- droidrun/agent/common/events.py +44 -1
- droidrun/agent/context/personas/__init__.py +2 -0
- droidrun/agent/context/personas/big_agent.py +96 -0
- droidrun/agent/context/personas/ui_expert.py +1 -0
- droidrun/agent/droid/droid_agent.py +15 -6
- droidrun/agent/planner/planner_agent.py +2 -2
- droidrun/agent/utils/executer.py +10 -2
- droidrun/agent/utils/trajectory.py +258 -11
- droidrun/cli/main.py +73 -36
- droidrun/macro/__init__.py +14 -0
- droidrun/macro/__main__.py +10 -0
- droidrun/macro/cli.py +228 -0
- droidrun/macro/replay.py +309 -0
- droidrun/portal.py +16 -17
- droidrun/telemetry/tracker.py +3 -2
- droidrun/tools/adb.py +668 -200
- droidrun/tools/ios.py +163 -163
- droidrun/tools/tools.py +32 -14
- {droidrun-0.3.2.dist-info → droidrun-0.3.3.dist-info}/METADATA +21 -8
- {droidrun-0.3.2.dist-info → droidrun-0.3.3.dist-info}/RECORD +25 -24
- droidrun/adb/__init__.py +0 -13
- droidrun/adb/device.py +0 -345
- droidrun/adb/manager.py +0 -93
- droidrun/adb/wrapper.py +0 -226
- {droidrun-0.3.2.dist-info → droidrun-0.3.3.dist-info}/WHEEL +0 -0
- {droidrun-0.3.2.dist-info → droidrun-0.3.3.dist-info}/entry_points.txt +0 -0
- {droidrun-0.3.2.dist-info → droidrun-0.3.3.dist-info}/licenses/LICENSE +0 -0
droidrun/tools/ios.py
CHANGED
@@ -4,10 +4,9 @@ UI Actions - Core UI interaction tools for iOS device control.
|
|
4
4
|
|
5
5
|
import re
|
6
6
|
import time
|
7
|
-
import asyncio
|
8
7
|
from typing import Optional, Dict, Tuple, List, Any
|
9
8
|
import logging
|
10
|
-
import
|
9
|
+
import requests
|
11
10
|
from droidrun.tools.tools import Tools
|
12
11
|
|
13
12
|
logger = logging.getLogger("IOS")
|
@@ -59,43 +58,39 @@ class IOSTools(Tools):
|
|
59
58
|
self.bundle_identifiers = bundle_identifiers
|
60
59
|
logger.info(f"iOS device URL: {url}")
|
61
60
|
|
62
|
-
|
63
|
-
self, serial: Optional[str] = None
|
64
|
-
) -> List[Dict[str, Any]]:
|
61
|
+
def get_state(self) -> List[Dict[str, Any]]:
|
65
62
|
"""
|
66
63
|
Get all clickable UI elements from the iOS device using accessibility API.
|
67
64
|
|
68
|
-
Args:
|
69
|
-
serial: Optional device URL (not used for iOS, uses instance URL)
|
70
|
-
|
71
65
|
Returns:
|
72
66
|
List of dictionaries containing UI elements extracted from the device screen
|
73
67
|
"""
|
74
68
|
try:
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
69
|
+
a11y_url = f"{self.url}/vision/a11y"
|
70
|
+
response = requests.get(a11y_url)
|
71
|
+
|
72
|
+
if response.status_code == 200:
|
73
|
+
a11y_data = response.json()
|
74
|
+
|
75
|
+
# Parse the iOS accessibility tree format
|
76
|
+
elements = self._parse_ios_accessibility_tree(
|
77
|
+
a11y_data["accessibilityTree"]
|
78
|
+
)
|
79
|
+
|
80
|
+
# Cache the elements for tap_by_index usage
|
81
|
+
self.clickable_elements_cache = elements
|
82
|
+
|
83
|
+
return {
|
84
|
+
"a11y_tree": self.clickable_elements_cache,
|
85
|
+
"phone_state": self._get_phone_state(),
|
86
|
+
}
|
87
|
+
else:
|
88
|
+
logger.error(
|
89
|
+
f"Failed to get accessibility data: HTTP {response.status_code}"
|
90
|
+
)
|
91
|
+
raise ValueError(
|
92
|
+
f"Failed to get accessibility data: HTTP {response.status_code}"
|
93
|
+
)
|
99
94
|
|
100
95
|
except Exception as e:
|
101
96
|
logger.error(f"Error getting clickable elements: {e}")
|
@@ -208,7 +203,7 @@ class IOSTools(Tools):
|
|
208
203
|
|
209
204
|
return elements
|
210
205
|
|
211
|
-
|
206
|
+
def tap_by_index(self, index: int) -> str:
|
212
207
|
"""
|
213
208
|
Tap on a UI element by its index.
|
214
209
|
|
@@ -275,55 +270,51 @@ class IOSTools(Tools):
|
|
275
270
|
self.last_tapped_rect = f"{x},{y},{width},{height}"
|
276
271
|
|
277
272
|
# Make the tap request
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
return " | ".join(response_parts)
|
301
|
-
else:
|
302
|
-
return f"Error: Failed to tap element. HTTP {response.status}"
|
273
|
+
tap_url = f"{self.url}/gestures/tap"
|
274
|
+
payload = {"rect": ios_rect, "count": 1, "longPress": False}
|
275
|
+
|
276
|
+
logger.info(f"payload {payload}")
|
277
|
+
|
278
|
+
response = requests.post(tap_url, json=payload)
|
279
|
+
if response.status_code == 200:
|
280
|
+
# Add a small delay to allow UI to update
|
281
|
+
time.sleep(0.5)
|
282
|
+
|
283
|
+
# Create a descriptive response
|
284
|
+
response_parts = []
|
285
|
+
response_parts.append(f"Tapped element with index {index}")
|
286
|
+
response_parts.append(f"Text: '{element.get('text', 'No text')}'")
|
287
|
+
response_parts.append(
|
288
|
+
f"Class: {element.get('className', 'Unknown class')}"
|
289
|
+
)
|
290
|
+
response_parts.append(f"Rect: {ios_rect}")
|
291
|
+
|
292
|
+
return " | ".join(response_parts)
|
293
|
+
else:
|
294
|
+
return f"Error: Failed to tap element. HTTP {response.status_code}"
|
303
295
|
|
304
296
|
except Exception as e:
|
305
297
|
return f"Error: {str(e)}"
|
306
298
|
|
307
|
-
"""
|
299
|
+
"""def tap_by_coordinates(self, x: int, y: int) -> bool:
|
308
300
|
# Format rect in iOS format: {{x,y},{w,h}}
|
309
301
|
width = 1
|
310
302
|
height = 1
|
311
303
|
ios_rect = f"{{{{{x},{y}}},{{{width},{height}}}}}"
|
312
304
|
|
313
305
|
# Make the tap request
|
314
|
-
|
315
|
-
|
316
|
-
payload = {"rect": ios_rect, "count": 1, "longPress": False}
|
306
|
+
tap_url = f"{self.url}/gestures/tap"
|
307
|
+
payload = {"rect": ios_rect, "count": 1, "longPress": False}
|
317
308
|
|
318
|
-
|
309
|
+
logger.info(f"payload {payload}")
|
319
310
|
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
311
|
+
response = requests.post(tap_url, json=payload)
|
312
|
+
if response.status_code == 200:
|
313
|
+
return True
|
314
|
+
else:
|
315
|
+
return False"""
|
325
316
|
|
326
|
-
|
317
|
+
def tap(self, index: int) -> str:
|
327
318
|
"""
|
328
319
|
Tap on a UI element by its index.
|
329
320
|
|
@@ -336,9 +327,9 @@ class IOSTools(Tools):
|
|
336
327
|
Returns:
|
337
328
|
Result message
|
338
329
|
"""
|
339
|
-
return
|
330
|
+
return self.tap_by_index(index)
|
340
331
|
|
341
|
-
|
332
|
+
def swipe(
|
342
333
|
self, start_x: int, start_y: int, end_x: int, end_y: int, duration_ms: int = 300
|
343
334
|
) -> bool:
|
344
335
|
"""
|
@@ -364,31 +355,47 @@ class IOSTools(Tools):
|
|
364
355
|
else:
|
365
356
|
direction = "down" if dy > 0 else "up"
|
366
357
|
|
367
|
-
|
368
|
-
|
369
|
-
payload = {"x": float(start_x), "y": float(start_y), "dir": direction}
|
358
|
+
swipe_url = f"{self.url}/gestures/swipe"
|
359
|
+
payload = {"x": float(start_x), "y": float(start_y), "dir": direction}
|
370
360
|
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
361
|
+
response = requests.post(swipe_url, json=payload)
|
362
|
+
if response.status_code == 200:
|
363
|
+
logger.info(
|
364
|
+
f"Swiped from ({start_x}, {start_y}) to ({end_x}, {end_y}) direction: {direction}"
|
365
|
+
)
|
366
|
+
return True
|
367
|
+
else:
|
368
|
+
logger.error(f"Failed to swipe: HTTP {response.status_code}")
|
369
|
+
return False
|
380
370
|
|
381
371
|
except Exception as e:
|
382
372
|
logger.error(f"Error performing swipe: {e}")
|
383
373
|
return False
|
384
374
|
|
385
|
-
|
375
|
+
def drag(
|
376
|
+
self, start_x: int, start_y: int, end_x: int, end_y: int, duration_ms: int = 3000
|
377
|
+
) -> bool:
|
378
|
+
"""
|
379
|
+
Drag from the given start coordinates to the given end coordinates.
|
380
|
+
Args:
|
381
|
+
start_x: Starting X coordinate
|
382
|
+
start_y: Starting Y coordinate
|
383
|
+
end_x: Ending X coordinate
|
384
|
+
end_y: Ending Y coordinate
|
385
|
+
duration_ms: Duration of swipe in milliseconds
|
386
|
+
Returns:
|
387
|
+
Bool indicating success or failure
|
388
|
+
"""
|
389
|
+
# TODO: implement this
|
390
|
+
logger.info(f"Drag action FAILED! Not implemented for iOS")
|
391
|
+
return False
|
392
|
+
|
393
|
+
def input_text(self, text: str) -> str:
|
386
394
|
"""
|
387
395
|
Input text on the iOS device.
|
388
396
|
|
389
397
|
Args:
|
390
398
|
text: Text to input. Can contain spaces, newlines, and special characters including non-ASCII.
|
391
|
-
serial: Optional device serial (not used for iOS, uses instance URL)
|
392
399
|
|
393
400
|
Returns:
|
394
401
|
Result message
|
@@ -397,24 +404,23 @@ class IOSTools(Tools):
|
|
397
404
|
# Use the last tapped element's rect if available, otherwise use a default
|
398
405
|
rect = self.last_tapped_rect if self.last_tapped_rect else "0,0,100,100"
|
399
406
|
|
400
|
-
|
401
|
-
|
402
|
-
payload = {"rect": rect, "text": text}
|
407
|
+
type_url = f"{self.url}/inputs/type"
|
408
|
+
payload = {"rect": rect, "text": text}
|
403
409
|
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
+
response = requests.post(type_url, json=payload)
|
411
|
+
if response.status_code == 200:
|
412
|
+
time.sleep(0.5) # Wait for text input to complete
|
413
|
+
return f"Text input completed: {text[:50]}{'...' if len(text) > 50 else ''}"
|
414
|
+
else:
|
415
|
+
return f"Error: Failed to input text. HTTP {response.status_code}"
|
410
416
|
|
411
417
|
except Exception as e:
|
412
418
|
return f"Error sending text input: {str(e)}"
|
413
419
|
|
414
|
-
|
420
|
+
def back(self) -> str:
|
415
421
|
raise NotImplementedError("Back is not yet implemented for iOS")
|
416
422
|
|
417
|
-
|
423
|
+
def press_key(self, keycode: int) -> str:
|
418
424
|
# TODO: refactor this. its not about physical keys but BACK, ENTER, DELETE, etc.
|
419
425
|
"""
|
420
426
|
Press a key on the iOS device.
|
@@ -431,20 +437,19 @@ class IOSTools(Tools):
|
|
431
437
|
key_names = {0: "HOME", 4: "ACTION", 5: "CAMERA"}
|
432
438
|
key_name = key_names.get(keycode, str(keycode))
|
433
439
|
|
434
|
-
|
435
|
-
|
436
|
-
payload = {"key": keycode}
|
440
|
+
key_url = f"{self.url}/inputs/key"
|
441
|
+
payload = {"key": keycode}
|
437
442
|
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
+
response = requests.post(key_url, json=payload)
|
444
|
+
if response.status_code == 200:
|
445
|
+
return f"Pressed key {key_name}"
|
446
|
+
else:
|
447
|
+
return f"Error: Failed to press key. HTTP {response.status_code}"
|
443
448
|
|
444
449
|
except Exception as e:
|
445
450
|
return f"Error pressing key: {str(e)}"
|
446
451
|
|
447
|
-
|
452
|
+
def start_app(self, package: str, activity: str = "") -> str:
|
448
453
|
"""
|
449
454
|
Start an app on the iOS device.
|
450
455
|
|
@@ -453,97 +458,92 @@ class IOSTools(Tools):
|
|
453
458
|
activity: Optional activity name (not used on iOS)
|
454
459
|
"""
|
455
460
|
try:
|
456
|
-
|
457
|
-
|
458
|
-
payload = {"bundleIdentifier": package}
|
461
|
+
launch_url = f"{self.url}/inputs/launch"
|
462
|
+
payload = {"bundleIdentifier": package}
|
459
463
|
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
464
|
+
response = requests.post(launch_url, json=payload)
|
465
|
+
if response.status_code == 200:
|
466
|
+
time.sleep(1.0) # Wait for app to launch
|
467
|
+
return f"Successfully launched app: {package}"
|
468
|
+
else:
|
469
|
+
return f"Error: Failed to launch app {package}. HTTP {response.status_code}"
|
466
470
|
|
467
471
|
except Exception as e:
|
468
472
|
return f"Error launching app: {str(e)}"
|
469
473
|
|
470
|
-
|
474
|
+
def take_screenshot(self) -> Tuple[str, bytes]:
|
471
475
|
"""
|
472
476
|
Take a screenshot of the iOS device.
|
473
477
|
This function captures the current screen and adds the screenshot to context in the next message.
|
474
478
|
Also stores the screenshot in the screenshots list with timestamp for later GIF creation.
|
475
479
|
"""
|
476
480
|
try:
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
481
|
+
screenshot_url = f"{self.url}/vision/screenshot"
|
482
|
+
response = requests.get(screenshot_url)
|
483
|
+
|
484
|
+
if response.status_code == 200:
|
485
|
+
screenshot_data = response.content
|
486
|
+
|
487
|
+
# Store screenshot with timestamp
|
488
|
+
screenshot_info = {
|
489
|
+
"timestamp": time.time(),
|
490
|
+
"data": screenshot_data,
|
491
|
+
}
|
492
|
+
self.screenshots.append(screenshot_info)
|
493
|
+
self.last_screenshot = screenshot_data
|
494
|
+
|
495
|
+
logger.info(
|
496
|
+
f"Screenshot captured successfully, size: {len(screenshot_data)} bytes"
|
497
|
+
)
|
498
|
+
return ("PNG", screenshot_data)
|
499
|
+
else:
|
500
|
+
logger.error(
|
501
|
+
f"Failed to capture screenshot: HTTP {response.status_code}"
|
502
|
+
)
|
503
|
+
raise ValueError(
|
504
|
+
f"Failed to capture screenshot: HTTP {response.status_code}"
|
505
|
+
)
|
502
506
|
|
503
507
|
except Exception as e:
|
504
508
|
logger.error(f"Error capturing screenshot: {e}")
|
505
509
|
raise ValueError(f"Error taking screenshot: {str(e)}")
|
506
510
|
|
507
|
-
|
511
|
+
def _get_phone_state(self) -> Dict[str, Any]:
|
508
512
|
"""
|
509
513
|
Get the current phone state including current activity and keyboard visibility.
|
510
514
|
|
511
|
-
Args:
|
512
|
-
serial: Optional device serial number (not used for iOS)
|
513
|
-
|
514
515
|
Returns:
|
515
516
|
Dictionary with current phone state information
|
516
517
|
"""
|
517
518
|
try:
|
518
519
|
# For iOS, we can get some state info from the accessibility API
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
520
|
+
a11y_url = f"{self.url}/vision/state"
|
521
|
+
response = requests.get(a11y_url)
|
522
|
+
|
523
|
+
if response.status_code == 200:
|
524
|
+
state_data = response.json()
|
525
|
+
|
526
|
+
return {
|
527
|
+
"current_activity": state_data["activity"],
|
528
|
+
"keyboard_shown": state_data["keyboardShown"],
|
529
|
+
}
|
530
|
+
else:
|
531
|
+
return {
|
532
|
+
"error": f"Failed to get device state: HTTP {response.status_code}",
|
533
|
+
"current_activity": "Unknown",
|
534
|
+
"keyboard_shown": False,
|
535
|
+
}
|
535
536
|
|
536
537
|
except Exception as e:
|
537
538
|
return {"error": str(e), "message": f"Error getting phone state: {str(e)}"}
|
538
539
|
|
539
|
-
|
540
|
+
def list_packages(self, include_system_apps: bool = True) -> List[str]:
|
540
541
|
all_packages = set(self.bundle_identifiers)
|
541
542
|
if include_system_apps:
|
542
543
|
all_packages.update(SYSTEM_BUNDLE_IDENTIFIERS)
|
543
544
|
return sorted(list(all_packages))
|
544
545
|
|
545
|
-
|
546
|
-
async def remember(self, information: str) -> str:
|
546
|
+
def remember(self, information: str) -> str:
|
547
547
|
"""
|
548
548
|
Store important information to remember for future context.
|
549
549
|
|
droidrun/tools/tools.py
CHANGED
@@ -14,14 +14,14 @@ class Tools(ABC):
|
|
14
14
|
"""
|
15
15
|
|
16
16
|
@abstractmethod
|
17
|
-
|
17
|
+
def get_state(self) -> Dict[str, Any]:
|
18
18
|
"""
|
19
19
|
Get the current state of the tool.
|
20
20
|
"""
|
21
21
|
pass
|
22
22
|
|
23
23
|
@abstractmethod
|
24
|
-
|
24
|
+
def tap_by_index(self, index: int) -> str:
|
25
25
|
"""
|
26
26
|
Tap the element at the given index.
|
27
27
|
"""
|
@@ -32,7 +32,7 @@ class Tools(ABC):
|
|
32
32
|
# pass
|
33
33
|
|
34
34
|
@abstractmethod
|
35
|
-
|
35
|
+
def swipe(
|
36
36
|
self, start_x: int, start_y: int, end_x: int, end_y: int, duration_ms: int = 300
|
37
37
|
) -> bool:
|
38
38
|
"""
|
@@ -41,86 +41,98 @@ class Tools(ABC):
|
|
41
41
|
pass
|
42
42
|
|
43
43
|
@abstractmethod
|
44
|
-
|
44
|
+
def drag(
|
45
|
+
self, start_x: int, start_y: int, end_x: int, end_y: int, duration_ms: int = 3000
|
46
|
+
) -> bool:
|
47
|
+
"""
|
48
|
+
Drag from the given start coordinates to the given end coordinates.
|
49
|
+
"""
|
50
|
+
pass
|
51
|
+
|
52
|
+
@abstractmethod
|
53
|
+
def input_text(self, text: str) -> str:
|
45
54
|
"""
|
46
55
|
Input the given text into a focused input field.
|
47
56
|
"""
|
48
57
|
pass
|
49
58
|
|
50
59
|
@abstractmethod
|
51
|
-
|
60
|
+
def back(self) -> str:
|
52
61
|
"""
|
53
62
|
Press the back button.
|
54
63
|
"""
|
55
64
|
pass
|
56
65
|
|
57
66
|
@abstractmethod
|
58
|
-
|
67
|
+
def press_key(self, keycode: int) -> str:
|
59
68
|
"""
|
60
69
|
Enter the given keycode.
|
61
70
|
"""
|
62
71
|
pass
|
63
72
|
|
64
73
|
@abstractmethod
|
65
|
-
|
74
|
+
def start_app(self, package: str, activity: str = "") -> str:
|
66
75
|
"""
|
67
76
|
Start the given app.
|
68
77
|
"""
|
69
78
|
pass
|
70
79
|
|
71
80
|
@abstractmethod
|
72
|
-
|
81
|
+
def take_screenshot(self) -> Tuple[str, bytes]:
|
73
82
|
"""
|
74
83
|
Take a screenshot of the device.
|
75
84
|
"""
|
76
85
|
pass
|
77
86
|
|
78
87
|
@abstractmethod
|
79
|
-
|
88
|
+
def list_packages(self, include_system_apps: bool = False) -> List[str]:
|
80
89
|
"""
|
81
90
|
List all packages on the device.
|
82
91
|
"""
|
83
92
|
pass
|
84
93
|
|
85
94
|
@abstractmethod
|
86
|
-
|
95
|
+
def remember(self, information: str) -> str:
|
87
96
|
"""
|
88
97
|
Remember the given information. This is used to store information in the tool's memory.
|
89
98
|
"""
|
90
99
|
pass
|
91
100
|
|
92
101
|
@abstractmethod
|
93
|
-
|
102
|
+
def get_memory(self) -> List[str]:
|
94
103
|
"""
|
95
104
|
Get the memory of the tool.
|
96
105
|
"""
|
97
106
|
pass
|
98
107
|
|
99
108
|
@abstractmethod
|
100
|
-
def complete(self, success: bool, reason: str = "") ->
|
109
|
+
def complete(self, success: bool, reason: str = "") -> None:
|
101
110
|
"""
|
102
111
|
Complete the tool. This is used to indicate that the tool has completed its task.
|
103
112
|
"""
|
104
113
|
pass
|
105
114
|
|
106
115
|
|
107
|
-
def describe_tools(tools: Tools) -> Dict[str, Callable[..., Any]]:
|
116
|
+
def describe_tools(tools: Tools, exclude_tools: Optional[List[str]] = None) -> Dict[str, Callable[..., Any]]:
|
108
117
|
"""
|
109
118
|
Describe the tools available for the given Tools instance.
|
110
119
|
|
111
120
|
Args:
|
112
121
|
tools: The Tools instance to describe.
|
122
|
+
exclude_tools: List of tool names to exclude from the description.
|
113
123
|
|
114
124
|
Returns:
|
115
125
|
A dictionary mapping tool names to their descriptions.
|
116
126
|
"""
|
127
|
+
exclude_tools = exclude_tools or []
|
117
128
|
|
118
|
-
|
129
|
+
description = {
|
119
130
|
# UI interaction
|
120
131
|
"swipe": tools.swipe,
|
121
132
|
"input_text": tools.input_text,
|
122
133
|
"press_key": tools.press_key,
|
123
134
|
"tap_by_index": tools.tap_by_index,
|
135
|
+
"drag": tools.drag,
|
124
136
|
# App management
|
125
137
|
"start_app": tools.start_app,
|
126
138
|
"list_packages": tools.list_packages,
|
@@ -128,3 +140,9 @@ def describe_tools(tools: Tools) -> Dict[str, Callable[..., Any]]:
|
|
128
140
|
"remember": tools.remember,
|
129
141
|
"complete": tools.complete,
|
130
142
|
}
|
143
|
+
|
144
|
+
# Remove excluded tools
|
145
|
+
for tool_name in exclude_tools:
|
146
|
+
description.pop(tool_name, None)
|
147
|
+
|
148
|
+
return description
|