droidrun 0.2.0__py3-none-any.whl → 0.3.1__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 +16 -11
- droidrun/__main__.py +1 -1
- droidrun/adb/__init__.py +3 -3
- droidrun/adb/device.py +1 -1
- droidrun/adb/manager.py +2 -2
- droidrun/agent/__init__.py +6 -0
- droidrun/agent/codeact/__init__.py +2 -4
- droidrun/agent/codeact/codeact_agent.py +330 -235
- droidrun/agent/codeact/events.py +12 -20
- droidrun/agent/codeact/prompts.py +0 -52
- droidrun/agent/common/default.py +5 -0
- droidrun/agent/common/events.py +4 -0
- droidrun/agent/context/__init__.py +23 -0
- droidrun/agent/context/agent_persona.py +15 -0
- droidrun/agent/context/context_injection_manager.py +66 -0
- droidrun/agent/context/episodic_memory.py +15 -0
- droidrun/agent/context/personas/__init__.py +11 -0
- droidrun/agent/context/personas/app_starter.py +44 -0
- droidrun/agent/context/personas/default.py +95 -0
- droidrun/agent/context/personas/extractor.py +52 -0
- droidrun/agent/context/personas/ui_expert.py +107 -0
- droidrun/agent/context/reflection.py +20 -0
- droidrun/agent/context/task_manager.py +124 -0
- droidrun/agent/droid/__init__.py +2 -2
- droidrun/agent/droid/droid_agent.py +269 -325
- droidrun/agent/droid/events.py +28 -0
- droidrun/agent/oneflows/reflector.py +265 -0
- droidrun/agent/planner/__init__.py +2 -4
- droidrun/agent/planner/events.py +9 -13
- droidrun/agent/planner/planner_agent.py +288 -0
- droidrun/agent/planner/prompts.py +33 -53
- droidrun/agent/utils/__init__.py +3 -0
- droidrun/agent/utils/async_utils.py +1 -40
- droidrun/agent/utils/chat_utils.py +265 -48
- droidrun/agent/utils/executer.py +49 -14
- droidrun/agent/utils/llm_picker.py +14 -10
- droidrun/agent/utils/trajectory.py +184 -0
- droidrun/cli/__init__.py +1 -1
- droidrun/cli/logs.py +283 -0
- droidrun/cli/main.py +364 -441
- droidrun/tools/__init__.py +5 -10
- droidrun/tools/{actions.py → adb.py} +381 -412
- droidrun/tools/ios.py +596 -0
- droidrun/tools/tools.py +95 -0
- droidrun-0.3.1.dist-info/METADATA +150 -0
- droidrun-0.3.1.dist-info/RECORD +50 -0
- droidrun/agent/planner/task_manager.py +0 -355
- droidrun/agent/planner/workflow.py +0 -371
- droidrun/tools/device.py +0 -29
- droidrun/tools/loader.py +0 -60
- droidrun-0.2.0.dist-info/METADATA +0 -373
- droidrun-0.2.0.dist-info/RECORD +0 -32
- {droidrun-0.2.0.dist-info → droidrun-0.3.1.dist-info}/WHEEL +0 -0
- {droidrun-0.2.0.dist-info → droidrun-0.3.1.dist-info}/entry_points.txt +0 -0
- {droidrun-0.2.0.dist-info → droidrun-0.3.1.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
|
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.
|
28
|
-
|
29
|
-
|
30
|
-
|
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 `
|
42
|
-
*
|
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 `
|
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 `
|
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 `
|
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
|
-
* `
|
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:**
|
71
|
+
**User Goal:** Open Gmail and compose a new email.
|
71
72
|
|
72
73
|
**(Round 1) Planner Input:**
|
73
|
-
* Goal: "
|
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
|
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
|
-
|
85
|
-
|
86
|
-
|
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
|
89
|
+
**(After specialized agents perform these steps...)**
|
91
90
|
|
92
91
|
**(Round 2) Planner Input:**
|
93
|
-
* Goal: "
|
94
|
-
* Current State: Screenshot of
|
95
|
-
* Task History: Shows
|
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
|
-
|
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}"""
|
@@ -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
|
-
|
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
|
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
|
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
|
-
|
29
|
-
|
105
|
+
if ui_state:
|
106
|
+
# Parse the JSON and format it in natural language
|
30
107
|
try:
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
except
|
35
|
-
if
|
36
|
-
|
37
|
-
|
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(
|
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,182 @@ async def add_screenshot_image_block(tools: 'Tools', chat_history: List[ChatMess
|
|
55
128
|
return chat_history
|
56
129
|
|
57
130
|
|
58
|
-
async def
|
59
|
-
|
60
|
-
#
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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', '')
|
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', '')
|
143
|
+
element_class = focused_element.get('className', '')
|
144
|
+
element_resource_id = focused_element.get('resourceId', '')
|
145
|
+
|
146
|
+
# Build focused element description
|
147
|
+
focused_desc = f"'{element_text}' {element_class}"
|
148
|
+
if element_resource_id:
|
149
|
+
focused_desc += f" | ID: {element_resource_id}"
|
150
|
+
else:
|
151
|
+
focused_desc = "None"
|
152
|
+
|
153
|
+
phone_state_text = f"""
|
154
|
+
**Current Phone State:**
|
155
|
+
• **App:** {current_app} ({package_name})
|
156
|
+
• **Keyboard:** {'Visible' if keyboard_visible else 'Hidden'}
|
157
|
+
• **Focused Element:** {focused_desc}
|
158
|
+
"""
|
159
|
+
else:
|
160
|
+
# Handle error cases or malformed data
|
161
|
+
if isinstance(phone_state, dict) and 'error' in phone_state:
|
162
|
+
phone_state_text = f"\n📱 **Phone State Error:** {phone_state.get('message', 'Unknown error')}\n"
|
163
|
+
else:
|
164
|
+
phone_state_text = f"\n📱 **Phone State:** {phone_state}\n"
|
165
|
+
|
166
|
+
ui_block = TextBlock(text=phone_state_text)
|
167
|
+
chat_history = chat_history.copy()
|
168
|
+
chat_history[-1] = message_copy(chat_history[-1])
|
169
|
+
chat_history[-1].blocks.append(ui_block)
|
84
170
|
return chat_history
|
85
171
|
|
86
|
-
async def
|
87
|
-
|
88
|
-
ui_block = TextBlock(text=f"\
|
172
|
+
async def add_packages_block(packages, chat_history: List[ChatMessage]) -> List[ChatMessage]:
|
173
|
+
|
174
|
+
ui_block = TextBlock(text=f"\nInstalled packages: {packages}\n```\n")
|
89
175
|
chat_history = chat_history.copy()
|
90
176
|
chat_history[-1] = message_copy(chat_history[-1])
|
91
177
|
chat_history[-1].blocks.append(ui_block)
|
92
|
-
return chat_history
|
178
|
+
return chat_history
|
179
|
+
|
180
|
+
async def add_memory_block(memory: List[str], chat_history: List[ChatMessage]) -> List[ChatMessage]:
|
181
|
+
memory_block = "\n### Remembered Information:\n"
|
182
|
+
for idx, item in enumerate(memory, 1):
|
183
|
+
memory_block += f"{idx}. {item}\n"
|
184
|
+
|
185
|
+
for i, msg in enumerate(chat_history):
|
186
|
+
if msg.role == "user":
|
187
|
+
if isinstance(msg.content, str):
|
188
|
+
updated_content = f"{memory_block}\n\n{msg.content}"
|
189
|
+
chat_history[i] = ChatMessage(role="user", content=updated_content)
|
190
|
+
elif isinstance(msg.content, list):
|
191
|
+
memory_text_block = TextBlock(text=memory_block)
|
192
|
+
content_blocks = [memory_text_block] + msg.content
|
193
|
+
chat_history[i] = ChatMessage(role="user", content=content_blocks)
|
194
|
+
break
|
195
|
+
return chat_history
|
196
|
+
|
197
|
+
async def get_reflection_block(reflections: List[Reflection]) -> ChatMessage:
|
198
|
+
reflection_block = "\n### You also have additional Knowledge to help you guide your current task from previous expierences:\n"
|
199
|
+
for reflection in reflections:
|
200
|
+
reflection_block += f"**{reflection.advice}\n"
|
201
|
+
|
202
|
+
return ChatMessage(role="user", content=reflection_block)
|
203
|
+
|
204
|
+
async def add_task_history_block(completed_tasks: list[dict], failed_tasks: list[dict], chat_history: List[ChatMessage]) -> List[ChatMessage]:
|
205
|
+
task_history = ""
|
206
|
+
|
207
|
+
# Combine all tasks and show in chronological order
|
208
|
+
all_tasks = completed_tasks + failed_tasks
|
209
|
+
|
210
|
+
if all_tasks:
|
211
|
+
task_history += "Task History (chronological order):\n"
|
212
|
+
for i, task in enumerate(all_tasks, 1):
|
213
|
+
if hasattr(task, 'description'):
|
214
|
+
status_indicator = "[success]" if hasattr(task, 'status') and task.status == "completed" else "[failed]"
|
215
|
+
task_history += f"{i}. {status_indicator} {task.description}\n"
|
216
|
+
elif isinstance(task, dict):
|
217
|
+
# For backward compatibility with dict format
|
218
|
+
task_description = task.get('description', str(task))
|
219
|
+
status_indicator = "[success]" if task in completed_tasks else "[failed]"
|
220
|
+
task_history += f"{i}. {status_indicator} {task_description}\n"
|
221
|
+
else:
|
222
|
+
status_indicator = "[success]" if task in completed_tasks else "[failed]"
|
223
|
+
task_history += f"{i}. {status_indicator} {task}\n"
|
224
|
+
|
225
|
+
|
226
|
+
task_block = TextBlock(text=f"{task_history}")
|
227
|
+
|
228
|
+
chat_history = chat_history.copy()
|
229
|
+
chat_history[-1] = message_copy(chat_history[-1])
|
230
|
+
chat_history[-1].blocks.append(task_block)
|
231
|
+
return chat_history
|
232
|
+
|
233
|
+
def parse_tool_descriptions(tool_list) -> str:
|
234
|
+
"""Parses the available tools and their descriptions for the system prompt."""
|
235
|
+
logger.info("🛠️ Parsing tool descriptions...")
|
236
|
+
tool_descriptions = []
|
237
|
+
|
238
|
+
for tool in tool_list.values():
|
239
|
+
assert callable(tool), f"Tool {tool} is not callable."
|
240
|
+
tool_name = tool.__name__
|
241
|
+
tool_signature = inspect.signature(tool)
|
242
|
+
tool_docstring = tool.__doc__ or "No description available."
|
243
|
+
formatted_signature = f"def {tool_name}{tool_signature}:\n \"\"\"{tool_docstring}\"\"\"\n..."
|
244
|
+
tool_descriptions.append(formatted_signature)
|
245
|
+
logger.debug(f" - Parsed tool: {tool_name}")
|
246
|
+
descriptions = "\n".join(tool_descriptions)
|
247
|
+
logger.info(f"🔩 Found {len(tool_descriptions)} tools.")
|
248
|
+
return descriptions
|
249
|
+
|
250
|
+
|
251
|
+
def parse_persona_description(personas) -> str:
|
252
|
+
"""Parses the available agent personas and their descriptions for the system prompt."""
|
253
|
+
logger.debug("👥 Parsing agent persona descriptions for Planner Agent...")
|
254
|
+
|
255
|
+
if not personas:
|
256
|
+
logger.warning("No agent personas provided to Planner Agent")
|
257
|
+
return "No specialized agents available."
|
258
|
+
|
259
|
+
persona_descriptions = []
|
260
|
+
for persona in personas:
|
261
|
+
# Format each persona with name, description, and expertise areas
|
262
|
+
expertise_list = ", ".join(persona.expertise_areas) if persona.expertise_areas else "General tasks"
|
263
|
+
formatted_persona = f"- **{persona.name}**: {persona.description}\n Expertise: {expertise_list}"
|
264
|
+
persona_descriptions.append(formatted_persona)
|
265
|
+
logger.debug(f" - Parsed persona: {persona.name}")
|
266
|
+
|
267
|
+
# Join all persona descriptions into a single string
|
268
|
+
descriptions = "\n".join(persona_descriptions)
|
269
|
+
logger.debug(f"👤 Found {len(persona_descriptions)} agent personas.")
|
270
|
+
return descriptions
|
271
|
+
|
272
|
+
|
273
|
+
def extract_code_and_thought(response_text: str) -> Tuple[Optional[str], str]:
|
274
|
+
"""
|
275
|
+
Extracts code from Markdown blocks (```python ... ```) and the surrounding text (thought),
|
276
|
+
handling indented code blocks.
|
277
|
+
|
278
|
+
Returns:
|
279
|
+
Tuple[Optional[code_string], thought_string]
|
280
|
+
"""
|
281
|
+
logger.debug("✂️ Extracting code and thought from response...")
|
282
|
+
code_pattern = r"^\s*```python\s*\n(.*?)\n^\s*```\s*?$"
|
283
|
+
code_matches = list(re.finditer(code_pattern, response_text, re.DOTALL | re.MULTILINE))
|
284
|
+
|
285
|
+
if not code_matches:
|
286
|
+
logger.debug(" - No code block found. Entire response is thought.")
|
287
|
+
return None, response_text.strip()
|
288
|
+
|
289
|
+
extracted_code_parts = []
|
290
|
+
for match in code_matches:
|
291
|
+
code_content = match.group(1)
|
292
|
+
extracted_code_parts.append(code_content)
|
293
|
+
|
294
|
+
extracted_code = "\n\n".join(extracted_code_parts)
|
295
|
+
|
296
|
+
|
297
|
+
thought_parts = []
|
298
|
+
last_end = 0
|
299
|
+
for match in code_matches:
|
300
|
+
start, end = match.span(0)
|
301
|
+
thought_parts.append(response_text[last_end:start])
|
302
|
+
last_end = end
|
303
|
+
thought_parts.append(response_text[last_end:])
|
304
|
+
|
305
|
+
thought_text = "".join(thought_parts).strip()
|
306
|
+
thought_preview = (thought_text[:100] + '...') if len(thought_text) > 100 else thought_text
|
307
|
+
logger.debug(f" - Extracted thought: {thought_preview}")
|
308
|
+
|
309
|
+
return extracted_code, thought_text
|