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.
- droidrun/agent/codeact/__init__.py +1 -4
- droidrun/agent/codeact/codeact_agent.py +95 -86
- droidrun/agent/codeact/events.py +1 -2
- droidrun/agent/context/__init__.py +5 -9
- droidrun/agent/context/episodic_memory.py +1 -3
- droidrun/agent/context/task_manager.py +8 -2
- droidrun/agent/droid/droid_agent.py +102 -141
- droidrun/agent/droid/events.py +45 -14
- droidrun/agent/executor/__init__.py +6 -4
- droidrun/agent/executor/events.py +29 -9
- droidrun/agent/executor/executor_agent.py +86 -28
- droidrun/agent/executor/prompts.py +8 -2
- droidrun/agent/manager/__init__.py +6 -7
- droidrun/agent/manager/events.py +16 -4
- droidrun/agent/manager/manager_agent.py +130 -69
- droidrun/agent/manager/prompts.py +1 -159
- droidrun/agent/utils/chat_utils.py +64 -2
- droidrun/agent/utils/device_state_formatter.py +54 -26
- droidrun/agent/utils/executer.py +66 -80
- droidrun/agent/utils/inference.py +11 -10
- droidrun/agent/utils/tools.py +58 -6
- droidrun/agent/utils/trajectory.py +18 -12
- droidrun/cli/logs.py +118 -56
- droidrun/cli/main.py +154 -136
- droidrun/config_manager/__init__.py +9 -7
- droidrun/config_manager/app_card_loader.py +148 -0
- droidrun/config_manager/config_manager.py +200 -102
- droidrun/config_manager/path_resolver.py +104 -0
- droidrun/config_manager/prompt_loader.py +75 -0
- droidrun/macro/__init__.py +1 -1
- droidrun/macro/cli.py +23 -18
- droidrun/telemetry/__init__.py +2 -2
- droidrun/telemetry/events.py +3 -3
- droidrun/telemetry/tracker.py +1 -1
- droidrun/tools/adb.py +1 -1
- droidrun/tools/ios.py +3 -2
- {droidrun-0.3.10.dev2.dist-info → droidrun-0.3.10.dev4.dist-info}/METADATA +10 -3
- droidrun-0.3.10.dev4.dist-info/RECORD +61 -0
- droidrun/agent/codeact/prompts.py +0 -26
- droidrun/agent/context/agent_persona.py +0 -16
- droidrun/agent/context/context_injection_manager.py +0 -66
- droidrun/agent/context/personas/__init__.py +0 -11
- droidrun/agent/context/personas/app_starter.py +0 -44
- droidrun/agent/context/personas/big_agent.py +0 -96
- droidrun/agent/context/personas/default.py +0 -95
- droidrun/agent/context/personas/ui_expert.py +0 -108
- droidrun/agent/planner/__init__.py +0 -13
- droidrun/agent/planner/events.py +0 -21
- droidrun/agent/planner/planner_agent.py +0 -311
- droidrun/agent/planner/prompts.py +0 -124
- droidrun-0.3.10.dev2.dist-info/RECORD +0 -70
- {droidrun-0.3.10.dev2.dist-info → droidrun-0.3.10.dev4.dist-info}/WHEEL +0 -0
- {droidrun-0.3.10.dev2.dist-info → droidrun-0.3.10.dev4.dist-info}/entry_points.txt +0 -0
- {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
|
-
-
|
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
|
-
|
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
|
104
|
+
def format_device_state(state: Dict[str, Any]) -> Tuple[str, str, List[Dict], Dict]:
|
105
105
|
"""
|
106
|
-
|
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
|
-
|
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
|
111
|
+
state: Dictionary containing device state data from tools.get_state()
|
118
112
|
|
119
113
|
Returns:
|
120
|
-
Tuple of
|
121
|
-
|
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
|
-
|
122
|
+
error_msg = f"Error getting device state: {state.get('message', 'Unknown error')}"
|
123
|
+
return (error_msg, "", [], {})
|
126
124
|
|
127
|
-
# Extract
|
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
|
135
|
+
# Format phone state section
|
135
136
|
phone_state_text = format_phone_state(phone_state)
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
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 =
|
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
|
-
|
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
|
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
|
-
|
171
|
-
|
172
|
-
print(
|
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__":
|
droidrun/agent/utils/executer.py
CHANGED
@@ -1,19 +1,20 @@
|
|
1
|
-
import asyncio
|
2
|
-
import contextlib
|
3
1
|
import io
|
4
|
-
import
|
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
|
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] =
|
32
|
-
globals: Dict[str, Any] =
|
33
|
-
tools=
|
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:
|
44
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
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
|
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
|
-
|
73
|
+
|
84
74
|
if self.use_same_scope:
|
85
|
-
# If using the same scope,
|
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
|
-
|
81
|
+
def _execute_in_thread(self, code: str, ui_state: Any) -> str:
|
92
82
|
"""
|
93
|
-
Execute
|
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
|
102
|
-
self.globals['ui_state'] =
|
103
|
-
|
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
|
-
|
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
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
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")
|