droidrun 0.3.8__py3-none-any.whl → 0.3.10.dev2__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 (74) hide show
  1. droidrun/__init__.py +2 -3
  2. droidrun/__main__.py +1 -1
  3. droidrun/agent/__init__.py +1 -1
  4. droidrun/agent/codeact/__init__.py +1 -4
  5. droidrun/agent/codeact/codeact_agent.py +112 -48
  6. droidrun/agent/codeact/events.py +6 -3
  7. droidrun/agent/codeact/prompts.py +2 -2
  8. droidrun/agent/common/constants.py +2 -0
  9. droidrun/agent/common/events.py +5 -3
  10. droidrun/agent/context/__init__.py +1 -3
  11. droidrun/agent/context/agent_persona.py +2 -1
  12. droidrun/agent/context/context_injection_manager.py +6 -6
  13. droidrun/agent/context/episodic_memory.py +5 -3
  14. droidrun/agent/context/personas/__init__.py +3 -3
  15. droidrun/agent/context/personas/app_starter.py +3 -3
  16. droidrun/agent/context/personas/big_agent.py +3 -3
  17. droidrun/agent/context/personas/default.py +3 -3
  18. droidrun/agent/context/personas/ui_expert.py +5 -5
  19. droidrun/agent/context/task_manager.py +15 -17
  20. droidrun/agent/droid/__init__.py +1 -1
  21. droidrun/agent/droid/droid_agent.py +327 -182
  22. droidrun/agent/droid/events.py +91 -9
  23. droidrun/agent/executor/__init__.py +13 -0
  24. droidrun/agent/executor/events.py +24 -0
  25. droidrun/agent/executor/executor_agent.py +327 -0
  26. droidrun/agent/executor/prompts.py +136 -0
  27. droidrun/agent/manager/__init__.py +18 -0
  28. droidrun/agent/manager/events.py +20 -0
  29. droidrun/agent/manager/manager_agent.py +459 -0
  30. droidrun/agent/manager/prompts.py +223 -0
  31. droidrun/agent/oneflows/app_starter_workflow.py +118 -0
  32. droidrun/agent/oneflows/text_manipulator.py +204 -0
  33. droidrun/agent/planner/__init__.py +3 -3
  34. droidrun/agent/planner/events.py +6 -3
  35. droidrun/agent/planner/planner_agent.py +60 -53
  36. droidrun/agent/planner/prompts.py +2 -2
  37. droidrun/agent/usage.py +15 -13
  38. droidrun/agent/utils/__init__.py +11 -1
  39. droidrun/agent/utils/async_utils.py +2 -1
  40. droidrun/agent/utils/chat_utils.py +48 -60
  41. droidrun/agent/utils/device_state_formatter.py +177 -0
  42. droidrun/agent/utils/executer.py +13 -12
  43. droidrun/agent/utils/inference.py +114 -0
  44. droidrun/agent/utils/llm_picker.py +2 -0
  45. droidrun/agent/utils/message_utils.py +85 -0
  46. droidrun/agent/utils/tools.py +220 -0
  47. droidrun/agent/utils/trajectory.py +8 -7
  48. droidrun/cli/__init__.py +1 -1
  49. droidrun/cli/logs.py +29 -28
  50. droidrun/cli/main.py +279 -143
  51. droidrun/config_manager/__init__.py +25 -0
  52. droidrun/config_manager/config_manager.py +583 -0
  53. droidrun/macro/__init__.py +2 -2
  54. droidrun/macro/__main__.py +1 -1
  55. droidrun/macro/cli.py +36 -34
  56. droidrun/macro/replay.py +7 -9
  57. droidrun/portal.py +1 -1
  58. droidrun/telemetry/__init__.py +2 -2
  59. droidrun/telemetry/events.py +3 -4
  60. droidrun/telemetry/phoenix.py +173 -0
  61. droidrun/telemetry/tracker.py +7 -5
  62. droidrun/tools/__init__.py +1 -1
  63. droidrun/tools/adb.py +210 -82
  64. droidrun/tools/ios.py +7 -5
  65. droidrun/tools/tools.py +25 -8
  66. {droidrun-0.3.8.dist-info → droidrun-0.3.10.dev2.dist-info}/METADATA +13 -7
  67. droidrun-0.3.10.dev2.dist-info/RECORD +70 -0
  68. droidrun/agent/common/default.py +0 -5
  69. droidrun/agent/context/reflection.py +0 -20
  70. droidrun/agent/oneflows/reflector.py +0 -265
  71. droidrun-0.3.8.dist-info/RECORD +0 -55
  72. {droidrun-0.3.8.dist-info → droidrun-0.3.10.dev2.dist-info}/WHEEL +0 -0
  73. {droidrun-0.3.8.dist-info → droidrun-0.3.10.dev2.dist-info}/entry_points.txt +0 -0
  74. {droidrun-0.3.8.dist-info → droidrun-0.3.10.dev2.dist-info}/licenses/LICENSE +0 -0
droidrun/macro/cli.py CHANGED
@@ -3,15 +3,17 @@ Command-line interface for DroidRun macro replay.
3
3
  """
4
4
 
5
5
  import asyncio
6
- import click
7
6
  import logging
8
7
  import os
9
8
  from typing import Optional
9
+
10
+ import click
11
+ from adbutils import adb
10
12
  from rich.console import Console
11
13
  from rich.table import Table
12
- from droidrun.macro.replay import MacroPlayer, replay_macro_file, replay_macro_folder
14
+
13
15
  from droidrun.agent.utils.trajectory import Trajectory
14
- from adbutils import adb
16
+ from droidrun.macro.replay import MacroPlayer
15
17
 
16
18
  console = Console()
17
19
 
@@ -20,21 +22,21 @@ def configure_logging(debug: bool = False):
20
22
  """Configure logging for the macro CLI."""
21
23
  logger = logging.getLogger("droidrun-macro")
22
24
  logger.handlers = []
23
-
25
+
24
26
  handler = logging.StreamHandler()
25
-
27
+
26
28
  if debug:
27
29
  level = logging.DEBUG
28
30
  formatter = logging.Formatter("%(levelname)s %(name)s %(message)s", "%H:%M:%S")
29
31
  else:
30
32
  level = logging.INFO
31
33
  formatter = logging.Formatter("%(message)s", "%H:%M:%S")
32
-
34
+
33
35
  handler.setFormatter(formatter)
34
36
  logger.addHandler(handler)
35
37
  logger.setLevel(level)
36
38
  logger.propagate = False
37
-
39
+
38
40
  return logger
39
41
 
40
42
 
@@ -55,12 +57,12 @@ def macro_cli():
55
57
  def replay(path: str, device: Optional[str], delay: float, start_from: int, max_steps: Optional[int], debug: bool, dry_run: bool):
56
58
  """Replay a macro from a file or trajectory folder."""
57
59
  logger = configure_logging(debug)
58
-
60
+
59
61
  logger.info("🎬 DroidRun Macro Replay")
60
-
62
+
61
63
  # Convert start_from from 1-based to 0-based
62
64
  start_from_zero = max(0, start_from - 1)
63
-
65
+
64
66
  if device is None:
65
67
  logger.info("🔍 Finding connected device...")
66
68
  devices = adb.list()
@@ -70,7 +72,7 @@ def replay(path: str, device: Optional[str], delay: float, start_from: int, max_
70
72
  logger.info(f"📱 Using device: {device}")
71
73
  else:
72
74
  logger.info(f"📱 Using device: {device}")
73
-
75
+
74
76
  asyncio.run(_replay_async(path, device, delay, start_from_zero, max_steps, dry_run, logger))
75
77
 
76
78
 
@@ -88,40 +90,40 @@ async def _replay_async(path: str, device: str, delay: float, start_from: int, m
88
90
  else:
89
91
  logger.error(f"❌ Invalid path: {path}")
90
92
  return
91
-
93
+
92
94
  if not macro_data:
93
95
  logger.error("❌ Failed to load macro data")
94
96
  return
95
-
97
+
96
98
  # Show macro information
97
99
  description = macro_data.get("description", "No description")
98
100
  total_actions = macro_data.get("total_actions", 0)
99
101
  version = macro_data.get("version", "unknown")
100
-
102
+
101
103
  logger.info("📋 Macro Information:")
102
104
  logger.info(f" Description: {description}")
103
105
  logger.info(f" Version: {version}")
104
106
  logger.info(f" Total actions: {total_actions}")
105
107
  logger.info(f" Device: {device}")
106
108
  logger.info(f" Delay between actions: {delay}s")
107
-
109
+
108
110
  if start_from > 0:
109
111
  logger.info(f" Starting from step: {start_from + 1}")
110
112
  if max_steps:
111
113
  logger.info(f" Maximum steps: {max_steps}")
112
-
114
+
113
115
  if dry_run:
114
116
  logger.info("🔍 DRY RUN MODE - Actions will be shown but not executed")
115
117
  await _show_dry_run(macro_data, start_from, max_steps, logger)
116
118
  else:
117
119
  logger.info("▶️ Starting macro replay...")
118
120
  success = await player.replay_macro(macro_data, start_from_step=start_from, max_steps=max_steps)
119
-
121
+
120
122
  if success:
121
123
  logger.info("🎉 Macro replay completed successfully!")
122
124
  else:
123
125
  logger.error("💥 Macro replay completed with errors")
124
-
126
+
125
127
  except Exception as e:
126
128
  logger.error(f"💥 Error: {e}")
127
129
  if logger.isEnabledFor(logging.DEBUG):
@@ -132,25 +134,25 @@ async def _replay_async(path: str, device: str, delay: float, start_from: int, m
132
134
  async def _show_dry_run(macro_data: dict, start_from: int, max_steps: Optional[int], logger: logging.Logger):
133
135
  """Show what actions would be executed in dry run mode."""
134
136
  actions = macro_data.get("actions", [])
135
-
137
+
136
138
  # Apply filters
137
139
  if start_from > 0:
138
140
  actions = actions[start_from:]
139
141
  if max_steps:
140
142
  actions = actions[:max_steps]
141
-
143
+
142
144
  logger.info(f"📋 Found {len(actions)} actions to execute:")
143
-
145
+
144
146
  table = Table(title="Actions to Execute")
145
147
  table.add_column("Step", style="cyan")
146
148
  table.add_column("Type", style="green")
147
149
  table.add_column("Details", style="white")
148
150
  table.add_column("Description", style="yellow")
149
-
151
+
150
152
  for i, action in enumerate(actions, start=start_from + 1):
151
153
  action_type = action.get("action_type", action.get("type", "unknown"))
152
154
  details = ""
153
-
155
+
154
156
  if action_type == "tap":
155
157
  x, y = action.get("x", 0), action.get("y", 0)
156
158
  element_text = action.get("element_text", "")
@@ -165,10 +167,10 @@ async def _show_dry_run(macro_data: dict, start_from: int, max_steps: Optional[i
165
167
  elif action_type == "key_press":
166
168
  key_name = action.get("key_name", "UNKNOWN")
167
169
  details = f"{key_name}"
168
-
170
+
169
171
  description = action.get("description", "")
170
172
  table.add_row(str(i), action_type, details, description[:50] + "..." if len(description) > 50 else description)
171
-
173
+
172
174
  # Still use console for table display as it's structured data
173
175
  console.print(table)
174
176
 
@@ -179,9 +181,9 @@ async def _show_dry_run(macro_data: dict, start_from: int, max_steps: Optional[i
179
181
  def list(directory: str, debug: bool):
180
182
  """List available trajectory folders in a directory."""
181
183
  logger = configure_logging(debug)
182
-
184
+
183
185
  logger.info(f"📁 Scanning directory: {directory}")
184
-
186
+
185
187
  try:
186
188
  folders = []
187
189
  for item in os.listdir(directory):
@@ -198,25 +200,25 @@ def list(directory: str, debug: bool):
198
200
  except Exception as e:
199
201
  logger.debug(f"Error loading macro from {item}: {e}")
200
202
  folders.append((item, "Error loading", 0))
201
-
203
+
202
204
  if not folders:
203
205
  logger.info("📭 No trajectory folders found")
204
206
  return
205
-
207
+
206
208
  logger.info(f"🎯 Found {len(folders)} trajectory(s):")
207
-
209
+
208
210
  table = Table(title=f"Available Trajectories in {directory}")
209
211
  table.add_column("Folder", style="cyan")
210
212
  table.add_column("Description", style="white")
211
213
  table.add_column("Actions", style="green")
212
-
214
+
213
215
  for folder, description, actions in sorted(folders):
214
216
  table.add_row(folder, description[:80] + "..." if len(description) > 80 else description, str(actions))
215
-
217
+
216
218
  # Still use console for table display as it's structured data
217
219
  console.print(table)
218
220
  logger.info(f"💡 Use 'droidrun macro replay {directory}/<folder>' to replay a trajectory")
219
-
221
+
220
222
  except Exception as e:
221
223
  logger.error(f"💥 Error: {e}")
222
224
  if logger.isEnabledFor(logging.DEBUG):
@@ -225,4 +227,4 @@ def list(directory: str, debug: bool):
225
227
 
226
228
 
227
229
  if __name__ == "__main__":
228
- macro_cli()
230
+ macro_cli()
droidrun/macro/replay.py CHANGED
@@ -5,14 +5,12 @@ This module provides functionality to load and replay macro JSON files
5
5
  that were generated during DroidAgent trajectory recording.
6
6
  """
7
7
 
8
- import json
9
8
  import asyncio
10
9
  import logging
11
- import time
12
- import os
13
- from typing import Dict, List, Any, Optional
14
- from droidrun.tools.adb import AdbTools
10
+ from typing import Any, Dict, Optional
11
+
15
12
  from droidrun.agent.utils.trajectory import Trajectory
13
+ from droidrun.tools.adb import AdbTools
16
14
 
17
15
  logger = logging.getLogger("droidrun-macro")
18
16
 
@@ -145,7 +143,7 @@ class MacroPlayer:
145
143
  return True
146
144
 
147
145
  elif action_type == "back":
148
- logger.info(f"⬅️ Pressing back button")
146
+ logger.info("⬅️ Pressing back button")
149
147
  result = tools.back()
150
148
  logger.debug(f" Result: {result}")
151
149
  return True
@@ -211,10 +209,10 @@ class MacroPlayer:
211
209
 
212
210
  if success:
213
211
  success_count += 1
214
- logger.info(f" ✅ Action completed successfully")
212
+ logger.info(" ✅ Action completed successfully")
215
213
  else:
216
214
  failed_count += 1
217
- logger.error(f" ❌ Action failed")
215
+ logger.error(" ❌ Action failed")
218
216
 
219
217
  # Wait between actions (except for the last one)
220
218
  if i < len(actions):
@@ -227,7 +225,7 @@ class MacroPlayer:
227
225
  (success_count / total_executed * 100) if total_executed > 0 else 0
228
226
  )
229
227
 
230
- logger.info(f"\n🎉 Macro replay completed!")
228
+ logger.info("\n🎉 Macro replay completed!")
231
229
  logger.info(
232
230
  f"📊 Success: {success_count}/{total_executed} ({success_rate:.1f}%)"
233
231
  )
droidrun/portal.py CHANGED
@@ -144,7 +144,7 @@ def ping_portal_content(device: AdbDevice, debug: bool = False):
144
144
 
145
145
  def ping_portal_tcp(device: AdbDevice, debug: bool = False):
146
146
  try:
147
- tools = AdbTools(serial=device.serial, use_tcp=True)
147
+ AdbTools(serial=device.serial, use_tcp=True)
148
148
  except Exception as e:
149
149
  raise Exception("Failed to setup TCP forwarding") from e
150
150
 
@@ -1,4 +1,4 @@
1
+ from .events import DroidAgentFinalizeEvent, DroidAgentInitEvent
1
2
  from .tracker import capture, flush, print_telemetry_message
2
- from .events import DroidAgentInitEvent, DroidAgentFinalizeEvent
3
3
 
4
- __all__ = ["capture", "flush", "DroidAgentInitEvent", "DroidAgentFinalizeEvent", "print_telemetry_message"]
4
+ __all__ = ["capture", "flush", "DroidAgentInitEvent", "DroidAgentFinalizeEvent", "print_telemetry_message"]
@@ -1,7 +1,7 @@
1
- from typing import List
2
- from droidrun.agent.context import Task
1
+
3
2
  from pydantic import BaseModel
4
3
 
4
+
5
5
  class TelemetryEvent(BaseModel):
6
6
  pass
7
7
 
@@ -14,10 +14,9 @@ class DroidAgentInitEvent(TelemetryEvent):
14
14
  timeout: int
15
15
  vision: bool
16
16
  reasoning: bool
17
- reflection: bool
18
17
  enable_tracing: bool
19
18
  debug: bool
20
- save_trajectories: str = "none",
19
+ save_trajectories: str = "none",
21
20
 
22
21
 
23
22
  class DroidAgentFinalizeEvent(TelemetryEvent):
@@ -0,0 +1,173 @@
1
+ import asyncio
2
+ import functools
3
+ import inspect
4
+ import os
5
+ import uuid
6
+ from contextvars import Token, copy_context
7
+ from typing import Any, Callable
8
+
9
+ from llama_index.core.callbacks.base_handler import BaseCallbackHandler
10
+ from llama_index_instrumentation import get_dispatcher
11
+ from openinference.instrumentation import TraceConfig
12
+
13
+ dispatcher = get_dispatcher()
14
+
15
+ def arize_phoenix_callback_handler(**kwargs: Any) -> BaseCallbackHandler:
16
+ # newer versions of arize, v2.x
17
+ from openinference.instrumentation.llama_index import LlamaIndexInstrumentor
18
+ from openinference.semconv.resource import ResourceAttributes
19
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
20
+ OTLPSpanExporter,
21
+ )
22
+ from opentelemetry.sdk import trace as trace_sdk
23
+ from opentelemetry.sdk.resources import Resource
24
+ from opentelemetry.sdk.trace.export import SimpleSpanProcessor
25
+
26
+ endpoint = kwargs.get("endpoint", os.getenv("phoenix_url", "http://127.0.0.1:6006")) + "/v1/traces"
27
+
28
+ resource_attributes = {}
29
+ phoenix_project_name = os.getenv("phoenix_project_name", "")
30
+ if phoenix_project_name.strip():
31
+ resource_attributes[ResourceAttributes.PROJECT_NAME] = phoenix_project_name
32
+ resource = Resource(attributes=resource_attributes)
33
+
34
+ tracer_provider = trace_sdk.TracerProvider(resource=resource)
35
+ tracer_provider.add_span_processor(
36
+ SimpleSpanProcessor(OTLPSpanExporter(endpoint))
37
+ )
38
+ config = TraceConfig(
39
+ base64_image_max_length=64000000
40
+ )
41
+
42
+ return LlamaIndexInstrumentor().instrument(
43
+ tracer_provider=kwargs.get("tracer_provider", tracer_provider),
44
+ separate_trace_from_runtime_context=kwargs.get(
45
+ "separate_trace_from_runtime_context"
46
+ ),
47
+ config=config
48
+ )
49
+
50
+
51
+ def clean_span(span_name: str):
52
+ """
53
+ Create a span with a clean name (without class prefix).
54
+
55
+ This function returns a decorator that creates spans with custom names
56
+ instead of the default class.method format.
57
+
58
+ It preserves parent-child relationships by using the same active span
59
+ context variable as the built-in dispatcher decorator does.
60
+
61
+ Args:
62
+ span_name: The desired name for the span
63
+
64
+ Returns:
65
+ A decorator function
66
+ """
67
+ def decorator(func: Callable) -> Callable:
68
+ # Support both sync and async callables
69
+ if inspect.iscoroutinefunction(func):
70
+ @functools.wraps(func)
71
+ async def async_wrapper(*args, **kwargs):
72
+ # Import here to avoid circular imports
73
+ from llama_index_instrumentation.dispatcher import active_instrument_tags
74
+ from llama_index_instrumentation.span import active_span_id
75
+
76
+
77
+ span_id = f"{span_name}-{uuid.uuid4()}"
78
+ bound_args = inspect.signature(func).bind(*args, **kwargs)
79
+ # Treat as method only if qualname indicates Class.method
80
+ is_method = "." in getattr(func, "__qualname__", "")
81
+ instance = args[0] if (args and is_method) else None
82
+
83
+ tags = active_instrument_tags.get()
84
+ token = active_span_id.set(span_id)
85
+ parent_id = None if token.old_value is Token.MISSING else token.old_value
86
+
87
+ dispatcher.span_enter(
88
+ id_=span_id,
89
+ bound_args=bound_args,
90
+ instance=instance,
91
+ parent_id=parent_id,
92
+ tags=tags,
93
+ )
94
+ try:
95
+ result = await func(*args, **kwargs)
96
+ except Exception as e:
97
+ dispatcher.span_drop(id_=span_id, bound_args=bound_args, instance=instance, err=e)
98
+ raise
99
+ else:
100
+ dispatcher.span_exit(id_=span_id, bound_args=bound_args, instance=instance, result=result)
101
+ return result
102
+ finally:
103
+ active_span_id.reset(token)
104
+
105
+ return async_wrapper
106
+ else:
107
+ @functools.wraps(func)
108
+ def wrapper(*args, **kwargs):
109
+ # Import here to avoid circular imports
110
+ from llama_index_instrumentation.dispatcher import active_instrument_tags
111
+ from llama_index_instrumentation.span import active_span_id
112
+
113
+
114
+ span_id = f"{span_name}-{uuid.uuid4()}"
115
+ bound_args = inspect.signature(func).bind(*args, **kwargs)
116
+ # Treat as method only if qualname indicates Class.method
117
+ is_method = "." in getattr(func, "__qualname__", "")
118
+ instance = args[0] if (args and is_method) else None
119
+
120
+ tags = active_instrument_tags.get()
121
+ context = copy_context()
122
+ token = active_span_id.set(span_id)
123
+ parent_id = None if token.old_value is Token.MISSING else token.old_value
124
+
125
+ dispatcher.span_enter(
126
+ id_=span_id,
127
+ bound_args=bound_args,
128
+ instance=instance,
129
+ parent_id=parent_id,
130
+ tags=tags,
131
+ )
132
+ try:
133
+ result = func(*args, **kwargs)
134
+ if isinstance(result, asyncio.Future):
135
+ new_future = asyncio.ensure_future(result)
136
+
137
+ def _on_done(fut: asyncio.Future) -> None:
138
+ try:
139
+ fut_result = None if fut.exception() else fut.result()
140
+ dispatcher.span_exit(
141
+ id_=span_id,
142
+ bound_args=bound_args,
143
+ instance=instance,
144
+ result=fut_result,
145
+ )
146
+ except Exception as e2:
147
+ dispatcher.span_drop(
148
+ id_=span_id,
149
+ bound_args=bound_args,
150
+ instance=instance,
151
+ err=e2,
152
+ )
153
+ raise
154
+ finally:
155
+ try:
156
+ context.run(active_span_id.reset, token)
157
+ except ValueError:
158
+ pass
159
+
160
+ new_future.add_done_callback(_on_done)
161
+ return new_future
162
+ except Exception as e:
163
+ dispatcher.span_drop(id_=span_id, bound_args=bound_args, instance=instance, err=e)
164
+ raise
165
+ else:
166
+ dispatcher.span_exit(id_=span_id, bound_args=bound_args, instance=instance, result=result)
167
+ return result
168
+ finally:
169
+ if not isinstance(locals().get("result"), asyncio.Future):
170
+ active_span_id.reset(token)
171
+
172
+ return wrapper
173
+ return decorator
@@ -1,8 +1,10 @@
1
- from posthog import Posthog
1
+ import logging
2
+ import os
2
3
  from pathlib import Path
3
4
  from uuid import uuid4
4
- import os
5
- import logging
5
+
6
+ from posthog import Posthog
7
+
6
8
  from .events import TelemetryEvent
7
9
 
8
10
  logger = logging.getLogger("droidrun-telemetry")
@@ -76,9 +78,9 @@ def capture(event: TelemetryEvent, user_id: str | None = None):
76
78
  def flush():
77
79
  try:
78
80
  if not is_telemetry_enabled():
79
- logger.debug(f"Telemetry disabled, skipping flush")
81
+ logger.debug("Telemetry disabled, skipping flush")
80
82
  return
81
83
  posthog.flush()
82
- logger.debug(f"Flushed telemetry data")
84
+ logger.debug("Flushed telemetry data")
83
85
  except Exception as e:
84
86
  logger.error(f"Error flushing telemetry data: {e}")
@@ -2,8 +2,8 @@
2
2
  DroidRun Tools - Core functionality for Android device control.
3
3
  """
4
4
 
5
- from droidrun.tools.tools import Tools, describe_tools
6
5
  from droidrun.tools.adb import AdbTools
7
6
  from droidrun.tools.ios import IOSTools
7
+ from droidrun.tools.tools import Tools, describe_tools
8
8
 
9
9
  __all__ = ["Tools", "describe_tools", "AdbTools", "IOSTools"]