droidrun 0.2.0__py3-none-any.whl → 0.3.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.
Files changed (57) hide show
  1. droidrun/__init__.py +16 -11
  2. droidrun/__main__.py +1 -1
  3. droidrun/adb/__init__.py +3 -3
  4. droidrun/adb/device.py +1 -1
  5. droidrun/adb/manager.py +2 -2
  6. droidrun/agent/__init__.py +6 -0
  7. droidrun/agent/codeact/__init__.py +2 -4
  8. droidrun/agent/codeact/codeact_agent.py +321 -235
  9. droidrun/agent/codeact/events.py +12 -20
  10. droidrun/agent/codeact/prompts.py +0 -52
  11. droidrun/agent/common/default.py +5 -0
  12. droidrun/agent/common/events.py +4 -0
  13. droidrun/agent/context/__init__.py +23 -0
  14. droidrun/agent/context/agent_persona.py +15 -0
  15. droidrun/agent/context/context_injection_manager.py +66 -0
  16. droidrun/agent/context/episodic_memory.py +15 -0
  17. droidrun/agent/context/personas/__init__.py +11 -0
  18. droidrun/agent/context/personas/app_starter.py +44 -0
  19. droidrun/agent/context/personas/default.py +95 -0
  20. droidrun/agent/context/personas/extractor.py +52 -0
  21. droidrun/agent/context/personas/ui_expert.py +107 -0
  22. droidrun/agent/context/reflection.py +20 -0
  23. droidrun/agent/context/task_manager.py +124 -0
  24. droidrun/agent/context/todo.txt +4 -0
  25. droidrun/agent/droid/__init__.py +2 -2
  26. droidrun/agent/droid/droid_agent.py +264 -325
  27. droidrun/agent/droid/events.py +28 -0
  28. droidrun/agent/oneflows/reflector.py +265 -0
  29. droidrun/agent/planner/__init__.py +2 -4
  30. droidrun/agent/planner/events.py +9 -13
  31. droidrun/agent/planner/planner_agent.py +268 -0
  32. droidrun/agent/planner/prompts.py +33 -53
  33. droidrun/agent/utils/__init__.py +3 -0
  34. droidrun/agent/utils/async_utils.py +1 -40
  35. droidrun/agent/utils/chat_utils.py +268 -48
  36. droidrun/agent/utils/executer.py +49 -14
  37. droidrun/agent/utils/llm_picker.py +14 -10
  38. droidrun/agent/utils/trajectory.py +184 -0
  39. droidrun/cli/__init__.py +1 -1
  40. droidrun/cli/logs.py +283 -0
  41. droidrun/cli/main.py +333 -439
  42. droidrun/run.py +105 -0
  43. droidrun/tools/__init__.py +5 -10
  44. droidrun/tools/{actions.py → adb.py} +279 -238
  45. droidrun/tools/ios.py +594 -0
  46. droidrun/tools/tools.py +99 -0
  47. droidrun-0.3.0.dist-info/METADATA +149 -0
  48. droidrun-0.3.0.dist-info/RECORD +52 -0
  49. droidrun/agent/planner/task_manager.py +0 -355
  50. droidrun/agent/planner/workflow.py +0 -371
  51. droidrun/tools/device.py +0 -29
  52. droidrun/tools/loader.py +0 -60
  53. droidrun-0.2.0.dist-info/METADATA +0 -373
  54. droidrun-0.2.0.dist-info/RECORD +0 -32
  55. {droidrun-0.2.0.dist-info → droidrun-0.3.0.dist-info}/WHEEL +0 -0
  56. {droidrun-0.2.0.dist-info → droidrun-0.3.0.dist-info}/entry_points.txt +0 -0
  57. {droidrun-0.2.0.dist-info → droidrun-0.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -6,7 +6,7 @@ separated from the workflow logic for better maintainability.
6
6
  """
7
7
 
8
8
  # System prompt for the PlannerAgent that explains its role and capabilities
9
- DEFAULT_PLANNER_SYSTEM_PROMPT = """You are an Android Task Planner. Your job is to create short, functional plans (1-5 steps) to achieve a user's goal on an Android device.
9
+ DEFAULT_PLANNER_SYSTEM_PROMPT = """You are an Android Task Planner. Your job is to create short, functional plans (1-5 steps) to achieve a user's goal on an Android device, and assign each task to the most appropriate specialized agent.
10
10
 
11
11
  **Inputs You Receive:**
12
12
  1. **User's Overall Goal.**
@@ -20,34 +20,34 @@ DEFAULT_PLANNER_SYSTEM_PROMPT = """You are an Android Task Planner. Your job is
20
20
  * For failed tasks, the detailed reasons for failure.
21
21
  * This history persists across all planning cycles and is never lost, even when creating new tasks.
22
22
 
23
+ **Available Specialized Agents:**
24
+ You have access to specialized agents, each optimized for specific types of tasks:
25
+ {agents}
26
+
23
27
  **Your Task:**
24
- Given the goal, current state, and task history, devise the **next 1-5 functional steps**. Focus on what to achieve, not how. Planning fewer steps at a time improves accuracy, as the state can change.
28
+ Given the goal, current state, and task history, devise the **next 1-5 functional steps** and assign each to the most appropriate specialized agent.
29
+ Focus on what to achieve, not how. Planning fewer steps at a time improves accuracy, as the state can change.
25
30
 
26
31
  **Step Format:**
27
- Each step must be a functional goal. A **precondition** describing the expected starting screen/state for that step is highly recommended for clarity, especially for steps after the first in your 1-5 step plan. Each task string can start with "Precondition: ... Goal: ...". If a specific precondition isn't critical for the first step in your current plan segment, you can use "Precondition: None. Goal: ..." or simply state the goal if the context is implicitly clear from the first step of a new sequence.
28
-
29
- **Executor Agent Capabilities:**
30
- The plan you create will be executed by another agent. This executor can:
31
- * `swipe(direction: str, distance_percentage: int)`
32
- * `input_text(text: str, element_hint: Optional[str] = None)`
33
- * `press_key(keycode: int)` (Common: 3=HOME, 4=BACK)
34
- * `tap_by_coordinates(x: int, y: int)` (This is a fallback; prefer functional goals)
35
- * `start_app(package_name: str)`
36
- * `remember(info: str)
37
- `
38
- * (The executor will use the UI JSON to find elements for your functional goals like "Tap 'Settings button'" or "Enter text into 'Username field'").
32
+ Each step must be a functional goal.
33
+ A **precondition** describing the expected starting screen/state for that step is highly recommended for clarity, especially for steps after the first in your 1-5 step plan.
34
+ Each task string can start with "Precondition: ... Goal: ...".
35
+ If a specific precondition isn't critical for the first step in your current plan segment, you can use "Precondition: None. Goal: ..." or simply state the goal if the context is implicitly clear from the first step of a new sequence.
39
36
 
40
37
  **Your Output:**
41
- * Use the `set_tasks` tool to provide your 1-5 step plan as a list of strings.
42
- * **After your planned steps are executed, you will be invoked again with the new device state.** You will then:
38
+ * Use the `set_tasks_with_agents` tool to provide your 1-5 step plan with agent assignments.
39
+ * Each task should be assigned to a specialized agent using it's name.
40
+
41
+ * **After your planned steps are executed, you will be invoked again with the new device state.**
42
+ You will then:
43
43
  1. Assess if the **overall user goal** is complete.
44
44
  2. If complete, call the `complete_goal(message: str)` tool.
45
- 3. If not complete, generate the next 1-5 steps using `set_tasks`.
45
+ 3. If not complete, generate the next 1-5 steps using `set_tasks_with_agents`.
46
46
 
47
47
  **Memory Persistence:**
48
48
  * You maintain a COMPLETE memory of ALL tasks across the entire session:
49
49
  * Every task that was completed or failed is preserved in your context.
50
- * Previously completed steps are never lost when calling `set_tasks()` for new steps.
50
+ * Previously completed steps are never lost when calling `set_tasks_with_agents()` for new steps.
51
51
  * You will see all historical tasks each time you're called.
52
52
  * Use this accumulated knowledge to build progressively on successful steps.
53
53
  * When you see discovered information (e.g., dates, locations), use it explicitly in future tasks.
@@ -57,67 +57,47 @@ The plan you create will be executed by another agent. This executor can:
57
57
  * **NO Low-Level Actions:** Do NOT specify swipes, taps on coordinates, or element IDs in your plan.
58
58
  * **Short Plans (1-5 steps):** Plan only the immediate next actions.
59
59
  * **Learn From History:** If a task failed previously, try a different approach.
60
- * **Use Tools:** Your response *must* be a Python code block calling `set_tasks` or `complete_goal`.
60
+ * **Use Tools:** Your response *must* be a Python code block calling `set_tasks_with_agents` or `complete_goal`.
61
+ * **Smart Agent Assignment:** Choose the most appropriate agent for each task type.
61
62
 
62
63
  **Available Planning Tools:**
63
- * `set_tasks(tasks: List[str])`: Defines the sequence of tasks. Each element in the list is a string representing a single task.
64
+ * `set_tasks_with_agents(task_assignments: List[Dict[str, str]])`: Defines the sequence of tasks with agent assignments. Each element should be a dictionary with 'task' and 'agent' keys.
64
65
  * `complete_goal(message: str)`: Call this when the overall user goal has been achieved. The message can summarize the completion.
65
66
 
66
67
  ---
67
68
 
68
69
  **Example Interaction Flow:**
69
70
 
70
- **User Goal:** Turn on Wi-Fi.
71
+ **User Goal:** Open Gmail and compose a new email.
71
72
 
72
73
  **(Round 1) Planner Input:**
73
- * Goal: "Turn on Wi-Fi"
74
+ * Goal: "Open Gmail and compose a new email"
74
75
  * Current State: Screenshot of Home screen, UI JSON.
75
76
  * Task History: None (first planning cycle)
76
77
 
77
78
  **Planner Thought Process (Round 1):**
78
- Need to open settings first, then go to Network settings. This is the first plan.
79
- 1. Task 1: "Precondition: None. Goal: Open the Settings app."
80
- 2. Task 2: "Precondition: Settings main screen is open. Goal: Navigate to 'Network & internet' settings."
79
+ Need to first open Gmail app, then navigate to compose. The first task is app launching, the second is UI navigation.
81
80
 
82
81
  **Planner Output (Round 1):**
83
82
  ```python
84
- set_tasks(tasks=[
85
- "Precondition: None. Goal: Open the Settings app.",
86
- "Precondition: Settings main screen is open. Goal: Navigate to 'Network & internet' settings."
83
+ set_tasks_with_agents([
84
+ {{'task': 'Precondition: None. Goal: Open the Gmail app.', 'agent': <Specialized_Agent>}},
85
+ {{'task': 'Precondition: Gmail app is open and loaded. Goal: Navigate to compose new email.', 'agent': <Specialized Agents>}}
87
86
  ])
88
87
  ```
89
88
 
90
- **(After Executor performs these steps...)**
89
+ **(After specialized agents perform these steps...)**
91
90
 
92
91
  **(Round 2) Planner Input:**
93
- * Goal: "Turn on Wi-Fi"
94
- * Current State: Screenshot of "Network & internet" screen, UI JSON showing "Wi-Fi" option.
95
- * Task History: Shows ALL previously completed tasks, including "Open the Settings app" and "Navigate to 'Network & internet' settings"
96
-
97
- **Planner Thought Process (Round 2):**
98
- Now on "Network & internet". Need to tap Wi-Fi, then enable it. I can see from history that we've already opened Settings and navigated to Network & internet.
99
- 1. Task 1: "Precondition: 'Network & internet' screen is open. Goal: Tap the 'Wi-Fi' option."
100
- 2. Task 2: "Precondition: Wi-Fi settings screen is open. Goal: Enable the Wi-Fi toggle if it's off."
92
+ * Goal: "Open Gmail and compose a new email"
93
+ * Current State: Screenshot of Gmail compose screen, UI JSON showing compose interface.
94
+ * Task History: Shows completed tasks with their assigned agents
101
95
 
102
96
  **Planner Output (Round 2):**
103
97
  ```python
104
- set_tasks(tasks=[
105
- "Precondition: 'Network & internet' screen is open. Goal: Tap the 'Wi-Fi' option.",
106
- "Precondition: Wi-Fi settings screen is open. Goal: Enable the Wi-Fi toggle if it's off."
107
- ])
98
+ complete_goal(message="Gmail has been opened and compose email screen is ready for use.")
108
99
  ```
109
-
110
- **(After Executor performs these steps...)**
111
-
112
- **(Round 3) Planner Input:**
113
- * Goal: "Turn on Wi-Fi"
114
- * Current State: Screenshot of Wi-Fi screen, UI JSON showing Wi-Fi is now ON.
115
- * Task History: Shows ALL previous tasks completed successfully (all 4 tasks from previous rounds)
116
-
117
- **Planner Output (Round 3):**
118
- ```python
119
- complete_goal(message="Wi-Fi has been successfully enabled.")
120
- ```"""
100
+ """
121
101
 
122
102
  # User prompt template that simply states the goal
123
103
  DEFAULT_PLANNER_USER_PROMPT = """Goal: {goal}"""
@@ -0,0 +1,3 @@
1
+ """
2
+ Utility modules for DroidRun agents.
3
+ """
@@ -1,7 +1,4 @@
1
1
  import asyncio
2
- import nest_asyncio
3
- nest_asyncio_applied = False
4
-
5
2
 
6
3
  def async_to_sync(func):
7
4
  """
@@ -15,42 +12,6 @@ def async_to_sync(func):
15
12
  """
16
13
 
17
14
  def wrapper(*args, **kwargs):
18
- global nest_asyncio_applied # Declare modification of global at the start of the scope
19
- coro = func(*args, **kwargs)
20
- try:
21
- # Try to get the running event loop.
22
- loop = asyncio.get_running_loop()
23
-
24
- # If the loop is running, apply nest_asyncio if available and needed.
25
- # Removed global declaration from here
26
- if nest_asyncio and not nest_asyncio_applied:
27
- nest_asyncio.apply()
28
- nest_asyncio_applied = True
29
- # Run the coroutine to completion within the running loop.
30
- # This requires nest_asyncio to work correctly in nested scenarios.
31
- # Changed from ensure_future to run_until_complete to make it truly synchronous.
32
- return loop.run_until_complete(coro)
33
-
34
- except RuntimeError:
35
- # No running event loop found.
36
- try:
37
- # Check if there's a loop policy and a current event loop set, even if not running.
38
- loop = asyncio.get_event_loop_policy().get_event_loop()
39
- if loop.is_running():
40
- # This case should ideally be caught by get_running_loop(),
41
- # but as a fallback, handle similarly if loop is running.
42
- # Removed global declaration from here
43
- if nest_asyncio and not nest_asyncio_applied:
44
- nest_asyncio.apply()
45
- nest_asyncio_applied = True
46
- return loop.run_until_complete(coro)
47
- else:
48
- # Loop exists but is not running, run until complete.
49
- return loop.run_until_complete(coro)
50
- except RuntimeError:
51
- # If get_event_loop() also fails (no loop set at all for this thread),
52
- # use asyncio.run() which creates a new loop.
53
- return asyncio.run(coro)
54
-
15
+ return asyncio.run(func(*args, **kwargs))
55
16
 
56
17
  return wrapper
@@ -1,14 +1,18 @@
1
1
  import base64
2
+ import re
3
+ import inspect
4
+
5
+
2
6
  import json
3
7
  import logging
4
- from typing import List, TYPE_CHECKING
8
+ from typing import List, TYPE_CHECKING, Optional, Tuple
9
+ from droidrun.agent.context import Reflection
5
10
  from llama_index.core.base.llms.types import ChatMessage, ImageBlock, TextBlock
6
11
 
7
12
  if TYPE_CHECKING:
8
- from ...tools import Tools
13
+ from droidrun.tools import Tools
9
14
 
10
15
  logger = logging.getLogger("droidrun")
11
- logging.basicConfig(level=logging.INFO)
12
16
 
13
17
  def message_copy(message: ChatMessage, deep = True) -> ChatMessage:
14
18
  if deep:
@@ -23,29 +27,98 @@ def message_copy(message: ChatMessage, deep = True) -> ChatMessage:
23
27
 
24
28
  return copied_message
25
29
 
26
- async def add_ui_text_block(tools: 'Tools', chat_history: List[ChatMessage], retry = 5, copy = True) -> List[ChatMessage]:
30
+ async def add_reflection_summary(reflection: Reflection, chat_history: List[ChatMessage]) -> List[ChatMessage]:
31
+ """Add reflection summary and advice to help the planner understand what went wrong and what to do differently."""
32
+
33
+ reflection_text = "\n### The last task failed. You have additional information about what happenend. \nThe Reflection from Previous Attempt:\n"
34
+
35
+ if reflection.summary:
36
+ reflection_text += f"**What happened:** {reflection.summary}\n\n"
37
+
38
+ if reflection.advice:
39
+ reflection_text += f"**Recommended approach for this retry:** {reflection.advice}\n"
40
+
41
+ reflection_block = TextBlock(text=reflection_text)
42
+
43
+ # Copy chat_history and append reflection block to the last message
44
+ chat_history = chat_history.copy()
45
+ chat_history[-1] = message_copy(chat_history[-1])
46
+ chat_history[-1].blocks.append(reflection_block)
47
+
48
+ return chat_history
49
+
50
+ def _format_ui_elements(ui_data, level=0) -> str:
51
+ """Format UI elements in natural language: index. className: resourceId, text - bounds"""
52
+ if not ui_data:
53
+ return ""
54
+
55
+ formatted_lines = []
56
+ indent = " " * level # Indentation for nested elements
57
+
58
+ # Handle both list and single element
59
+ elements = ui_data if isinstance(ui_data, list) else [ui_data]
60
+
61
+ for element in elements:
62
+ if not isinstance(element, dict):
63
+ continue
64
+
65
+ # Extract element properties
66
+ index = element.get('index', '')
67
+ class_name = element.get('className', '')
68
+ resource_id = element.get('resourceId', '')
69
+ text = element.get('text', '')
70
+ bounds = element.get('bounds', '')
71
+ children = element.get('children', [])
72
+
73
+
74
+ # Format the line: index. className: resourceId, text - bounds
75
+ line_parts = []
76
+ if index != '':
77
+ line_parts.append(f"{index}.")
78
+ if class_name:
79
+ line_parts.append(class_name + ":")
80
+
81
+ details = []
82
+ if resource_id:
83
+ details.append(f'"{resource_id}"')
84
+ if text:
85
+ details.append(f'"{text}"')
86
+ if details:
87
+ line_parts.append(", ".join(details))
88
+
89
+ if bounds:
90
+ line_parts.append(f"- ({bounds})")
91
+
92
+ formatted_line = f"{indent}{' '.join(line_parts)}"
93
+ formatted_lines.append(formatted_line)
94
+
95
+ # Recursively format children with increased indentation
96
+ if children:
97
+ child_formatted = _format_ui_elements(children, level + 1)
98
+ if child_formatted:
99
+ formatted_lines.append(child_formatted)
100
+
101
+ return "\n".join(formatted_lines)
102
+
103
+ async def add_ui_text_block(ui_state: str, chat_history: List[ChatMessage], copy = True) -> List[ChatMessage]:
27
104
  """Add UI elements to the chat history without modifying the original."""
28
- ui_elements = None
29
- for i in range(retry):
105
+ if ui_state:
106
+ # Parse the JSON and format it in natural language
30
107
  try:
31
- ui_elements = await tools.get_clickables()
32
- if ui_elements:
33
- break
34
- except Exception as e:
35
- if i < 4:
36
- logger.warning(f" - Error getting UI elements: {e}. Retrying...")
37
- else:
38
- logger.error(f" - Error getting UI elements: {e}. No UI elements will be sent.")
39
- if ui_elements:
40
- ui_block = TextBlock(text="\nCurrent Clickable UI elements from the device using the custom TopViewService:\n```json\n" + json.dumps(ui_elements) + "\n```\n")
108
+ ui_data = json.loads(ui_state) if isinstance(ui_state, str) else ui_state
109
+ formatted_ui = _format_ui_elements(ui_data)
110
+ ui_block = TextBlock(text=f"\nCurrent Clickable UI elements from the device in the schema 'index. className: resourceId, text - bounds(x1,y1,x2,y2)':\n{formatted_ui}\n")
111
+ except (json.JSONDecodeError, TypeError):
112
+ # Fallback to original format if parsing fails
113
+ ui_block = TextBlock(text="\nCurrent Clickable UI elements from the device using the custom TopViewService:\n```json\n" + json.dumps(ui_state) + "\n```\n")
114
+
41
115
  if copy:
42
116
  chat_history = chat_history.copy()
43
117
  chat_history[-1] = message_copy(chat_history[-1])
44
118
  chat_history[-1].blocks.append(ui_block)
45
119
  return chat_history
46
120
 
47
- async def add_screenshot_image_block(tools: 'Tools', chat_history: List[ChatMessage], retry: int = 5, copy = True) -> None:
48
- screenshot = await take_screenshot(tools, retry)
121
+ async def add_screenshot_image_block(screenshot, chat_history: List[ChatMessage], copy = True) -> None:
49
122
  if screenshot:
50
123
  image_block = ImageBlock(image=base64.b64encode(screenshot))
51
124
  if copy:
@@ -55,38 +128,185 @@ async def add_screenshot_image_block(tools: 'Tools', chat_history: List[ChatMess
55
128
  return chat_history
56
129
 
57
130
 
58
- async def take_screenshot(tools: 'Tools', retry: int = 5) -> None:
59
- """Take a screenshot and return the image."""
60
- # Retry taking screenshot
61
- tools.last_screenshot = None
62
- for i in range(retry):
63
- try:
64
- await tools.take_screenshot()
65
- if tools.last_screenshot:
66
- break
67
- except Exception as e:
68
- if i < 4:
69
- logger.warning(f" - Error taking screenshot: {e}. Retrying...")
70
- else:
71
- logger.error(f" - Error taking screenshot: {e}. No screenshot will be sent.")
72
- return None
73
- screenshot = tools.last_screenshot
74
- tools.last_screenshot = None # Reset last screenshot after taking it
75
- return screenshot
76
-
77
- async def add_screenshot(chat_history: List[ChatMessage], screenshot, copy = True) -> List[ChatMessage]:
78
- """Add a screenshot to the chat history."""
79
- image_block = ImageBlock(image=base64.b64encode(screenshot))
80
- if copy:
81
- chat_history[-1] = message_copy(chat_history[-1])
82
- chat_history = chat_history.copy() # Create a copy of chat history to avoid modifying the original
83
- chat_history[-1].blocks.append(image_block)
131
+ async def add_phone_state_block(phone_state, chat_history: List[ChatMessage]) -> List[ChatMessage]:
132
+
133
+ # Format the phone state data nicely
134
+ if isinstance(phone_state, dict) and 'error' not in phone_state:
135
+ current_app = phone_state.get('currentApp', 'Unknown')
136
+ package_name = phone_state.get('packageName', 'Unknown')
137
+ keyboard_visible = phone_state.get('keyboardVisible', False)
138
+ focused_element = phone_state.get('focusedElement')
139
+
140
+ # Format the focused element
141
+ if focused_element:
142
+ element_text = focused_element.get('text', 'No text')
143
+ element_class = focused_element.get('className', 'Unknown')
144
+ element_bounds = focused_element.get('bounds', 'Unknown')
145
+ element_type = focused_element.get('type', 'unknown')
146
+ element_resource_id = focused_element.get('resourceId', '')
147
+
148
+ # Build focused element description
149
+ focused_desc = f"'{element_text}' ({element_class})"
150
+ if element_resource_id:
151
+ focused_desc += f" | ID: {element_resource_id}"
152
+ focused_desc += f" | Bounds: {element_bounds} | Type: {element_type}"
153
+ else:
154
+ focused_desc = "None"
155
+
156
+ phone_state_text = f"""
157
+ **Current Phone State:**
158
+ • **App:** {current_app} ({package_name})
159
+ • **Keyboard:** {'Visible' if keyboard_visible else 'Hidden'}
160
+ • **Focused Element:** {focused_desc}
161
+ """
162
+ else:
163
+ # Handle error cases or malformed data
164
+ if isinstance(phone_state, dict) and 'error' in phone_state:
165
+ phone_state_text = f"\n📱 **Phone State Error:** {phone_state.get('message', 'Unknown error')}\n"
166
+ else:
167
+ phone_state_text = f"\n📱 **Phone State:** {phone_state}\n"
168
+
169
+ ui_block = TextBlock(text=phone_state_text)
170
+ chat_history = chat_history.copy()
171
+ chat_history[-1] = message_copy(chat_history[-1])
172
+ chat_history[-1].blocks.append(ui_block)
84
173
  return chat_history
85
174
 
86
- async def add_phone_state_block(tools: 'Tools', chat_history: List[ChatMessage]) -> List[ChatMessage]:
87
- phone_state = await tools.get_phone_state()
88
- ui_block = TextBlock(text=f"\nCurrent Phone state: {phone_state}\n```\n")
175
+ async def add_packages_block(packages, chat_history: List[ChatMessage]) -> List[ChatMessage]:
176
+
177
+ ui_block = TextBlock(text=f"\nInstalled packages: {packages}\n```\n")
89
178
  chat_history = chat_history.copy()
90
179
  chat_history[-1] = message_copy(chat_history[-1])
91
180
  chat_history[-1].blocks.append(ui_block)
92
- return chat_history
181
+ return chat_history
182
+
183
+ async def add_memory_block(memory: List[str], chat_history: List[ChatMessage]) -> List[ChatMessage]:
184
+ memory_block = "\n### Remembered Information:\n"
185
+ for idx, item in enumerate(memory, 1):
186
+ memory_block += f"{idx}. {item}\n"
187
+
188
+ for i, msg in enumerate(chat_history):
189
+ if msg.role == "user":
190
+ if isinstance(msg.content, str):
191
+ updated_content = f"{memory_block}\n\n{msg.content}"
192
+ chat_history[i] = ChatMessage(role="user", content=updated_content)
193
+ elif isinstance(msg.content, list):
194
+ memory_text_block = TextBlock(text=memory_block)
195
+ content_blocks = [memory_text_block] + msg.content
196
+ chat_history[i] = ChatMessage(role="user", content=content_blocks)
197
+ break
198
+ return chat_history
199
+
200
+ async def get_reflection_block(reflections: List[Reflection]) -> ChatMessage:
201
+ reflection_block = "\n### You also have additional Knowledge to help you guide your current task from previous expierences:\n"
202
+ for reflection in reflections:
203
+ reflection_block += f"**{reflection.advice}\n"
204
+
205
+ return ChatMessage(role="user", content=reflection_block)
206
+
207
+ async def add_task_history_block(completed_tasks: list[dict], failed_tasks: list[dict], chat_history: List[ChatMessage]) -> List[ChatMessage]:
208
+ task_history = ""
209
+
210
+ # Combine all tasks and show in chronological order
211
+ all_tasks = completed_tasks + failed_tasks
212
+
213
+ if all_tasks:
214
+ task_history += "Task History (chronological order):\n"
215
+ for i, task in enumerate(all_tasks, 1):
216
+ if hasattr(task, 'description'):
217
+ status_indicator = "[success]" if hasattr(task, 'status') and task.status == "completed" else "[failed]"
218
+ task_history += f"{i}. {status_indicator} {task.description}\n"
219
+ elif isinstance(task, dict):
220
+ # For backward compatibility with dict format
221
+ task_description = task.get('description', str(task))
222
+ status_indicator = "[success]" if task in completed_tasks else "[failed]"
223
+ task_history += f"{i}. {status_indicator} {task_description}\n"
224
+ else:
225
+ status_indicator = "[success]" if task in completed_tasks else "[failed]"
226
+ task_history += f"{i}. {status_indicator} {task}\n"
227
+
228
+
229
+ task_block = TextBlock(text=f"{task_history}")
230
+
231
+ chat_history = chat_history.copy()
232
+ chat_history[-1] = message_copy(chat_history[-1])
233
+ chat_history[-1].blocks.append(task_block)
234
+ return chat_history
235
+
236
+ def parse_tool_descriptions(tool_list) -> str:
237
+ """Parses the available tools and their descriptions for the system prompt."""
238
+ logger.info("🛠️ Parsing tool descriptions...")
239
+ tool_descriptions = []
240
+
241
+ for tool in tool_list.values():
242
+ assert callable(tool), f"Tool {tool} is not callable."
243
+ tool_name = tool.__name__
244
+ tool_signature = inspect.signature(tool)
245
+ tool_docstring = tool.__doc__ or "No description available."
246
+ formatted_signature = f"def {tool_name}{tool_signature}:\n \"\"\"{tool_docstring}\"\"\"\n..."
247
+ tool_descriptions.append(formatted_signature)
248
+ logger.debug(f" - Parsed tool: {tool_name}")
249
+ descriptions = "\n".join(tool_descriptions)
250
+ logger.info(f"🔩 Found {len(tool_descriptions)} tools.")
251
+ return descriptions
252
+
253
+
254
+ def parse_persona_description(personas) -> str:
255
+ """Parses the available agent personas and their descriptions for the system prompt."""
256
+ logger.debug("👥 Parsing agent persona descriptions for Planner Agent...")
257
+
258
+ if not personas:
259
+ logger.warning("No agent personas provided to Planner Agent")
260
+ return "No specialized agents available."
261
+
262
+ persona_descriptions = []
263
+ for persona in personas:
264
+ # Format each persona with name, description, and expertise areas
265
+ expertise_list = ", ".join(persona.expertise_areas) if persona.expertise_areas else "General tasks"
266
+ formatted_persona = f"- **{persona.name}**: {persona.description}\n Expertise: {expertise_list}"
267
+ persona_descriptions.append(formatted_persona)
268
+ logger.debug(f" - Parsed persona: {persona.name}")
269
+
270
+ # Join all persona descriptions into a single string
271
+ descriptions = "\n".join(persona_descriptions)
272
+ logger.debug(f"👤 Found {len(persona_descriptions)} agent personas.")
273
+ return descriptions
274
+
275
+
276
+ def extract_code_and_thought(response_text: str) -> Tuple[Optional[str], str]:
277
+ """
278
+ Extracts code from Markdown blocks (```python ... ```) and the surrounding text (thought),
279
+ handling indented code blocks.
280
+
281
+ Returns:
282
+ Tuple[Optional[code_string], thought_string]
283
+ """
284
+ logger.debug("✂️ Extracting code and thought from response...")
285
+ code_pattern = r"^\s*```python\s*\n(.*?)\n^\s*```\s*?$"
286
+ code_matches = list(re.finditer(code_pattern, response_text, re.DOTALL | re.MULTILINE))
287
+
288
+ if not code_matches:
289
+ logger.debug(" - No code block found. Entire response is thought.")
290
+ return None, response_text.strip()
291
+
292
+ extracted_code_parts = []
293
+ for match in code_matches:
294
+ code_content = match.group(1)
295
+ extracted_code_parts.append(code_content)
296
+
297
+ extracted_code = "\n\n".join(extracted_code_parts)
298
+
299
+
300
+ thought_parts = []
301
+ last_end = 0
302
+ for match in code_matches:
303
+ start, end = match.span(0)
304
+ thought_parts.append(response_text[last_end:start])
305
+ last_end = end
306
+ thought_parts.append(response_text[last_end:])
307
+
308
+ thought_text = "".join(thought_parts).strip()
309
+ thought_preview = (thought_text[:100] + '...') if len(thought_text) > 100 else thought_text
310
+ logger.debug(f" - Extracted thought: {thought_preview}")
311
+
312
+ return extracted_code, thought_text