droidrun 0.1.0__py3-none-any.whl → 0.2.0__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 CHANGED
@@ -5,15 +5,22 @@ DroidRun - A framework for controlling Android devices through LLM agents.
5
5
  __version__ = "0.1.0"
6
6
 
7
7
  # Import main classes for easier access
8
- from droidrun.agent.react_agent import ReActAgent as Agent
9
- from droidrun.agent.react_agent import ReActStep, ReActStepType
10
- from droidrun.llm import OpenAILLM, AnthropicLLM
8
+ from .agent.codeact.codeact_agent import CodeActAgent as Agent
9
+ from .agent.planner.workflow import PlannerAgent
10
+ from .agent.utils.executer import SimpleCodeExecutor
11
+ from .agent.utils.llm_picker import load_llm
12
+ from .tools.device import DeviceManager
13
+ from .tools.actions import Tools
14
+ from .tools.loader import load_tools
15
+
11
16
 
12
17
  # Make main components available at package level
13
18
  __all__ = [
14
19
  "Agent",
15
- "ReActStep",
16
- "ReActStepType",
17
- "OpenAILLM",
18
- "AnthropicLLM",
19
- ]
20
+ "PlannerAgent",
21
+ "DeviceManager",
22
+ "Tools",
23
+ "load_llm",
24
+ "SimpleCodeExecutor",
25
+ "load_tools",
26
+ ]
droidrun/__main__.py CHANGED
@@ -1,8 +1,7 @@
1
1
  """
2
2
  DroidRun main entry point
3
3
  """
4
-
5
- from droidrun.cli.main import cli
4
+ from .cli.main import cli
6
5
 
7
6
  if __name__ == '__main__':
8
- cli()
7
+ cli()
droidrun/adb/device.py CHANGED
@@ -264,7 +264,7 @@ class Device:
264
264
 
265
265
  import logging
266
266
  logger = logging.getLogger("droidrun")
267
- logger.info(
267
+ logger.debug(
268
268
  f"Screenshot compressed successfully: {png_size:.1f}KB → {jpg_size:.1f}KB ({reduction:.1f}% reduction)"
269
269
  )
270
270
 
@@ -0,0 +1,13 @@
1
+ from .codeact_agent import CodeActAgent
2
+ from .prompts import (
3
+ DEFAULT_CODE_ACT_SYSTEM_PROMPT,
4
+ DEFAULT_CODE_ACT_USER_PROMPT,
5
+ DEFAULT_NO_THOUGHTS_PROMPT
6
+ )
7
+
8
+ __all__ = [
9
+ "CodeActAgent",
10
+ "DEFAULT_CODE_ACT_SYSTEM_PROMPT",
11
+ "DEFAULT_CODE_ACT_USER_PROMPT",
12
+ "DEFAULT_NO_THOUGHTS_PROMPT"
13
+ ]
@@ -0,0 +1,334 @@
1
+ import logging
2
+ import re
3
+ import inspect
4
+ import time
5
+ from typing import Awaitable, Callable, List, Optional, Dict, Any, Tuple, TYPE_CHECKING, Union
6
+ from llama_index.core.base.llms.types import ChatMessage, ChatResponse, TextBlock
7
+ from llama_index.core.prompts import PromptTemplate
8
+ from llama_index.core.llms.llm import LLM
9
+ from llama_index.core.workflow import Workflow, StartEvent, StopEvent, Context, step
10
+ from llama_index.core.memory import ChatMemoryBuffer
11
+ from .events import FinalizeEvent, InputEvent, ModelOutputEvent, ExecutionEvent, ExecutionResultEvent
12
+ from ..utils.chat_utils import add_screenshot, add_screenshot_image_block, add_ui_text_block, message_copy
13
+ from .prompts import (
14
+ DEFAULT_CODE_ACT_SYSTEM_PROMPT,
15
+ DEFAULT_CODE_ACT_USER_PROMPT,
16
+ DEFAULT_NO_THOUGHTS_PROMPT
17
+ )
18
+
19
+ if TYPE_CHECKING:
20
+ from ...tools import Tools
21
+
22
+ logger = logging.getLogger("droidrun")
23
+
24
+
25
+ class CodeActAgent(Workflow):
26
+ """
27
+ An agent that uses a ReAct-like cycle (Thought -> Code -> Observation)
28
+ to solve problems requiring code execution. It extracts code from
29
+ Markdown blocks and uses specific step types for tracking.
30
+ """
31
+ def __init__(
32
+ self,
33
+ llm: LLM,
34
+ code_execute_fn: Callable[[str], Awaitable[Dict[str, Any]]],
35
+ tools: 'Tools',
36
+ available_tools: List = [],
37
+ max_steps: int = 10, # Default max steps (kept for backwards compatibility but no longer enforced)
38
+ system_prompt: Optional[str] = None,
39
+ user_prompt: Optional[str] = None,
40
+ vision: bool = False,
41
+ debug: bool = False,
42
+ *args,
43
+ **kwargs
44
+ ):
45
+ # assert instead of if
46
+ assert llm, "llm must be provided."
47
+ assert code_execute_fn, "code_execute_fn must be provided"
48
+ super().__init__(*args, **kwargs)
49
+
50
+ self.llm = llm
51
+ self.code_execute_fn = code_execute_fn
52
+ self.available_tools = available_tools or []
53
+ self.tools = tools
54
+ self.max_steps = max_steps # Kept for backwards compatibility but not enforced
55
+ self.tool_descriptions = self.parse_tool_descriptions() # Parse tool descriptions once at initialization
56
+ self.system_prompt_content = (system_prompt or DEFAULT_CODE_ACT_SYSTEM_PROMPT).format(tool_descriptions=self.tool_descriptions)
57
+ self.system_prompt = ChatMessage(role="system", content=self.system_prompt_content)
58
+ self.user_prompt = user_prompt
59
+ self.no_thoughts_prompt = None
60
+ self.memory = None
61
+ self.goal = None
62
+ self.steps_counter = 0 # Initialize step counter (kept for tracking purposes)
63
+ self.code_exec_counter = 0 # Initialize execution counter
64
+ self.vision = vision
65
+ self.debug = debug
66
+ logger.info("✅ CodeActAgent initialized successfully.")
67
+
68
+ def parse_tool_descriptions(self) -> str:
69
+ """Parses the available tools and their descriptions for the system prompt."""
70
+ logger.info("🛠️ Parsing tool descriptions...")
71
+ # self.available_tools is a list of functions, we need to get their docstrings, names, and signatures and display them as `def name(args) -> return_type:\n"""docstring""" ...\n`
72
+ tool_descriptions = []
73
+ excluded_tools = ["take_screenshot"] # List of tools to exclude
74
+
75
+ for tool in self.available_tools:
76
+ assert callable(tool), f"Tool {tool} is not callable."
77
+ tool_name = tool.__name__
78
+
79
+ # Skip excluded tools
80
+ if tool_name in excluded_tools:
81
+ logger.debug(f" - Skipping excluded tool: {tool_name}")
82
+ continue
83
+
84
+ tool_signature = inspect.signature(tool)
85
+ tool_docstring = tool.__doc__ or "No description available."
86
+ # Format the function signature and docstring
87
+ formatted_signature = f"def {tool_name}{tool_signature}:\n \"\"\"{tool_docstring}\"\"\"\n..."
88
+ tool_descriptions.append(formatted_signature)
89
+ logger.debug(f" - Parsed tool: {tool_name}")
90
+ # Join all tool descriptions into a single string
91
+ descriptions = "\n".join(tool_descriptions)
92
+ logger.info(f"🔩 Found {len(tool_descriptions)} tools.")
93
+ return descriptions
94
+
95
+ def _extract_code_and_thought(self, response_text: str) -> Tuple[Optional[str], str]:
96
+ """
97
+ Extracts code from Markdown blocks (```python ... ```) and the surrounding text (thought),
98
+ handling indented code blocks.
99
+
100
+ Returns:
101
+ Tuple[Optional[code_string], thought_string]
102
+ """
103
+ if self.debug:
104
+ logger.debug("✂️ Extracting code and thought from response...")
105
+ code_pattern = r"^\s*```python\s*\n(.*?)\n^\s*```\s*?$" # Added ^\s*, re.MULTILINE, and made closing fence match more robust
106
+ # Use re.DOTALL to make '.' match newlines and re.MULTILINE to make '^' match start of lines
107
+ code_matches = list(re.finditer(code_pattern, response_text, re.DOTALL | re.MULTILINE))
108
+
109
+ if not code_matches:
110
+ # No code found, the entire response is thought
111
+ if self.debug:
112
+ logger.debug(" - No code block found. Entire response is thought.")
113
+ return None, response_text.strip()
114
+
115
+ extracted_code_parts = []
116
+ for match in code_matches:
117
+ # group(1) is the (.*?) part - the actual code content
118
+ code_content = match.group(1)
119
+ extracted_code_parts.append(code_content) # Keep original indentation for now
120
+
121
+ extracted_code = "\n\n".join(extracted_code_parts)
122
+ if self.debug:
123
+ logger.debug(f" - Combined extracted code:\n```python\n{extracted_code}\n```")
124
+
125
+
126
+ # Extract thought text (text before the first code block, between blocks, and after the last)
127
+ thought_parts = []
128
+ last_end = 0
129
+ for match in code_matches:
130
+ # Use span(0) to get the start/end of the *entire* match (including fences and indentation)
131
+ start, end = match.span(0)
132
+ thought_parts.append(response_text[last_end:start])
133
+ last_end = end
134
+ thought_parts.append(response_text[last_end:]) # Text after the last block
135
+
136
+ thought_text = "".join(thought_parts).strip()
137
+ # Avoid overly long debug messages for thought
138
+ if self.debug:
139
+ thought_preview = (thought_text[:100] + '...') if len(thought_text) > 100 else thought_text
140
+ logger.debug(f" - Extracted thought: {thought_preview}")
141
+
142
+ return extracted_code, thought_text
143
+
144
+ @step
145
+ async def prepare_chat(self, ev: StartEvent, ctx: Context) -> InputEvent:
146
+ """Prepare chat history from user input."""
147
+ logger.info("💬 Preparing chat for task execution...")
148
+ # Get or create memory
149
+ self.memory: ChatMemoryBuffer = await ctx.get(
150
+ "memory", default=ChatMemoryBuffer.from_defaults(llm=self.llm)
151
+ )
152
+ user_input = ev.get("input", default=None)
153
+ assert user_input, "User input cannot be empty."
154
+ # Add user input to memory
155
+ if self.debug:
156
+ logger.debug(" - Adding goal to memory.")
157
+ goal = user_input
158
+ self.user_message = ChatMessage(role="user", content=PromptTemplate(self.user_prompt or DEFAULT_CODE_ACT_USER_PROMPT).format(goal=goal))
159
+ self.no_thoughts_prompt = ChatMessage(role="user", content=PromptTemplate(DEFAULT_NO_THOUGHTS_PROMPT).format(goal=goal))
160
+ await self.memory.aput(self.user_message)
161
+ # Update context
162
+ await ctx.set("memory", self.memory)
163
+ input_messages = self.memory.get_all()
164
+ return InputEvent(input=input_messages)
165
+ @step
166
+ async def handle_llm_input(self, ev: InputEvent, ctx: Context) -> Union[ModelOutputEvent, FinalizeEvent]:
167
+ """Handle LLM input."""
168
+ # Get chat history from event
169
+ chat_history = ev.input
170
+ assert len(chat_history) > 0, "Chat history cannot be empty."
171
+
172
+ self.steps_counter += 1
173
+ logger.info(f"🧠 Step {self.steps_counter}: Thinking...")
174
+
175
+ # Get LLM response
176
+ response = await self._get_llm_response(chat_history)
177
+ # Add response to memory
178
+ await self.memory.aput(response.message)
179
+ if self.debug:
180
+ logger.debug("🤖 LLM response received.")
181
+ code, thoughts = self._extract_code_and_thought(response.message.content)
182
+ if self.debug:
183
+ logger.debug(f" - Thoughts: {'Yes' if thoughts else 'No'}, Code: {'Yes' if code else 'No'}")
184
+ return ModelOutputEvent(thoughts=thoughts, code=code)
185
+
186
+ @step
187
+ async def handle_llm_output(self, ev: ModelOutputEvent, ctx: Context) -> Union[ExecutionEvent, FinalizeEvent]:
188
+ """Handle LLM output."""
189
+ if self.debug:
190
+ logger.debug("⚙️ Handling LLM output...")
191
+ # Get code and thoughts from event
192
+ code = ev.code
193
+ thoughts = ev.thoughts
194
+
195
+ # Warning if no thoughts are provided
196
+ if not thoughts:
197
+ logger.warning("🤔 LLM provided code without thoughts. Adding reminder prompt.")
198
+ await self.memory.aput(self.no_thoughts_prompt)
199
+ else:
200
+ # print thought but start with emoji at the start of the log
201
+ logger.info(f"🤔 Reasoning: {thoughts}")
202
+
203
+ # If code is present, execute it
204
+ if code:
205
+ return ExecutionEvent(code=code)
206
+ else:
207
+ message = ChatMessage(role="user", content="No code was provided. If you want to mark task as complete (whether it failed or succeeded), use complete(success:bool, reason:str) function within a code block ```pythn\n```.")
208
+ await self.memory.aput(message)
209
+ return InputEvent(input=self.memory.get_all())
210
+
211
+ @step
212
+ async def execute_code(self, ev: ExecutionEvent, ctx: Context) -> ExecutionResultEvent:
213
+ """Execute the code and return the result."""
214
+ code = ev.code
215
+ assert code, "Code cannot be empty."
216
+ logger.info(f"⚡ Executing action...")
217
+ if self.debug:
218
+ logger.debug(f"Code to execute:\n```python\n{code}\n```")
219
+ # Execute the code using the provided function
220
+ try:
221
+ self.code_exec_counter += 1
222
+ result = await self.code_execute_fn(code)
223
+ logger.info(f"💡 Code execution successful. Result: {result}")
224
+ if self.tools.finished == True:
225
+ logger.debug(" - Task completed.")
226
+ return FinalizeEvent(result={'success': self.tools.success, 'reason': self.tools.reason})
227
+ return ExecutionResultEvent(output=str(result)) # Ensure output is string
228
+ except Exception as e:
229
+ logger.error(f"💥 Action failed: {e}")
230
+ if self.debug:
231
+ logger.error("Exception details:", exc_info=True)
232
+ error_message = f"Error during execution: {e}"
233
+ return ExecutionResultEvent(output=error_message) # Return error message as output
234
+
235
+ @step
236
+ async def handle_execution_result(self, ev: ExecutionResultEvent, ctx: Context) -> InputEvent:
237
+ """Handle the execution result. Currently it just returns InputEvent."""
238
+ if self.debug:
239
+ logger.debug("📊 Handling execution result...")
240
+ # Get the output from the event
241
+ output = ev.output
242
+ if output is None:
243
+ output = "Code executed, but produced no output."
244
+ logger.warning(" - Execution produced no output.")
245
+ else:
246
+ if self.debug:
247
+ logger.debug(f" - Execution output: {output[:100]}..." if len(output) > 100 else f" - Execution output: {output}")
248
+ # Add the output to memory as an user message (observation)
249
+ observation_message = ChatMessage(role="user", content=f"Execution Result:\n```\n{output}\n```")
250
+ await self.memory.aput(observation_message)
251
+ if self.debug:
252
+ logger.debug(" - Added execution result to memory.")
253
+ return InputEvent(input=self.memory.get_all())
254
+
255
+
256
+ @step
257
+ async def finalize(self, ev: FinalizeEvent, ctx: Context) -> StopEvent:
258
+ """Finalize the workflow."""
259
+ self.tools.finished = False # Reset finished flag
260
+ await ctx.set("memory", self.memory) # Ensure memory is set in context
261
+
262
+ # Include steps and code execution information in the result
263
+ result = ev.result or {}
264
+ result.update({
265
+ "codeact_steps": self.steps_counter,
266
+ "code_executions": self.code_exec_counter
267
+ })
268
+
269
+ return StopEvent(result=result)
270
+
271
+ async def _get_llm_response(self, chat_history: List[ChatMessage]) -> ChatResponse:
272
+ """Get streaming response from LLM."""
273
+ if self.debug:
274
+ logger.debug(f" - Sending {len(chat_history)} messages to LLM.")
275
+ # Combine system prompt with chat history
276
+ if self.vision:
277
+ chat_history = await add_screenshot_image_block(self.tools, chat_history)
278
+ elif self.tools.last_screenshot:
279
+ chat_history = await add_screenshot(chat_history, self.tools.last_screenshot)
280
+ self.tools.last_screenshot = None # Reset last screenshot after sending it
281
+
282
+ # always add ui
283
+ chat_history = await add_ui_text_block(self.tools, chat_history)
284
+
285
+ # Add remembered information if available
286
+ if hasattr(self.tools, 'memory') and self.tools.memory:
287
+ memory_block = "\n### Remembered Information:\n"
288
+ for idx, item in enumerate(self.tools.memory, 1):
289
+ memory_block += f"{idx}. {item}\n"
290
+
291
+ # Find the first user message and inject memory before it
292
+ for i, msg in enumerate(chat_history):
293
+ if msg.role == "user":
294
+ if isinstance(msg.content, str):
295
+ # For text-only messages
296
+ updated_content = f"{memory_block}\n\n{msg.content}"
297
+ chat_history[i] = ChatMessage(role="user", content=updated_content)
298
+ elif isinstance(msg.content, list):
299
+ # For multimodal content
300
+ memory_text_block = TextBlock(text=memory_block)
301
+ # Insert memory text block at beginning
302
+ content_blocks = [memory_text_block] + msg.content
303
+ chat_history[i] = ChatMessage(role="user", content=content_blocks)
304
+ break
305
+
306
+ messages_to_send = [self.system_prompt] + chat_history
307
+
308
+ messages_to_send = [message_copy(msg) for msg in messages_to_send]
309
+ try:
310
+ response = await self.llm.achat(
311
+ messages=messages_to_send
312
+ )
313
+ assert hasattr(response, "message"), f"LLM response does not have a message attribute.\nResponse: {response}"
314
+ except Exception as e:
315
+ if self.llm.class_name() == "Gemini_LLM" and "You exceeded your current quota" in str(e):
316
+ s = str(e._details[2])
317
+ match = re.search(r'seconds:\s*(\d+)', s)
318
+ if match:
319
+ seconds = int(match.group(1)) + 1
320
+ logger.error(f"Rate limit error. Retrying in {seconds} seconds...")
321
+ time.sleep(seconds)
322
+ else:
323
+ logger.error(f"Rate limit error. Retrying in 5 seconds...")
324
+ time.sleep(40)
325
+ response = await self.llm.achat(
326
+ messages=messages_to_send
327
+ )
328
+ else:
329
+ logger.error(f"Error getting LLM response: {e}")
330
+ return StopEvent(result={'finished': True, 'message': f"Error getting LLM response: {e}", 'steps': self.steps_counter, 'code_executions': self.code_exec_counter}) # Return final message and steps
331
+ if self.debug:
332
+ logger.debug(" - Received response from LLM.")
333
+ return response
334
+
@@ -0,0 +1,36 @@
1
+ from llama_index.core.llms import ChatMessage
2
+ from llama_index.core.workflow import Event
3
+ from typing import Any, Optional
4
+
5
+ from pydantic import PrivateAttr
6
+
7
+
8
+ class InputEvent(Event):
9
+ input: list[ChatMessage]
10
+
11
+ class ModelOutputEvent(Event):
12
+ thoughts: Optional[str] = None
13
+ code: Optional[str] = None
14
+
15
+ class ExecutionEvent(Event):
16
+ code: str
17
+ globals: dict[str, str] = {}
18
+ locals: dict[str, str] = {}
19
+
20
+ class ExecutionResultEvent(Event):
21
+ output: str
22
+
23
+ class FinalizeEvent(Event):
24
+ _result: Any = PrivateAttr(default=None)
25
+
26
+ def __init__(self, result: Any = None, **kwargs: Any) -> None:
27
+ # forces the user to provide a result
28
+ super().__init__(_result=result, **kwargs)
29
+
30
+ def _get_result(self) -> Any:
31
+ """This can be overridden by subclasses to return the desired result."""
32
+ return self._result
33
+
34
+ @property
35
+ def result(self) -> Any:
36
+ return self._get_result()
@@ -0,0 +1,78 @@
1
+ """
2
+ Prompt templates for the CodeActAgent.
3
+
4
+ This module contains all the prompts used by the CodeActAgent,
5
+ separated from the workflow logic for better maintainability.
6
+ """
7
+
8
+ # System prompt for the CodeActAgent that explains its role and capabilities
9
+ DEFAULT_CODE_ACT_SYSTEM_PROMPT = """You are a helpful AI assistant that can write and execute Python code to solve problems.
10
+
11
+ You will be given a task to perform. You should output:
12
+ - Python code wrapped in ``` tags that provides the solution to the task, or a step towards the solution. Any output you want to extract from the code should be printed to the console.
13
+ - Text to be shown directly to the user, if you want to ask for more information or provide the final answer.
14
+ - If the previous code execution can be used to respond to the user, then respond directly (typically you want to avoid mentioning anything related to the code execution in your response).
15
+ - If you task is complete, you should use the complete(success:bool, reason:str) function within a code block to mark it as finished. The success parameter should be True if the task was completed successfully, and False otherwise. The reason parameter should be a string explaining the reason for failure if failed.
16
+ ## Response Format:
17
+ Example of proper code format:
18
+ To calculate the area of a circle, I need to use the formula: area = pi * radius^2. I will write a function to do this.
19
+ ```python
20
+ import math
21
+
22
+ def calculate_area(radius):
23
+ return math.pi * radius**2
24
+
25
+ # Calculate the area for radius = 5
26
+ area = calculate_area(5)
27
+ print(f"The area of the circle is {{area:.2f}} square units")
28
+ ```
29
+
30
+ Another example (with for loop):
31
+ To calculate the sum of numbers from 1 to 10, I will use a for loop.
32
+ ```python
33
+ sum = 0
34
+ for i in range(1, 11):
35
+ sum += i
36
+ print(f"The sum of numbers from 1 to 10 is {{sum}}")
37
+ ```
38
+
39
+ In addition to the Python Standard Library and any functions you have already written, you can use the following functions:
40
+ {tool_descriptions}
41
+
42
+ You'll receive a screenshot showing the current screen and its UI elements to help you complete the task. However, screenshots won't be saved in the chat history. So, make sure to describe what you see and explain the key parts of your plan in your thoughts, as those will be saved and used to assist you in future steps.
43
+
44
+ **Important Notes:**
45
+ - If there is a precondition for the task, you MUST check if it is met.
46
+ - If a goal's precondition is unmet, fail the task by calling `complete(success=False, reason='...')` with an explanation.
47
+
48
+ ## Final Answer Guidelines:
49
+ - When providing a final answer, focus on directly answering the user's question
50
+ - Avoid referencing the code you generated unless specifically asked
51
+ - Present the results clearly and concisely as if you computed them directly
52
+ - If relevant, you can briefly mention general methods used, but don't include code snippets in the final answer
53
+ - Structure your response like you're directly answering the user's query, not explaining how you solved it
54
+
55
+ Reminder: Always place your Python code between ```...``` tags when you want to run code.
56
+
57
+ You MUST ALWAYS to include your reasoning and thought process outside of the code block. You MUST DOUBLE CHECK that TASK IS COMPLETE with a SCREENSHOT.
58
+ """
59
+
60
+ # User prompt template that presents the current request and prompts for reasoning
61
+ DEFAULT_CODE_ACT_USER_PROMPT = """**Current Request:**
62
+ {goal}
63
+
64
+ **Is the precondition met? What is your reasoning and the next step to address this request?** Explain your thought process then provide code in ```python ... ``` tags if needed."""""
65
+
66
+ # Prompt to remind the agent to provide thoughts before code
67
+ DEFAULT_NO_THOUGHTS_PROMPT = """Your previous response provided code without explaining your reasoning first. Remember to always describe your thought process and plan *before* providing the code block.
68
+
69
+ The code you provided will be executed below.
70
+
71
+ Now, describe the next step you will take to address the original goal: {goal}"""
72
+
73
+ # Export all prompts
74
+ __all__ = [
75
+ "DEFAULT_CODE_ACT_SYSTEM_PROMPT",
76
+ "DEFAULT_CODE_ACT_USER_PROMPT",
77
+ "DEFAULT_NO_THOUGHTS_PROMPT"
78
+ ]
@@ -0,0 +1,13 @@
1
+ """
2
+ Droidrun Agent Module.
3
+
4
+ This module provides a ReAct agent for automating Android devices using reasoning and acting.
5
+ """
6
+
7
+ from ..codeact.codeact_agent import CodeActAgent
8
+ from .droid_agent import DroidAgent
9
+
10
+ __all__ = [
11
+ "CodeActAgent",
12
+ "DroidAgent"
13
+ ]