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 +15 -8
- droidrun/__main__.py +2 -3
- droidrun/adb/device.py +1 -1
- droidrun/agent/codeact/__init__.py +13 -0
- droidrun/agent/codeact/codeact_agent.py +334 -0
- droidrun/agent/codeact/events.py +36 -0
- droidrun/agent/codeact/prompts.py +78 -0
- droidrun/agent/droid/__init__.py +13 -0
- droidrun/agent/droid/droid_agent.py +418 -0
- droidrun/agent/planner/__init__.py +15 -0
- droidrun/agent/planner/events.py +20 -0
- droidrun/agent/planner/prompts.py +144 -0
- droidrun/agent/planner/task_manager.py +355 -0
- droidrun/agent/planner/workflow.py +371 -0
- droidrun/agent/utils/async_utils.py +56 -0
- droidrun/agent/utils/chat_utils.py +92 -0
- droidrun/agent/utils/executer.py +97 -0
- droidrun/agent/utils/llm_picker.py +143 -0
- droidrun/cli/main.py +422 -107
- droidrun/tools/__init__.py +4 -25
- droidrun/tools/actions.py +767 -783
- droidrun/tools/device.py +1 -1
- droidrun/tools/loader.py +60 -0
- {droidrun-0.1.0.dist-info → droidrun-0.2.0.dist-info}/METADATA +134 -37
- droidrun-0.2.0.dist-info/RECORD +32 -0
- droidrun/agent/__init__.py +0 -16
- droidrun/agent/llm_reasoning.py +0 -567
- droidrun/agent/react_agent.py +0 -556
- droidrun/llm/__init__.py +0 -24
- droidrun-0.1.0.dist-info/RECORD +0 -20
- {droidrun-0.1.0.dist-info → droidrun-0.2.0.dist-info}/WHEEL +0 -0
- {droidrun-0.1.0.dist-info → droidrun-0.2.0.dist-info}/entry_points.txt +0 -0
- {droidrun-0.1.0.dist-info → droidrun-0.2.0.dist-info}/licenses/LICENSE +0 -0
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
|
9
|
-
from
|
10
|
-
from
|
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
|
-
"
|
16
|
-
"
|
17
|
-
"
|
18
|
-
"
|
19
|
-
|
20
|
+
"PlannerAgent",
|
21
|
+
"DeviceManager",
|
22
|
+
"Tools",
|
23
|
+
"load_llm",
|
24
|
+
"SimpleCodeExecutor",
|
25
|
+
"load_tools",
|
26
|
+
]
|
droidrun/__main__.py
CHANGED
droidrun/adb/device.py
CHANGED
@@ -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
|
+
]
|