droidrun 0.3.10.dev2__py3-none-any.whl → 0.3.10.dev4__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 (54) hide show
  1. droidrun/agent/codeact/__init__.py +1 -4
  2. droidrun/agent/codeact/codeact_agent.py +95 -86
  3. droidrun/agent/codeact/events.py +1 -2
  4. droidrun/agent/context/__init__.py +5 -9
  5. droidrun/agent/context/episodic_memory.py +1 -3
  6. droidrun/agent/context/task_manager.py +8 -2
  7. droidrun/agent/droid/droid_agent.py +102 -141
  8. droidrun/agent/droid/events.py +45 -14
  9. droidrun/agent/executor/__init__.py +6 -4
  10. droidrun/agent/executor/events.py +29 -9
  11. droidrun/agent/executor/executor_agent.py +86 -28
  12. droidrun/agent/executor/prompts.py +8 -2
  13. droidrun/agent/manager/__init__.py +6 -7
  14. droidrun/agent/manager/events.py +16 -4
  15. droidrun/agent/manager/manager_agent.py +130 -69
  16. droidrun/agent/manager/prompts.py +1 -159
  17. droidrun/agent/utils/chat_utils.py +64 -2
  18. droidrun/agent/utils/device_state_formatter.py +54 -26
  19. droidrun/agent/utils/executer.py +66 -80
  20. droidrun/agent/utils/inference.py +11 -10
  21. droidrun/agent/utils/tools.py +58 -6
  22. droidrun/agent/utils/trajectory.py +18 -12
  23. droidrun/cli/logs.py +118 -56
  24. droidrun/cli/main.py +154 -136
  25. droidrun/config_manager/__init__.py +9 -7
  26. droidrun/config_manager/app_card_loader.py +148 -0
  27. droidrun/config_manager/config_manager.py +200 -102
  28. droidrun/config_manager/path_resolver.py +104 -0
  29. droidrun/config_manager/prompt_loader.py +75 -0
  30. droidrun/macro/__init__.py +1 -1
  31. droidrun/macro/cli.py +23 -18
  32. droidrun/telemetry/__init__.py +2 -2
  33. droidrun/telemetry/events.py +3 -3
  34. droidrun/telemetry/tracker.py +1 -1
  35. droidrun/tools/adb.py +1 -1
  36. droidrun/tools/ios.py +3 -2
  37. {droidrun-0.3.10.dev2.dist-info → droidrun-0.3.10.dev4.dist-info}/METADATA +10 -3
  38. droidrun-0.3.10.dev4.dist-info/RECORD +61 -0
  39. droidrun/agent/codeact/prompts.py +0 -26
  40. droidrun/agent/context/agent_persona.py +0 -16
  41. droidrun/agent/context/context_injection_manager.py +0 -66
  42. droidrun/agent/context/personas/__init__.py +0 -11
  43. droidrun/agent/context/personas/app_starter.py +0 -44
  44. droidrun/agent/context/personas/big_agent.py +0 -96
  45. droidrun/agent/context/personas/default.py +0 -95
  46. droidrun/agent/context/personas/ui_expert.py +0 -108
  47. droidrun/agent/planner/__init__.py +0 -13
  48. droidrun/agent/planner/events.py +0 -21
  49. droidrun/agent/planner/planner_agent.py +0 -311
  50. droidrun/agent/planner/prompts.py +0 -124
  51. droidrun-0.3.10.dev2.dist-info/RECORD +0 -70
  52. {droidrun-0.3.10.dev2.dist-info → droidrun-0.3.10.dev4.dist-info}/WHEEL +0 -0
  53. {droidrun-0.3.10.dev2.dist-info → droidrun-0.3.10.dev4.dist-info}/entry_points.txt +0 -0
  54. {droidrun-0.3.10.dev2.dist-info → droidrun-0.3.10.dev4.dist-info}/licenses/LICENSE +0 -0
@@ -5,155 +5,6 @@ Prompts for the ManagerAgent.
5
5
  import re
6
6
 
7
7
 
8
- def build_manager_system_prompt(
9
- instruction: str,
10
- has_text_to_modify: bool = False,
11
- app_card: str = "",
12
- device_date: str = "",
13
- important_notes: str = "",
14
- error_flag: bool = False,
15
- error_history: list = [], # noqa: B006
16
- custom_tools_descriptions: str = ""
17
- ) -> str:
18
- """
19
- Build the manager system prompt with all context.
20
-
21
- Args:
22
- instruction: User's goal/task
23
- has_text_to_modify: Whether focused text field has editable content
24
- app_card: App-specific instructions (TODO: implement app card system)
25
- device_date: Current device date (TODO: implement via adb shell date)
26
- important_notes: Additional important information
27
- error_flag: Whether consecutive errors occurred
28
- error_history: List of recent errors if error_flag=True
29
- custom_tools_descriptions: Formatted descriptions of custom tools available to executor
30
-
31
- Returns:
32
- Complete system prompt for Manager
33
- """
34
- prompt = (
35
- "You are an agent who can operate an Android phone on behalf of a user. "
36
- "Your goal is to track progress and devise high-level plans to achieve the user's requests.\n\n"
37
- "<user_request>\n"
38
- f"{instruction}\n"
39
- "</user_request>\n\n"
40
- )
41
-
42
-
43
- if device_date.strip():
44
- prompt += f"<device_date>\n{device_date}\n</device_date>\n\n"
45
-
46
-
47
-
48
-
49
- if app_card.strip():
50
- prompt += "App card gives information on how to operate the app and perform actions.\n"
51
- prompt += f"<app_card>\n{app_card.strip()}\n</app_card>\n\n"
52
-
53
- # Important notes
54
- if important_notes:
55
- prompt += "<important_notes>\n"
56
- prompt += f"{important_notes}\n"
57
- prompt += "</important_notes>\n\n"
58
-
59
- # Error escalation
60
- if error_flag and error_history:
61
- prompt += (
62
- "<potentially_stuck>\n"
63
- "You have encountered several failed attempts. Here are some logs:\n"
64
- )
65
- for error in error_history:
66
- prompt += (
67
- f"- Attempt: Action: {error['action']} | "
68
- f"Description: {error['summary']} | "
69
- f"Outcome: Failed | "
70
- f"Feedback: {error['error']}\n"
71
- )
72
- prompt += "</potentially_stuck>\n\n"
73
-
74
- # Guidelines
75
- prompt += """<guidelines>
76
- The following guidelines will help you plan this request.
77
- General:
78
- 1. Use the `open_app` action whenever you want to open an app, do not use the app drawer to open an app.
79
- 2. Use search to quickly find a file or entry with a specific name, if search function is applicable.
80
- 3. Only use copy to clipboard actions when the task specifically requires copying text to clipboard. Do not copy text just to use it later - use the Memory section instead.
81
- 4. When you need to remember information for later use, store it in the Memory section (using <add_memory> tags) with step context (e.g., "At step X, I obtained [information] from [source]").
82
- 5. File names in the user request must always match the exact file name you are working with, make that reflect in the plan too.
83
- 6. Make sure names and titles are not cutoff. If the request is to check who sent a message, make sure to check the message sender's full name not just what appears in the notification because it might be cut off.
84
- 7. Dates and file names must match the user query exactly.
85
- 8. Don't do more than what the user asks for."""
86
-
87
- # Text manipulation guidelines (conditional)
88
- if has_text_to_modify:
89
- prompt += """
90
-
91
- <text_manipulation>
92
- 1. Use **TEXT_TASK:** prefix in your plan when you need to modify text in the currently focused text input field
93
- 2. TEXT_TASK is for editing, formatting, or transforming existing text content in text boxes using Python code
94
- 3. Do not use TEXT_TASK for extracting text from messages, typing new text, or composing messages
95
- 4. The focused text field contains editable text that you can modify
96
- 5. Example plan item: 'TEXT_TASK: Add "Hello World" at the beginning of the text'
97
- 6. Always use TEXT_TASK for modifying text, do not try to select the text to copy/cut/paste or adjust the text
98
- </text_manipulation>"""
99
-
100
- prompt += """
101
-
102
- Memory Usage:
103
- - Always include step context: "At step [number], I obtained [actual content] from [source]"
104
- - Store the actual content you observe, not just references (e.g., store full recipe text, not "found recipes")
105
- - Use memory instead of copying text unless specifically requested
106
- - Memory is append-only: whatever you put in <add_memory> tags gets added to existing memory, not replaced
107
- - Update memory to track progress on multi-step tasks
108
-
109
- </guidelines>"""
110
-
111
- # Add custom tools section if custom tools are provided
112
- if custom_tools_descriptions.strip():
113
- prompt += """
114
-
115
- <custom_actions>
116
- The executor has access to these additional custom actions beyond the standard actions (click, type, swipe, etc.):
117
- """ + custom_tools_descriptions + """
118
-
119
- You can reference these custom actions or tell the Executer agent to use them in your plan when they help achieve the user's goal.
120
- </custom_actions>"""
121
-
122
- prompt += """
123
- ---
124
- Carefully assess the current status and the provided screenshot. Check if the current plan needs to be revised.
125
- Determine if the user request has been fully completed. If you are confident that no further actions are required, use the request_accomplished tag with a message in it. If the user request is not finished, update the plan and don't use it. If you are stuck with errors, think step by step about whether the overall plan needs to be revised to address the error.
126
- NOTE: 1. If the current situation prevents proceeding with the original plan or requires clarification from the user, make reasonable assumptions and revise the plan accordingly. Act as though you are the user in such cases. 2. Please refer to the helpful information and steps in the Guidelines first for planning. 3. If the first subgoal in plan has been completed, please update the plan in time according to the screenshot and progress to ensure that the next subgoal is always the first item in the plan. 4. If the first subgoal is not completed, please copy the previous round's plan or update the plan based on the completion of the subgoal.
127
- Provide your output in the following format, which contains four or five parts:
128
-
129
- <thought>
130
- An explanation of your rationale for the updated plan and current subgoal.
131
- </thought>
132
-
133
- <add_memory>
134
- Store important information here with step context for later reference. Always include "At step X, I obtained [actual content] from [source]".
135
- Examples:
136
- - At step 5, I obtained recipe details from recipes.jpg: Recipe 1 "Chicken Pasta" - ingredients: chicken, pasta, cream. Instructions: Cook pasta, sauté chicken, add cream.
137
- or
138
- - At step 12, I successfully added Recipe 1 to Broccoli app. Still need to add Recipe 2 and Recipe 3 from memory.
139
- Store important information here with step context for later reference.
140
- </add_memory>
141
-
142
- <plan>
143
- Please update or copy the existing plan according to the current page and progress. Please pay close attention to the historical operations. Please do not repeat the plan of completed content unless you can judge from the screen status that a subgoal is indeed not completed.
144
- </plan>
145
-
146
- <request_accomplished>
147
- Use this tag ONLY after actually completing the user's request through concrete actions, not at the beginning or for planning.
148
-
149
- 1. Always include a message inside this tag confirming what you accomplished
150
- 2. Ensure both opening and closing tags are present
151
- 3. Use exclusively for signaling completed user requests
152
- </request_accomplished>"""
153
-
154
- return prompt
155
-
156
-
157
8
  def parse_manager_response(response: str) -> dict:
158
9
  """
159
10
  Parse manager LLM response into structured dict.
@@ -163,7 +14,6 @@ def parse_manager_response(response: str) -> dict:
163
14
  - <add_memory>...</add_memory>
164
15
  - <plan>...</plan>
165
16
  - <request_accomplished>...</request_accomplished> (answer)
166
- - <historical_operations>...</historical_operations> (optional, for completed plan)
167
17
 
168
18
  Also derives:
169
19
  - current_subgoal: first line of plan (with list markers removed)
@@ -177,8 +27,7 @@ def parse_manager_response(response: str) -> dict:
177
27
  - memory: str
178
28
  - plan: str
179
29
  - current_subgoal: str (first line of plan, cleaned)
180
- - completed_subgoal: str
181
- - answer: str (from request_accomplished tag)
30
+ - request_accomplished: str (from request_accomplished tag)
182
31
  """
183
32
  def extract(tag: str) -> str:
184
33
  """Extract content between XML-style tags."""
@@ -191,12 +40,6 @@ def parse_manager_response(response: str) -> dict:
191
40
  plan = extract("plan")
192
41
  answer = extract("request_accomplished")
193
42
 
194
- # Extract completed subgoal (optional historical_operations tag)
195
- if "<historical_operations>" in response:
196
- completed_subgoal = extract("historical_operations")
197
- else:
198
- completed_subgoal = "No completed subgoal."
199
-
200
43
  # Parse current subgoal from first line of plan
201
44
  current_goal_text = plan
202
45
  # Prefer newline-separated plans; take the first non-empty line
@@ -215,7 +58,6 @@ def parse_manager_response(response: str) -> dict:
215
58
 
216
59
  return {
217
60
  "thought": thought,
218
- "completed_subgoal": completed_subgoal,
219
61
  "plan": plan,
220
62
  "memory": memory_section,
221
63
  "current_subgoal": current_subgoal,
@@ -155,6 +155,47 @@ async def add_packages_block(packages, chat_history: List[ChatMessage]) -> List[
155
155
  chat_history[-1].blocks.append(ui_block)
156
156
  return chat_history
157
157
 
158
+ async def add_device_state_block(
159
+ formatted_device_state: str,
160
+ chat_history: List[ChatMessage],
161
+ copy: bool = True
162
+ ) -> List[ChatMessage]:
163
+ """
164
+ Add formatted device state to the LAST user message in chat history.
165
+
166
+ This follows the pattern of other chat_utils functions:
167
+ - Doesn't create a new message
168
+ - Appends to last user message content
169
+ - Prevents device state from being saved to every message in memory
170
+
171
+ Args:
172
+ formatted_device_state: Complete formatted device state text
173
+ chat_history: Current chat history
174
+ copy: Whether to copy the history before modifying (default: True)
175
+
176
+ Returns:
177
+ Updated chat history with device state in last user message
178
+ """
179
+ if not formatted_device_state or not formatted_device_state.strip():
180
+ return chat_history
181
+
182
+ if not chat_history:
183
+ return chat_history
184
+
185
+ # Create device state block
186
+ device_state_block = TextBlock(text=f"\n{formatted_device_state}\n")
187
+
188
+ # Copy history if requested
189
+ if copy:
190
+ chat_history = chat_history.copy()
191
+ chat_history[-1] = message_copy(chat_history[-1])
192
+
193
+ # Append to last message blocks
194
+ chat_history[-1].blocks.append(device_state_block)
195
+
196
+ return chat_history
197
+
198
+
158
199
  async def add_memory_block(memory: List[str], chat_history: List[ChatMessage]) -> List[ChatMessage]:
159
200
  memory_block = "\n### Remembered Information:\n"
160
201
  for idx, item in enumerate(memory, 1):
@@ -294,6 +335,27 @@ def has_non_empty_content(msg):
294
335
  return True
295
336
  return False
296
337
 
297
- @clean_span("remove_empty_messages")
298
338
  def remove_empty_messages(messages):
299
- return [msg for msg in messages if has_non_empty_content(msg)]
339
+ """Remove empty messages and duplicates, with span decoration."""
340
+ if not messages or all(has_non_empty_content(msg) for msg in messages):
341
+ return messages
342
+
343
+ @clean_span("remove_empty_messages")
344
+ def process_messages():
345
+ # Remove empty messages first
346
+ cleaned = [msg for msg in messages if has_non_empty_content(msg)]
347
+
348
+ # Remove duplicates based on content
349
+ seen_contents = set()
350
+ unique_messages = []
351
+ for msg in cleaned:
352
+ content = msg.get('content', [])
353
+ content_str = str(content) # Simple string representation for deduplication
354
+ if content_str not in seen_contents:
355
+ seen_contents.add(content_str)
356
+ unique_messages.append(msg)
357
+
358
+ logger.debug(f"Removed empty messages and duplicates: {len(messages)} -> {len(unique_messages)}")
359
+ return unique_messages
360
+
361
+ return process_messages()
@@ -101,50 +101,75 @@ def format_ui_elements(ui_data: List[Dict[str, Any]], level: int = 0) -> str:
101
101
  return "\n".join(formatted_lines)
102
102
 
103
103
 
104
- def get_device_state_exact_format(state: Dict[str, Any]) -> Tuple[str, str]:
104
+ def format_device_state(state: Dict[str, Any]) -> Tuple[str, str, List[Dict], Dict]:
105
105
  """
106
- Get device state in exactly the format requested:
107
-
108
- **Current Phone State:**
109
- • **App:** App Name (package.name)
110
- • **Keyboard:** Hidden/Visible
111
- • **Focused Element:** 'text'
106
+ Format device state with all necessary data.
112
107
 
113
- Current Clickable UI elements from the device in the schema 'index. className: resourceId, text - bounds(x1,y1,x2,y2)':
114
- 1. ClassName: "resourceId", "text" - (x1, y1, x2, y2)
108
+ Returns formatted text for prompts plus raw components for storage.
115
109
 
116
110
  Args:
117
- state: Dictionary containing device state data from collector.get_device_state()
111
+ state: Dictionary containing device state data from tools.get_state()
118
112
 
119
113
  Returns:
120
- Tuple of (formatted_string, focused_text) where focused_text is the actual
121
- text content of the focused element, or empty string if none.
114
+ Tuple of:
115
+ - formatted_text (str): Complete formatted device state for prompts
116
+ - focused_text (str): Text content of focused element (empty if none)
117
+ - a11y_tree (List[Dict]): Raw accessibility tree
118
+ - phone_state (Dict): Raw phone state dict
122
119
  """
123
120
  try:
124
121
  if "error" in state:
125
- return (f"Error getting device state: {state.get('message', 'Unknown error')}", "")
122
+ error_msg = f"Error getting device state: {state.get('message', 'Unknown error')}"
123
+ return (error_msg, "", [], {})
126
124
 
127
- # Extract focused element text
125
+ # Extract raw components
128
126
  phone_state = state.get("phone_state", {})
127
+ a11y_tree = state.get("a11y_tree", [])
128
+
129
+ # Extract focused element text
129
130
  focused_element = phone_state.get('focusedElement')
130
131
  focused_text = ""
131
132
  if focused_element:
132
133
  focused_text = focused_element.get('text', '')
133
134
 
134
- # Format the state data
135
+ # Format phone state section
135
136
  phone_state_text = format_phone_state(phone_state)
136
- ui_data = state.get("a11y_tree", [])
137
- if ui_data:
138
- formatted_ui = format_ui_elements(ui_data)
139
- ui_elements_text = f"Current Clickable UI elements from the device in the schema 'index. className: resourceId, text - bounds(x1,y1,x2,y2)':\n{formatted_ui}"
137
+
138
+ # Format UI elements section
139
+ if a11y_tree:
140
+ formatted_ui = format_ui_elements(a11y_tree)
141
+ ui_elements_text = (
142
+ "Current Clickable UI elements from the device in the schema "
143
+ "'index. className: resourceId, text - bounds(x1,y1,x2,y2)':\n"
144
+ f"{formatted_ui}"
145
+ )
140
146
  else:
141
- ui_elements_text = "Current Clickable UI elements from the device in the schema 'index. className: resourceId, text - bounds(x1,y1,x2,y2)':\nNo UI elements found"
147
+ ui_elements_text = (
148
+ "Current Clickable UI elements from the device in the schema "
149
+ "'index. className: resourceId, text - bounds(x1,y1,x2,y2)':\n"
150
+ "No UI elements found"
151
+ )
152
+
153
+ # Combine into complete formatted text
154
+ formatted_text = f"{phone_state_text}\n\n{ui_elements_text}"
142
155
 
143
- formatted_string = f"{phone_state_text}\n \n\n{ui_elements_text}"
156
+ # Return all 4 components
157
+ return (formatted_text, focused_text, a11y_tree, phone_state)
144
158
 
145
- return (formatted_string, focused_text)
146
159
  except Exception as e:
147
- return (f"Error getting device state: {e}", "")
160
+ return (f"Error formatting device state: {e}", "", [], {})
161
+
162
+
163
+ # Backward compatibility alias
164
+ def get_device_state_exact_format(state: Dict[str, Any]) -> Tuple[str, str]:
165
+ """
166
+ Deprecated: Use format_device_state() instead.
167
+
168
+ This function is kept for backward compatibility with ManagerAgent and ExecutorAgent.
169
+ Returns only the first two values (formatted_text, focused_text).
170
+ """
171
+ formatted_text, focused_text, _, _ = format_device_state(state)
172
+ return (formatted_text, focused_text)
148
173
 
149
174
 
150
175
  def main():
@@ -167,10 +192,13 @@ def main():
167
192
  ]
168
193
  }
169
194
 
170
- formatted_string, focused_text = get_device_state_exact_format(example_state)
171
- print("Formatted String:")
172
- print(formatted_string)
195
+ # Test new format_device_state function
196
+ formatted_text, focused_text, a11y_tree, phone_state = format_device_state(example_state)
197
+ print("Formatted Text:")
198
+ print(formatted_text)
173
199
  print(f"\nFocused Text: '{focused_text}'")
200
+ print(f"\nA11y Tree: {a11y_tree}")
201
+ print(f"\nPhone State: {phone_state}")
174
202
 
175
203
 
176
204
  if __name__ == "__main__":
@@ -1,19 +1,20 @@
1
- import asyncio
2
- import contextlib
3
1
  import io
4
- import logging
5
- import threading
2
+ import contextlib
6
3
  import traceback
4
+ import logging
5
+ from typing import Any, Dict, Optional
7
6
  from asyncio import AbstractEventLoop
8
- from typing import Any, Dict
9
-
10
- from llama_index.core.workflow import Context
11
-
12
- from droidrun.agent.utils.async_utils import async_to_sync
13
- from droidrun.tools.adb import AdbTools
7
+ from pydantic import BaseModel
14
8
 
15
9
  logger = logging.getLogger("droidrun")
16
10
 
11
+ class ExecuterState(BaseModel):
12
+ """State object for the code executor."""
13
+ ui_state: Optional[Any] = None
14
+
15
+ class Config:
16
+ arbitrary_types_allowed = True
17
+
17
18
 
18
19
  class SimpleCodeExecutor:
19
20
  """
@@ -28,122 +29,107 @@ class SimpleCodeExecutor:
28
29
  def __init__(
29
30
  self,
30
31
  loop: AbstractEventLoop,
31
- locals: Dict[str, Any] = {}, # noqa: B006
32
- globals: Dict[str, Any] = {}, # noqa: B006
33
- tools={}, # noqa: B006
34
- tools_instance=None,
32
+ locals: Dict[str, Any] = None,
33
+ globals: Dict[str, Any] = None,
34
+ tools=None,
35
35
  use_same_scope: bool = True,
36
36
  ):
37
37
  """
38
38
  Initialize the code executor.
39
39
 
40
40
  Args:
41
+ loop: The event loop to use for async execution
41
42
  locals: Local variables to use in the execution context
42
43
  globals: Global variables to use in the execution context
43
- tools: List of tools available for execution
44
- tools_instance: Original tools instance (e.g., AdbTools instance)
44
+ tools: Dict or list of tools available for execution
45
+ use_same_scope: Whether to use the same scope for globals and locals
45
46
  """
46
-
47
- self.tools_instance = tools_instance
48
-
49
- # loop throught tools and add them to globals, but before that check if tool value is async, if so convert it to sync. tools is a dictionary of tool name: function
50
- # e.g. tools = {'tool_name': tool_function}
51
-
52
- # check if tools is a dictionary
47
+ if locals is None:
48
+ locals = {}
49
+ if globals is None:
50
+ globals = {}
51
+ if tools is None:
52
+ tools = {}
53
+
54
+ # Add tools to globals
53
55
  if isinstance(tools, dict):
54
- logger.debug(
55
- f"🔧 Initializing SimpleCodeExecutor with tools: {tools.items()}"
56
- )
57
- for tool_name, tool_function in tools.items():
58
- if asyncio.iscoroutinefunction(tool_function):
59
- # If the function is async, convert it to sync
60
- tool_function = async_to_sync(tool_function)
61
- # Add the tool to globals
62
- globals[tool_name] = tool_function
56
+ logger.debug(f"🔧 Initializing SimpleCodeExecutor with tools: {list(tools.keys())}")
57
+ globals.update(tools)
63
58
  elif isinstance(tools, list):
64
- logger.debug(f"🔧 Initializing SimpleCodeExecutor with tools: {tools}")
65
- # If tools is a list, convert it to a dictionary with tool name as key and function as value
59
+ logger.debug(f"🔧 Initializing SimpleCodeExecutor with {len(tools)} tools")
66
60
  for tool in tools:
67
- if asyncio.iscoroutinefunction(tool):
68
- # If the function is async, convert it to sync
69
- tool = async_to_sync(tool)
70
- # Add the tool to globals
71
61
  globals[tool.__name__] = tool
72
62
  else:
73
63
  raise ValueError("Tools must be a dictionary or a list of functions.")
74
64
 
65
+ # Add common imports
75
66
  import time
76
-
77
67
  globals["time"] = time
78
68
 
79
69
  self.globals = globals
80
70
  self.locals = locals
81
71
  self.loop = loop
82
72
  self.use_same_scope = use_same_scope
83
- self.tools = tools
73
+
84
74
  if self.use_same_scope:
85
- # If using the same scope, set the globals and locals to the same dictionary
75
+ # If using the same scope, merge globals and locals
86
76
  self.globals = self.locals = {
87
77
  **self.locals,
88
78
  **{k: v for k, v in self.globals.items() if k not in self.locals},
89
79
  }
90
80
 
91
- async def execute(self, ctx: Context, code: str) -> str:
81
+ def _execute_in_thread(self, code: str, ui_state: Any) -> str:
92
82
  """
93
- Execute Python code and capture output and return values.
94
-
95
- Args:
96
- code: Python code to execute
97
-
98
- Returns:
99
- str: Output from the execution, including print statements.
83
+ Execute code synchronously in a thread.
84
+ All async tools will be called synchronously here.
100
85
  """
101
- # Update UI elements before execution
102
- self.globals['ui_state'] = await ctx.store.get("ui_state", None)
103
- self.globals['step_screenshots'] = []
104
- self.globals['step_ui_states'] = []
105
-
106
- if self.tools_instance and isinstance(self.tools_instance, AdbTools):
107
- self.tools_instance._set_context(ctx)
108
-
86
+ # Update UI state
87
+ self.globals['ui_state'] = ui_state
88
+
109
89
  # Capture stdout and stderr
110
90
  stdout = io.StringIO()
111
91
  stderr = io.StringIO()
112
92
 
113
93
  output = ""
114
94
  try:
115
- # Execute with captured output
116
- thread_exception = []
117
95
  with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
118
-
119
- def execute_code():
120
- try:
121
- exec(code, self.globals, self.locals)
122
- except Exception as e:
123
- import traceback
124
-
125
- thread_exception.append((e, traceback.format_exc()))
126
-
127
- t = threading.Thread(target=execute_code)
128
- t.start()
129
- t.join()
96
+ # Just exec the code directly - no async needed!
97
+ exec(code, self.globals, self.locals)
130
98
 
131
99
  # Get output
132
100
  output = stdout.getvalue()
133
101
  if stderr.getvalue():
134
102
  output += "\n" + stderr.getvalue()
135
- if thread_exception:
136
- e, tb = thread_exception[0]
137
- output += f"\nError: {type(e).__name__}: {str(e)}\n{tb}"
138
103
 
139
104
  except Exception as e:
140
105
  # Capture exception information
141
106
  output = f"Error: {type(e).__name__}: {str(e)}\n"
142
107
  output += traceback.format_exc()
143
108
 
144
- result = {
145
- 'output': output,
146
- 'screenshots': self.globals['step_screenshots'],
147
- 'ui_states': self.globals['step_ui_states'],
148
- }
149
- return result
109
+ return output
110
+
111
+ async def execute(self, state: ExecuterState, code: str) -> str:
112
+ """
113
+ Execute Python code and capture output and return values.
114
+
115
+ Runs the code in a separate thread to prevent blocking.
116
+
117
+ Args:
118
+ state: ExecuterState containing ui_state and other execution context.
119
+ code: Python code to execute
120
+
121
+ Returns:
122
+ str: Output from the execution, including print statements.
123
+ """
124
+ # Get UI state from the state object
125
+ ui_state = state.ui_state
126
+
127
+ # Run the execution in a thread pool executor
128
+ output = await self.loop.run_in_executor(
129
+ None,
130
+ self._execute_in_thread,
131
+ code,
132
+ ui_state
133
+ )
134
+
135
+ return output
@@ -1,11 +1,12 @@
1
1
 
2
+ import asyncio
2
3
  import contextvars
3
4
  import threading
4
5
  import time
5
6
  from concurrent.futures import TimeoutError as FuturesTimeoutError
6
- import asyncio
7
7
  from typing import Any, Optional
8
8
 
9
+
9
10
  def call_with_retries(llm, messages, retries=3, timeout=500, delay=1.0):
10
11
  last_exception = None
11
12
 
@@ -67,26 +68,26 @@ async def acall_with_retries(
67
68
  ) -> Any:
68
69
  """
69
70
  Call LLM with retries and timeout handling.
70
-
71
+
71
72
  Args:
72
73
  llm: The LLM client instance
73
74
  messages: List of messages to send
74
75
  retries: Number of retry attempts
75
76
  timeout: Timeout in seconds for each attempt
76
77
  delay: Base delay between retries (multiplied by attempt number)
77
-
78
+
78
79
  Returns:
79
80
  The LLM response object
80
81
  """
81
82
  last_exception: Optional[Exception] = None
82
-
83
+
83
84
  for attempt in range(1, retries + 1):
84
85
  try:
85
86
  response = await asyncio.wait_for(
86
87
  llm.achat(messages=messages), # Use achat() instead of chat()
87
88
  timeout=timeout
88
89
  )
89
-
90
+
90
91
  # Validate response
91
92
  if (
92
93
  response is not None
@@ -97,18 +98,18 @@ async def acall_with_retries(
97
98
  else:
98
99
  print(f"Attempt {attempt} returned empty content")
99
100
  last_exception = ValueError("Empty response content")
100
-
101
+
101
102
  except asyncio.TimeoutError:
102
103
  print(f"Attempt {attempt} timed out after {timeout} seconds")
103
104
  last_exception = TimeoutError("Timed out")
104
-
105
+
105
106
  except Exception as e:
106
107
  print(f"Attempt {attempt} failed with error: {e!r}")
107
108
  last_exception = e
108
-
109
+
109
110
  if attempt < retries:
110
111
  await asyncio.sleep(delay * attempt)
111
-
112
+
112
113
  if last_exception:
113
114
  raise last_exception
114
- raise ValueError("All attempts returned empty response content")
115
+ raise ValueError("All attempts returned empty response content")