droidrun 0.3.2__py3-none-any.whl → 0.3.4__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/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 aiohttp
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
- async def get_state(
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
- async with aiohttp.ClientSession() as session:
76
- a11y_url = f"{self.url}/vision/a11y"
77
- async with session.get(a11y_url) as response:
78
- if response.status == 200:
79
- a11y_data = await response.json()
80
-
81
- # Parse the iOS accessibility tree format
82
- elements = self._parse_ios_accessibility_tree(
83
- a11y_data["accessibilityTree"]
84
- )
85
-
86
- # Cache the elements for tap_by_index usage
87
- self.clickable_elements_cache = elements
88
-
89
- return {
90
- "a11y_tree":self.clickable_elements_cache
91
- }
92
- else:
93
- logger.error(
94
- f"Failed to get accessibility data: HTTP {response.status}"
95
- )
96
- raise ValueError(
97
- f"Failed to get accessibility data: HTTP {response.status}"
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
- async def tap_by_index(self, index: int, serial: Optional[str] = None) -> str:
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
- async with aiohttp.ClientSession() as session:
279
- tap_url = f"{self.url}/gestures/tap"
280
- payload = {"rect": ios_rect, "count": 1, "longPress": False}
281
-
282
- logger.info(f"payload {payload}")
283
-
284
- async with session.post(tap_url, json=payload) as response:
285
- if response.status == 200:
286
- # Add a small delay to allow UI to update
287
- await asyncio.sleep(0.5)
288
-
289
- # Create a descriptive response
290
- response_parts = []
291
- response_parts.append(f"Tapped element with index {index}")
292
- response_parts.append(
293
- f"Text: '{element.get('text', 'No text')}'"
294
- )
295
- response_parts.append(
296
- f"Class: {element.get('className', 'Unknown class')}"
297
- )
298
- response_parts.append(f"Rect: {ios_rect}")
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
- """async def tap_by_coordinates(self, x: int, y: int) -> bool:
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
- async with aiohttp.ClientSession() as session:
315
- tap_url = f"{self.url}/gestures/tap"
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
- logger.info(f"payload {payload}")
309
+ logger.info(f"payload {payload}")
319
310
 
320
- async with session.post(tap_url, json=payload) as response:
321
- if response.status == 200:
322
- return True
323
- else:
324
- return False"""
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
- async def tap(self, index: int) -> str:
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 await self.tap_by_index(index)
330
+ return self.tap_by_index(index)
340
331
 
341
- async def swipe(
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
- async with aiohttp.ClientSession() as session:
368
- swipe_url = f"{self.url}/gestures/swipe"
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
- async with session.post(swipe_url, json=payload) as response:
372
- if response.status == 200:
373
- logger.info(
374
- f"Swiped from ({start_x}, {start_y}) to ({end_x}, {end_y}) direction: {direction}"
375
- )
376
- return True
377
- else:
378
- logger.error(f"Failed to swipe: HTTP {response.status}")
379
- return False
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
- async def input_text(self, text: str, serial: Optional[str] = None) -> str:
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
- async with aiohttp.ClientSession() as session:
401
- type_url = f"{self.url}/inputs/type"
402
- payload = {"rect": rect, "text": text}
407
+ type_url = f"{self.url}/inputs/type"
408
+ payload = {"rect": rect, "text": text}
403
409
 
404
- async with session.post(type_url, json=payload) as response:
405
- if response.status == 200:
406
- await asyncio.sleep(0.5) # Wait for text input to complete
407
- return f"Text input completed: {text[:50]}{'...' if len(text) > 50 else ''}"
408
- else:
409
- return f"Error: Failed to input text. HTTP {response.status}"
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
- async def back(self) -> str:
420
+ def back(self) -> str:
415
421
  raise NotImplementedError("Back is not yet implemented for iOS")
416
422
 
417
- async def press_key(self, keycode: int) -> str:
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
- async with aiohttp.ClientSession() as session:
435
- key_url = f"{self.url}/inputs/key"
436
- payload = {"key": keycode}
440
+ key_url = f"{self.url}/inputs/key"
441
+ payload = {"key": keycode}
437
442
 
438
- async with session.post(key_url, json=payload) as response:
439
- if response.status == 200:
440
- return f"Pressed key {key_name}"
441
- else:
442
- return f"Error: Failed to press key. HTTP {response.status}"
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
- async def start_app(self, package: str, activity: str = "") -> str:
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
- async with aiohttp.ClientSession() as session:
457
- launch_url = f"{self.url}/inputs/launch"
458
- payload = {"bundleIdentifier": package}
461
+ launch_url = f"{self.url}/inputs/launch"
462
+ payload = {"bundleIdentifier": package}
459
463
 
460
- async with session.post(launch_url, json=payload) as response:
461
- if response.status == 200:
462
- await asyncio.sleep(1.0) # Wait for app to launch
463
- return f"Successfully launched app: {package}"
464
- else:
465
- return f"Error: Failed to launch app {package}. HTTP {response.status}"
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
- async def take_screenshot(self) -> Tuple[str, bytes]:
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
- async with aiohttp.ClientSession() as session:
478
- screenshot_url = f"{self.url}/vision/screenshot"
479
- async with session.get(screenshot_url) as response:
480
- if response.status == 200:
481
- screenshot_data = await response.read()
482
-
483
- # Store screenshot with timestamp
484
- screenshot_info = {
485
- "timestamp": time.time(),
486
- "data": screenshot_data,
487
- }
488
- self.screenshots.append(screenshot_info)
489
- self.last_screenshot = screenshot_data
490
-
491
- logger.info(
492
- f"Screenshot captured successfully, size: {len(screenshot_data)} bytes"
493
- )
494
- return ("PNG", screenshot_data)
495
- else:
496
- logger.error(
497
- f"Failed to capture screenshot: HTTP {response.status}"
498
- )
499
- raise ValueError(
500
- f"Failed to capture screenshot: HTTP {response.status}"
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
- async def get_phone_state(self, serial: Optional[str] = None) -> Dict[str, Any]:
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
- async with aiohttp.ClientSession() as session:
520
- a11y_url = f"{self.url}/vision/state"
521
- async with session.get(a11y_url) as response:
522
- if response.status == 200:
523
- state_data = await response.json()
524
-
525
- return {
526
- "current_activity": state_data["activity"],
527
- "keyboard_shown": state_data["keyboardShown"],
528
- }
529
- else:
530
- return {
531
- "error": f"Failed to get device state: HTTP {response.status}",
532
- "current_activity": "Unknown",
533
- "keyboard_shown": False,
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
- async def list_packages(self, include_system_apps: bool = True) -> List[str]:
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
@@ -2,6 +2,8 @@ from abc import ABC, abstractmethod
2
2
  from typing import List, Optional, Dict, Any
3
3
  import logging
4
4
  from typing import Tuple, Dict, Callable, Any, Optional
5
+ from functools import wraps
6
+ import sys
5
7
 
6
8
  # Get a logger for this module
7
9
  logger = logging.getLogger(__name__)
@@ -13,15 +15,41 @@ class Tools(ABC):
13
15
  This class provides a common interface for all tools to implement.
14
16
  """
15
17
 
18
+ @staticmethod
19
+ def ui_action(func):
20
+ """"
21
+ Decorator to capture screenshots and UI states for actions that modify the UI.
22
+ """
23
+ @wraps(func)
24
+ def wrapper(*args, **kwargs):
25
+ self = args[0]
26
+ result = func(*args, **kwargs)
27
+
28
+ # Check if save_trajectories attribute exists and is set to "action"
29
+ if hasattr(self, 'save_trajectories') and self.save_trajectories == "action":
30
+ frame = sys._getframe(1)
31
+ caller_globals = frame.f_globals
32
+
33
+ step_screenshots = caller_globals.get('step_screenshots')
34
+ step_ui_states = caller_globals.get('step_ui_states')
35
+
36
+ if step_screenshots is not None:
37
+ step_screenshots.append(self.take_screenshot()[1])
38
+ if step_ui_states is not None:
39
+ step_ui_states.append(self.get_state())
40
+
41
+ return result
42
+ return wrapper
43
+
16
44
  @abstractmethod
17
- async def get_state(self) -> Dict[str, Any]:
45
+ def get_state(self) -> Dict[str, Any]:
18
46
  """
19
47
  Get the current state of the tool.
20
48
  """
21
49
  pass
22
50
 
23
51
  @abstractmethod
24
- async def tap_by_index(self, index: int) -> bool:
52
+ def tap_by_index(self, index: int) -> str:
25
53
  """
26
54
  Tap the element at the given index.
27
55
  """
@@ -32,7 +60,7 @@ class Tools(ABC):
32
60
  # pass
33
61
 
34
62
  @abstractmethod
35
- async def swipe(
63
+ def swipe(
36
64
  self, start_x: int, start_y: int, end_x: int, end_y: int, duration_ms: int = 300
37
65
  ) -> bool:
38
66
  """
@@ -41,86 +69,98 @@ class Tools(ABC):
41
69
  pass
42
70
 
43
71
  @abstractmethod
44
- async def input_text(self, text: str) -> bool:
72
+ def drag(
73
+ self, start_x: int, start_y: int, end_x: int, end_y: int, duration_ms: int = 3000
74
+ ) -> bool:
75
+ """
76
+ Drag from the given start coordinates to the given end coordinates.
77
+ """
78
+ pass
79
+
80
+ @abstractmethod
81
+ def input_text(self, text: str) -> str:
45
82
  """
46
83
  Input the given text into a focused input field.
47
84
  """
48
85
  pass
49
86
 
50
87
  @abstractmethod
51
- async def back(self) -> bool:
88
+ def back(self) -> str:
52
89
  """
53
90
  Press the back button.
54
91
  """
55
92
  pass
56
93
 
57
94
  @abstractmethod
58
- async def press_key(self, keycode: int) -> bool:
95
+ def press_key(self, keycode: int) -> str:
59
96
  """
60
97
  Enter the given keycode.
61
98
  """
62
99
  pass
63
100
 
64
101
  @abstractmethod
65
- async def start_app(self, package: str, activity: str = "") -> bool:
102
+ def start_app(self, package: str, activity: str = "") -> str:
66
103
  """
67
104
  Start the given app.
68
105
  """
69
106
  pass
70
107
 
71
108
  @abstractmethod
72
- async def take_screenshot(self) -> Tuple[str, bytes]:
109
+ def take_screenshot(self) -> Tuple[str, bytes]:
73
110
  """
74
111
  Take a screenshot of the device.
75
112
  """
76
113
  pass
77
114
 
78
115
  @abstractmethod
79
- async def list_packages(self, include_system_apps: bool = False) -> List[str]:
116
+ def list_packages(self, include_system_apps: bool = False) -> List[str]:
80
117
  """
81
118
  List all packages on the device.
82
119
  """
83
120
  pass
84
121
 
85
122
  @abstractmethod
86
- async def remember(self, information: str) -> str:
123
+ def remember(self, information: str) -> str:
87
124
  """
88
125
  Remember the given information. This is used to store information in the tool's memory.
89
126
  """
90
127
  pass
91
128
 
92
129
  @abstractmethod
93
- async def get_memory(self) -> List[str]:
130
+ def get_memory(self) -> List[str]:
94
131
  """
95
132
  Get the memory of the tool.
96
133
  """
97
134
  pass
98
135
 
99
136
  @abstractmethod
100
- def complete(self, success: bool, reason: str = "") -> bool:
137
+ def complete(self, success: bool, reason: str = "") -> None:
101
138
  """
102
139
  Complete the tool. This is used to indicate that the tool has completed its task.
103
140
  """
104
141
  pass
105
142
 
106
143
 
107
- def describe_tools(tools: Tools) -> Dict[str, Callable[..., Any]]:
144
+ def describe_tools(tools: Tools, exclude_tools: Optional[List[str]] = None) -> Dict[str, Callable[..., Any]]:
108
145
  """
109
146
  Describe the tools available for the given Tools instance.
110
147
 
111
148
  Args:
112
149
  tools: The Tools instance to describe.
150
+ exclude_tools: List of tool names to exclude from the description.
113
151
 
114
152
  Returns:
115
153
  A dictionary mapping tool names to their descriptions.
116
154
  """
155
+ exclude_tools = exclude_tools or []
117
156
 
118
- return {
157
+ description = {
119
158
  # UI interaction
120
159
  "swipe": tools.swipe,
121
160
  "input_text": tools.input_text,
122
161
  "press_key": tools.press_key,
123
162
  "tap_by_index": tools.tap_by_index,
163
+ "drag": tools.drag,
124
164
  # App management
125
165
  "start_app": tools.start_app,
126
166
  "list_packages": tools.list_packages,
@@ -128,3 +168,9 @@ def describe_tools(tools: Tools) -> Dict[str, Callable[..., Any]]:
128
168
  "remember": tools.remember,
129
169
  "complete": tools.complete,
130
170
  }
171
+
172
+ # Remove excluded tools
173
+ for tool_name in exclude_tools:
174
+ description.pop(tool_name, None)
175
+
176
+ return description