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.
- 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 +321 -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/context/todo.txt +4 -0
- droidrun/agent/droid/__init__.py +2 -2
- droidrun/agent/droid/droid_agent.py +264 -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 +268 -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 +268 -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 +333 -439
- droidrun/run.py +105 -0
- droidrun/tools/__init__.py +5 -10
- droidrun/tools/{actions.py → adb.py} +279 -238
- droidrun/tools/ios.py +594 -0
- droidrun/tools/tools.py +99 -0
- droidrun-0.3.0.dist-info/METADATA +149 -0
- droidrun-0.3.0.dist-info/RECORD +52 -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.0.dist-info}/WHEEL +0 -0
- {droidrun-0.2.0.dist-info → droidrun-0.3.0.dist-info}/entry_points.txt +0 -0
- {droidrun-0.2.0.dist-info → droidrun-0.3.0.dist-info}/licenses/LICENSE +0 -0
droidrun/agent/utils/executer.py
CHANGED
@@ -2,9 +2,16 @@ import io
|
|
2
2
|
import contextlib
|
3
3
|
import ast
|
4
4
|
import traceback
|
5
|
+
import logging
|
5
6
|
from typing import Any, Dict
|
6
|
-
from .async_utils import async_to_sync
|
7
|
+
from droidrun.agent.utils.async_utils import async_to_sync
|
8
|
+
from llama_index.core.workflow import Context
|
7
9
|
import asyncio
|
10
|
+
from asyncio import AbstractEventLoop
|
11
|
+
import threading
|
12
|
+
|
13
|
+
logger = logging.getLogger("droidrun")
|
14
|
+
|
8
15
|
|
9
16
|
class SimpleCodeExecutor:
|
10
17
|
"""
|
@@ -16,7 +23,14 @@ class SimpleCodeExecutor:
|
|
16
23
|
NOTE: not safe for production use! Use with caution.
|
17
24
|
"""
|
18
25
|
|
19
|
-
def __init__(
|
26
|
+
def __init__(
|
27
|
+
self,
|
28
|
+
loop: AbstractEventLoop,
|
29
|
+
locals: Dict[str, Any] = {},
|
30
|
+
globals: Dict[str, Any] = {},
|
31
|
+
tools={},
|
32
|
+
use_same_scope: bool = True,
|
33
|
+
):
|
20
34
|
"""
|
21
35
|
Initialize the code executor.
|
22
36
|
|
@@ -28,9 +42,12 @@ class SimpleCodeExecutor:
|
|
28
42
|
|
29
43
|
# 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
|
30
44
|
# e.g. tools = {'tool_name': tool_function}
|
31
|
-
|
45
|
+
|
32
46
|
# check if tools is a dictionary
|
33
47
|
if isinstance(tools, dict):
|
48
|
+
logger.debug(
|
49
|
+
f"🔧 Initializing SimpleCodeExecutor with tools: {tools.items()}"
|
50
|
+
)
|
34
51
|
for tool_name, tool_function in tools.items():
|
35
52
|
if asyncio.iscoroutinefunction(tool_function):
|
36
53
|
# If the function is async, convert it to sync
|
@@ -38,6 +55,7 @@ class SimpleCodeExecutor:
|
|
38
55
|
# Add the tool to globals
|
39
56
|
globals[tool_name] = tool_function
|
40
57
|
elif isinstance(tools, list):
|
58
|
+
logger.debug(f"🔧 Initializing SimpleCodeExecutor with tools: {tools}")
|
41
59
|
# If tools is a list, convert it to a dictionary with tool name as key and function as value
|
42
60
|
for tool in tools:
|
43
61
|
if asyncio.iscoroutinefunction(tool):
|
@@ -48,20 +66,22 @@ class SimpleCodeExecutor:
|
|
48
66
|
else:
|
49
67
|
raise ValueError("Tools must be a dictionary or a list of functions.")
|
50
68
|
|
51
|
-
|
52
|
-
# add time to globals
|
53
69
|
import time
|
54
|
-
|
55
|
-
|
70
|
+
|
71
|
+
globals["time"] = time
|
72
|
+
|
56
73
|
self.globals = globals
|
57
74
|
self.locals = locals
|
58
75
|
self.loop = loop
|
59
76
|
self.use_same_scope = use_same_scope
|
60
77
|
if self.use_same_scope:
|
61
78
|
# If using the same scope, set the globals and locals to the same dictionary
|
62
|
-
self.globals = self.locals = {
|
79
|
+
self.globals = self.locals = {
|
80
|
+
**self.locals,
|
81
|
+
**{k: v for k, v in self.globals.items() if k not in self.locals},
|
82
|
+
}
|
63
83
|
|
64
|
-
async def execute(self, code: str) -> str:
|
84
|
+
async def execute(self, ctx: Context, code: str) -> str:
|
65
85
|
"""
|
66
86
|
Execute Python code and capture output and return values.
|
67
87
|
|
@@ -71,6 +91,9 @@ class SimpleCodeExecutor:
|
|
71
91
|
Returns:
|
72
92
|
str: Output from the execution, including print statements.
|
73
93
|
"""
|
94
|
+
# Update UI elements before execution
|
95
|
+
self.globals['ui_state'] = await ctx.get("ui_state", None)
|
96
|
+
|
74
97
|
# Capture stdout and stderr
|
75
98
|
stdout = io.StringIO()
|
76
99
|
stderr = io.StringIO()
|
@@ -78,20 +101,32 @@ class SimpleCodeExecutor:
|
|
78
101
|
output = ""
|
79
102
|
try:
|
80
103
|
# Execute with captured output
|
81
|
-
|
82
|
-
|
83
|
-
|
104
|
+
thread_exception = []
|
105
|
+
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
|
106
|
+
|
107
|
+
def execute_code():
|
108
|
+
try:
|
109
|
+
exec(code, self.globals, self.locals)
|
110
|
+
except Exception as e:
|
111
|
+
import traceback
|
112
|
+
|
113
|
+
thread_exception.append((e, traceback.format_exc()))
|
84
114
|
|
85
|
-
|
115
|
+
t = threading.Thread(target=execute_code)
|
116
|
+
t.start()
|
117
|
+
t.join()
|
86
118
|
|
87
119
|
# Get output
|
88
120
|
output = stdout.getvalue()
|
89
121
|
if stderr.getvalue():
|
90
122
|
output += "\n" + stderr.getvalue()
|
123
|
+
if thread_exception:
|
124
|
+
e, tb = thread_exception[0]
|
125
|
+
output += f"\nError: {type(e).__name__}: {str(e)}\n{tb}"
|
91
126
|
|
92
127
|
except Exception as e:
|
93
128
|
# Capture exception information
|
94
129
|
output = f"Error: {type(e).__name__}: {str(e)}\n"
|
95
130
|
output += traceback.format_exc()
|
96
131
|
|
97
|
-
return output
|
132
|
+
return output
|
@@ -3,8 +3,7 @@ import logging
|
|
3
3
|
from typing import Any
|
4
4
|
from llama_index.core.llms.llm import LLM
|
5
5
|
# Configure logging
|
6
|
-
logger = logging.getLogger(
|
7
|
-
logger.addHandler(logging.NullHandler())
|
6
|
+
logger = logging.getLogger("droidrun")
|
8
7
|
|
9
8
|
def load_llm(provider_name: str, **kwargs: Any) -> LLM:
|
10
9
|
"""
|
@@ -32,6 +31,8 @@ def load_llm(provider_name: str, **kwargs: Any) -> LLM:
|
|
32
31
|
raise ValueError("provider_name cannot be empty.")
|
33
32
|
if provider_name == "OpenAILike":
|
34
33
|
module_provider_part = "openai_like"
|
34
|
+
elif provider_name == "GoogleGenAI":
|
35
|
+
module_provider_part = "google_genai"
|
35
36
|
else:
|
36
37
|
# Use lowercase for module path, handle hyphens for package name suggestion
|
37
38
|
lower_provider_name = provider_name.lower()
|
@@ -44,9 +45,9 @@ def load_llm(provider_name: str, **kwargs: Any) -> LLM:
|
|
44
45
|
install_package_name = f"llama-index-llms-{module_provider_part.replace('_', '-')}"
|
45
46
|
|
46
47
|
try:
|
47
|
-
logger.
|
48
|
+
logger.debug(f"Attempting to import module: {module_path}")
|
48
49
|
llm_module = importlib.import_module(module_path)
|
49
|
-
logger.
|
50
|
+
logger.debug(f"Successfully imported module: {module_path}")
|
50
51
|
|
51
52
|
except ModuleNotFoundError:
|
52
53
|
logger.error(f"Module '{module_path}' not found. Try: pip install {install_package_name}")
|
@@ -55,18 +56,21 @@ def load_llm(provider_name: str, **kwargs: Any) -> LLM:
|
|
55
56
|
) from None
|
56
57
|
|
57
58
|
try:
|
58
|
-
logger.
|
59
|
+
logger.debug(f"Attempting to get class '{provider_name}' from module {module_path}")
|
59
60
|
llm_class = getattr(llm_module, provider_name)
|
60
|
-
logger.
|
61
|
+
logger.debug(f"Found class: {llm_class.__name__}")
|
61
62
|
|
62
63
|
# Verify the class is a subclass of LLM
|
63
64
|
if not isinstance(llm_class, type) or not issubclass(llm_class, LLM):
|
64
65
|
raise TypeError(f"Class '{provider_name}' found in '{module_path}' is not a valid LLM subclass.")
|
65
66
|
|
67
|
+
# Filter out None values from kwargs
|
68
|
+
filtered_kwargs = {k: v for k, v in kwargs.items() if v is not None}
|
69
|
+
|
66
70
|
# Initialize
|
67
|
-
logger.
|
68
|
-
llm_instance = llm_class(**
|
69
|
-
logger.
|
71
|
+
logger.debug(f"Initializing {llm_class.__name__} with kwargs: {list(filtered_kwargs.keys())}")
|
72
|
+
llm_instance = llm_class(**filtered_kwargs)
|
73
|
+
logger.debug(f"Successfully loaded and initialized LLM: {provider_name}")
|
70
74
|
if not llm_instance:
|
71
75
|
raise RuntimeError(f"Failed to initialize LLM instance for {provider_name}.")
|
72
76
|
return llm_instance
|
@@ -81,7 +85,7 @@ def load_llm(provider_name: str, **kwargs: Any) -> LLM:
|
|
81
85
|
raise # Re-raise TypeError (could be from issubclass check or __init__)
|
82
86
|
except Exception as e:
|
83
87
|
logger.error(f"An unexpected error occurred initializing {provider_name}: {e}")
|
84
|
-
raise
|
88
|
+
raise e
|
85
89
|
|
86
90
|
# --- Example Usage ---
|
87
91
|
if __name__ == "__main__":
|
@@ -0,0 +1,184 @@
|
|
1
|
+
"""
|
2
|
+
Trajectory utilities for DroidRun agents.
|
3
|
+
|
4
|
+
This module provides helper functions for working with agent trajectories,
|
5
|
+
including saving, loading, and analyzing them.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import json
|
9
|
+
import logging
|
10
|
+
import os
|
11
|
+
import time
|
12
|
+
from typing import Dict, List, Any
|
13
|
+
from PIL import Image
|
14
|
+
import io
|
15
|
+
from llama_index.core.workflow import Event
|
16
|
+
|
17
|
+
logger = logging.getLogger("droidrun")
|
18
|
+
|
19
|
+
class Trajectory:
|
20
|
+
|
21
|
+
def __init__(self):
|
22
|
+
"""Initializes an empty trajectory class."""
|
23
|
+
self.events: List[Event] = []
|
24
|
+
self.screenshots: List[bytes] = []
|
25
|
+
|
26
|
+
|
27
|
+
def create_screenshot_gif(self, output_path: str, duration: int = 1000) -> str:
|
28
|
+
"""
|
29
|
+
Create a GIF from a list of screenshots.
|
30
|
+
|
31
|
+
Args:
|
32
|
+
output_path: Base path for the GIF (without extension)
|
33
|
+
duration: Duration for each frame in milliseconds
|
34
|
+
|
35
|
+
Returns:
|
36
|
+
Path to the created GIF file
|
37
|
+
"""
|
38
|
+
if len(self.screenshots) == 0:
|
39
|
+
return None
|
40
|
+
|
41
|
+
images = []
|
42
|
+
for screenshot in self.screenshots:
|
43
|
+
img_data = screenshot
|
44
|
+
img = Image.open(io.BytesIO(img_data))
|
45
|
+
images.append(img)
|
46
|
+
|
47
|
+
# Save as GIF
|
48
|
+
gif_path = f"{output_path}.gif"
|
49
|
+
images[0].save(
|
50
|
+
gif_path,
|
51
|
+
save_all=True,
|
52
|
+
append_images=images[1:],
|
53
|
+
duration=duration,
|
54
|
+
loop=0
|
55
|
+
)
|
56
|
+
|
57
|
+
return gif_path
|
58
|
+
|
59
|
+
def save_trajectory(
|
60
|
+
self,
|
61
|
+
directory: str = "trajectories",
|
62
|
+
) -> str:
|
63
|
+
"""
|
64
|
+
Save trajectory steps to a JSON file and create a GIF of screenshots if available.
|
65
|
+
|
66
|
+
Args:
|
67
|
+
directory: Directory to save the trajectory files
|
68
|
+
|
69
|
+
Returns:
|
70
|
+
Path to the saved trajectory file
|
71
|
+
"""
|
72
|
+
os.makedirs(directory, exist_ok=True)
|
73
|
+
|
74
|
+
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
75
|
+
base_path = os.path.join(directory, f"trajectory_{timestamp}")
|
76
|
+
|
77
|
+
def make_serializable(obj):
|
78
|
+
"""Recursively make objects JSON serializable."""
|
79
|
+
if hasattr(obj, "__class__") and obj.__class__.__name__ == "ChatMessage":
|
80
|
+
# Extract the text content from the ChatMessage
|
81
|
+
if hasattr(obj, "content") and obj.content is not None:
|
82
|
+
return {"role": obj.role.value, "content": obj.content}
|
83
|
+
# If content is not available, try extracting from blocks
|
84
|
+
elif hasattr(obj, "blocks") and obj.blocks:
|
85
|
+
text_content = ""
|
86
|
+
for block in obj.blocks:
|
87
|
+
if hasattr(block, "text"):
|
88
|
+
text_content += block.text
|
89
|
+
return {"role": obj.role.value, "content": text_content}
|
90
|
+
else:
|
91
|
+
return str(obj)
|
92
|
+
elif isinstance(obj, dict):
|
93
|
+
return {k: make_serializable(v) for k, v in obj.items()}
|
94
|
+
elif isinstance(obj, list):
|
95
|
+
return [make_serializable(item) for item in obj]
|
96
|
+
elif hasattr(obj, "__dict__"):
|
97
|
+
# Handle other custom objects by converting to dict
|
98
|
+
return {k: make_serializable(v) for k, v in obj.__dict__.items()
|
99
|
+
if not k.startswith('_')}
|
100
|
+
else:
|
101
|
+
return obj
|
102
|
+
|
103
|
+
serializable_events = []
|
104
|
+
for event in self.events:
|
105
|
+
event_dict = {
|
106
|
+
"type": event.__class__.__name__,
|
107
|
+
**{k: make_serializable(v) for k, v in event.__dict__.items()
|
108
|
+
if not k.startswith('_')}
|
109
|
+
}
|
110
|
+
serializable_events.append(event_dict)
|
111
|
+
|
112
|
+
json_path = f"{base_path}.json"
|
113
|
+
with open(json_path, "w") as f:
|
114
|
+
json.dump(serializable_events, f, indent=2)
|
115
|
+
|
116
|
+
self.create_screenshot_gif(base_path)
|
117
|
+
|
118
|
+
return json_path
|
119
|
+
|
120
|
+
def get_trajectory_statistics(trajectory_data: Dict[str, Any]) -> Dict[str, Any]:
|
121
|
+
"""
|
122
|
+
Get statistics about a trajectory.
|
123
|
+
|
124
|
+
Args:
|
125
|
+
trajectory_data: The trajectory data dictionary
|
126
|
+
|
127
|
+
Returns:
|
128
|
+
Dictionary with statistics about the trajectory
|
129
|
+
"""
|
130
|
+
trajectory_steps = trajectory_data.get("trajectory_steps", [])
|
131
|
+
|
132
|
+
# Count different types of steps
|
133
|
+
step_types = {}
|
134
|
+
for step in trajectory_steps:
|
135
|
+
step_type = step.get("type", "unknown")
|
136
|
+
step_types[step_type] = step_types.get(step_type, 0) + 1
|
137
|
+
|
138
|
+
# Count planning vs execution steps
|
139
|
+
planning_steps = sum(count for step_type, count in step_types.items()
|
140
|
+
if step_type.startswith("planner_"))
|
141
|
+
execution_steps = sum(count for step_type, count in step_types.items()
|
142
|
+
if step_type.startswith("codeact_"))
|
143
|
+
|
144
|
+
# Count successful vs failed executions
|
145
|
+
successful_executions = sum(1 for step in trajectory_steps
|
146
|
+
if step.get("type") == "codeact_execution"
|
147
|
+
and step.get("success", False))
|
148
|
+
failed_executions = sum(1 for step in trajectory_steps
|
149
|
+
if step.get("type") == "codeact_execution"
|
150
|
+
and not step.get("success", True))
|
151
|
+
|
152
|
+
# Return statistics
|
153
|
+
return {
|
154
|
+
"total_steps": len(trajectory_steps),
|
155
|
+
"step_types": step_types,
|
156
|
+
"planning_steps": planning_steps,
|
157
|
+
"execution_steps": execution_steps,
|
158
|
+
"successful_executions": successful_executions,
|
159
|
+
"failed_executions": failed_executions,
|
160
|
+
"goal_achieved": trajectory_data.get("success", False)
|
161
|
+
}
|
162
|
+
|
163
|
+
def print_trajectory_summary(self, trajectory_data: Dict[str, Any]) -> None:
|
164
|
+
"""
|
165
|
+
Print a summary of a trajectory.
|
166
|
+
|
167
|
+
Args:
|
168
|
+
trajectory_data: The trajectory data dictionary
|
169
|
+
"""
|
170
|
+
stats = self.get_trajectory_statistics(trajectory_data)
|
171
|
+
|
172
|
+
print("=== Trajectory Summary ===")
|
173
|
+
print(f"Goal: {trajectory_data.get('goal', 'Unknown')}")
|
174
|
+
print(f"Success: {trajectory_data.get('success', False)}")
|
175
|
+
print(f"Reason: {trajectory_data.get('reason', 'Unknown')}")
|
176
|
+
print(f"Total steps: {stats['total_steps']}")
|
177
|
+
print("Step breakdown:")
|
178
|
+
for step_type, count in stats['step_types'].items():
|
179
|
+
print(f" - {step_type}: {count}")
|
180
|
+
print(f"Planning steps: {stats['planning_steps']}")
|
181
|
+
print(f"Execution steps: {stats['execution_steps']}")
|
182
|
+
print(f"Successful executions: {stats['successful_executions']}")
|
183
|
+
print(f"Failed executions: {stats['failed_executions']}")
|
184
|
+
print("==========================")
|
droidrun/cli/__init__.py
CHANGED
droidrun/cli/logs.py
ADDED
@@ -0,0 +1,283 @@
|
|
1
|
+
import logging
|
2
|
+
from rich.layout import Layout
|
3
|
+
from rich.panel import Panel
|
4
|
+
from rich.spinner import Spinner
|
5
|
+
from rich.console import Console
|
6
|
+
from rich.live import Live
|
7
|
+
from typing import List
|
8
|
+
|
9
|
+
from droidrun.agent.common.events import ScreenshotEvent
|
10
|
+
from droidrun.agent.planner.events import (
|
11
|
+
PlanInputEvent,
|
12
|
+
PlanThinkingEvent,
|
13
|
+
PlanCreatedEvent,
|
14
|
+
)
|
15
|
+
from droidrun.agent.codeact.events import (
|
16
|
+
TaskInputEvent,
|
17
|
+
TaskThinkingEvent,
|
18
|
+
TaskExecutionEvent,
|
19
|
+
TaskExecutionResultEvent,
|
20
|
+
TaskEndEvent,
|
21
|
+
)
|
22
|
+
from droidrun.agent.droid.events import (
|
23
|
+
CodeActExecuteEvent,
|
24
|
+
CodeActResultEvent,
|
25
|
+
ReasoningLogicEvent,
|
26
|
+
TaskRunnerEvent,
|
27
|
+
FinalizeEvent,
|
28
|
+
)
|
29
|
+
|
30
|
+
|
31
|
+
class LogHandler(logging.Handler):
|
32
|
+
def __init__(self, goal: str, current_step: str = "Initializing..."):
|
33
|
+
super().__init__()
|
34
|
+
|
35
|
+
self.goal = goal
|
36
|
+
self.current_step = current_step
|
37
|
+
self.is_completed = False
|
38
|
+
self.is_success = False
|
39
|
+
self.spinner = Spinner("dots")
|
40
|
+
self.console = Console()
|
41
|
+
self.layout = self._create_layout()
|
42
|
+
self.logs: List[str] = []
|
43
|
+
|
44
|
+
def emit(self, record):
|
45
|
+
msg = self.format(record)
|
46
|
+
lines = msg.splitlines()
|
47
|
+
|
48
|
+
for line in lines:
|
49
|
+
self.logs.append(line)
|
50
|
+
# Optionally, limit the log list size
|
51
|
+
if len(self.logs) > 100:
|
52
|
+
self.logs.pop(0)
|
53
|
+
|
54
|
+
self.rerender()
|
55
|
+
|
56
|
+
def render(self):
|
57
|
+
return Live(self.layout, refresh_per_second=4, console=self.console)
|
58
|
+
|
59
|
+
def rerender(self):
|
60
|
+
self._update_layout(
|
61
|
+
self.layout,
|
62
|
+
self.logs,
|
63
|
+
self.current_step,
|
64
|
+
self.goal,
|
65
|
+
self.is_completed,
|
66
|
+
self.is_success,
|
67
|
+
)
|
68
|
+
|
69
|
+
def update_step(self, step: str):
|
70
|
+
self.current_step = step
|
71
|
+
self.rerender()
|
72
|
+
|
73
|
+
def _create_layout(self):
|
74
|
+
"""Create a layout with logs at top and status at bottom"""
|
75
|
+
layout = Layout()
|
76
|
+
layout.split(
|
77
|
+
Layout(name="logs"),
|
78
|
+
Layout(name="goal", size=3),
|
79
|
+
Layout(name="status", size=3),
|
80
|
+
)
|
81
|
+
return layout
|
82
|
+
|
83
|
+
def _update_layout(
|
84
|
+
self,
|
85
|
+
layout: Layout,
|
86
|
+
log_list: List[str],
|
87
|
+
step_message: str,
|
88
|
+
goal: str = None,
|
89
|
+
completed: bool = False,
|
90
|
+
success: bool = False,
|
91
|
+
):
|
92
|
+
"""Update the layout with current logs and step information"""
|
93
|
+
from rich.text import Text
|
94
|
+
import shutil
|
95
|
+
|
96
|
+
# Cache terminal size to avoid frequent recalculation
|
97
|
+
try:
|
98
|
+
terminal_height = shutil.get_terminal_size().lines
|
99
|
+
except:
|
100
|
+
terminal_height = 24 # fallback
|
101
|
+
|
102
|
+
# Reserve space for panels and borders (more conservative estimate)
|
103
|
+
other_components_height = 10 # goal panel + status panel + borders + padding
|
104
|
+
available_log_lines = max(8, terminal_height - other_components_height)
|
105
|
+
|
106
|
+
# Only show recent logs, but ensure we don't flicker
|
107
|
+
visible_logs = (
|
108
|
+
log_list[-available_log_lines:]
|
109
|
+
if len(log_list) > available_log_lines
|
110
|
+
else log_list
|
111
|
+
)
|
112
|
+
|
113
|
+
# Ensure we always have some content to prevent panel collapse
|
114
|
+
if not visible_logs:
|
115
|
+
visible_logs = ["Initializing..."]
|
116
|
+
|
117
|
+
log_content = "\n".join(visible_logs)
|
118
|
+
|
119
|
+
layout["logs"].update(
|
120
|
+
Panel(
|
121
|
+
log_content,
|
122
|
+
title=f"Activity Log ({len(log_list)} entries)",
|
123
|
+
border_style="blue",
|
124
|
+
title_align="left",
|
125
|
+
padding=(0, 1),
|
126
|
+
height=available_log_lines + 2,
|
127
|
+
)
|
128
|
+
)
|
129
|
+
|
130
|
+
if goal:
|
131
|
+
goal_text = Text(goal, style="bold")
|
132
|
+
layout["goal"].update(
|
133
|
+
Panel(
|
134
|
+
goal_text,
|
135
|
+
title="Goal",
|
136
|
+
border_style="magenta",
|
137
|
+
title_align="left",
|
138
|
+
padding=(0, 1),
|
139
|
+
height=3,
|
140
|
+
)
|
141
|
+
)
|
142
|
+
|
143
|
+
step_display = Text()
|
144
|
+
|
145
|
+
if completed:
|
146
|
+
if success:
|
147
|
+
step_display.append("✓ ", style="bold green")
|
148
|
+
panel_title = "Completed"
|
149
|
+
panel_style = "green"
|
150
|
+
else:
|
151
|
+
step_display.append("✗ ", style="bold red")
|
152
|
+
panel_title = "Failed"
|
153
|
+
panel_style = "red"
|
154
|
+
else:
|
155
|
+
step_display.append("⚡ ", style="bold yellow")
|
156
|
+
panel_title = "Status"
|
157
|
+
panel_style = "yellow"
|
158
|
+
|
159
|
+
step_display.append(step_message)
|
160
|
+
|
161
|
+
layout["status"].update(
|
162
|
+
Panel(
|
163
|
+
step_display,
|
164
|
+
title=panel_title,
|
165
|
+
border_style=panel_style,
|
166
|
+
title_align="left",
|
167
|
+
padding=(0, 1),
|
168
|
+
height=3,
|
169
|
+
)
|
170
|
+
)
|
171
|
+
|
172
|
+
def handle_event(self, event):
|
173
|
+
"""Handle streaming events from the agent workflow."""
|
174
|
+
logger = logging.getLogger("droidrun")
|
175
|
+
|
176
|
+
# Log different event types with proper names
|
177
|
+
if isinstance(event, ScreenshotEvent):
|
178
|
+
logger.debug("📸 Taking screenshot...")
|
179
|
+
|
180
|
+
# Planner events
|
181
|
+
elif isinstance(event, PlanInputEvent):
|
182
|
+
self.current_step = "Planning..."
|
183
|
+
logger.info("💭 Planner receiving input...")
|
184
|
+
|
185
|
+
elif isinstance(event, PlanThinkingEvent):
|
186
|
+
if event.thoughts:
|
187
|
+
thoughts_preview = (
|
188
|
+
event.thoughts[:150] + "..."
|
189
|
+
if len(event.thoughts) > 150
|
190
|
+
else event.thoughts
|
191
|
+
)
|
192
|
+
logger.info(f"🧠 Planning: {thoughts_preview}")
|
193
|
+
if event.code:
|
194
|
+
logger.info(f"📝 Generated plan code")
|
195
|
+
|
196
|
+
elif isinstance(event, PlanCreatedEvent):
|
197
|
+
if event.tasks:
|
198
|
+
task_count = len(event.tasks) if event.tasks else 0
|
199
|
+
self.current_step = f"Plan ready ({task_count} tasks)"
|
200
|
+
logger.info(f"📋 Plan created with {task_count} tasks")
|
201
|
+
for task in event.tasks:
|
202
|
+
desc = task.description
|
203
|
+
logger.info(f"- {desc}")
|
204
|
+
|
205
|
+
# CodeAct events
|
206
|
+
elif isinstance(event, TaskInputEvent):
|
207
|
+
self.current_step = "Processing task input..."
|
208
|
+
logger.info("💬 Task input received...")
|
209
|
+
|
210
|
+
elif isinstance(event, TaskThinkingEvent):
|
211
|
+
if hasattr(event, "thoughts") and event.thoughts:
|
212
|
+
thoughts_preview = (
|
213
|
+
event.thoughts[:150] + "..."
|
214
|
+
if len(event.thoughts) > 150
|
215
|
+
else event.thoughts
|
216
|
+
)
|
217
|
+
logger.info(f"🧠 Thinking: {thoughts_preview}")
|
218
|
+
if hasattr(event, "code") and event.code:
|
219
|
+
logger.info(f"💻 Executing action code")
|
220
|
+
logger.debug(f"{event.code}")
|
221
|
+
|
222
|
+
elif isinstance(event, TaskExecutionEvent):
|
223
|
+
self.current_step = "Executing action..."
|
224
|
+
logger.info(f"⚡ Executing action...")
|
225
|
+
|
226
|
+
elif isinstance(event, TaskExecutionResultEvent):
|
227
|
+
if hasattr(event, "output") and event.output:
|
228
|
+
output = str(event.output)
|
229
|
+
if "Error" in output or "Exception" in output:
|
230
|
+
output_preview = (
|
231
|
+
output[:100] + "..." if len(output) > 100 else output
|
232
|
+
)
|
233
|
+
logger.info(f"❌ Action error: {output_preview}")
|
234
|
+
else:
|
235
|
+
output_preview = (
|
236
|
+
output[:100] + "..." if len(output) > 100 else output
|
237
|
+
)
|
238
|
+
logger.info(f"⚡ Action result: {output_preview}")
|
239
|
+
|
240
|
+
elif isinstance(event, TaskEndEvent):
|
241
|
+
if hasattr(event, "success") and hasattr(event, "reason"):
|
242
|
+
if event.success:
|
243
|
+
self.current_step = event.reason
|
244
|
+
logger.info(f"✅ Task completed: {event.reason}")
|
245
|
+
else:
|
246
|
+
self.current_step = f"Task failed"
|
247
|
+
logger.info(f"❌ Task failed: {event.reason}")
|
248
|
+
|
249
|
+
# Droid coordination events
|
250
|
+
elif isinstance(event, CodeActExecuteEvent):
|
251
|
+
self.current_step = "Executing task..."
|
252
|
+
logger.info(f"🔧 Starting task execution...")
|
253
|
+
|
254
|
+
elif isinstance(event, CodeActResultEvent):
|
255
|
+
if hasattr(event, "success") and hasattr(event, "reason"):
|
256
|
+
if event.success:
|
257
|
+
self.current_step = event.reason
|
258
|
+
logger.info(f"✅ Task completed: {event.reason}")
|
259
|
+
else:
|
260
|
+
self.current_step = f"Task failed"
|
261
|
+
logger.info(f"❌ Task failed: {event.reason}")
|
262
|
+
|
263
|
+
elif isinstance(event, ReasoningLogicEvent):
|
264
|
+
self.current_step = "Planning..."
|
265
|
+
logger.info(f"🤔 Planning next steps...")
|
266
|
+
|
267
|
+
elif isinstance(event, TaskRunnerEvent):
|
268
|
+
self.current_step = "Processing tasks..."
|
269
|
+
logger.info(f"🏃 Processing task queue...")
|
270
|
+
|
271
|
+
elif isinstance(event, FinalizeEvent):
|
272
|
+
if hasattr(event, "success") and hasattr(event, "reason"):
|
273
|
+
self.is_completed = True
|
274
|
+
self.is_success = event.success
|
275
|
+
if event.success:
|
276
|
+
self.current_step = f"Success: {event.reason}"
|
277
|
+
logger.info(f"🎉 Goal achieved: {event.reason}")
|
278
|
+
else:
|
279
|
+
self.current_step = f"Failed: {event.reason}"
|
280
|
+
logger.info(f"❌ Goal failed: {event.reason}")
|
281
|
+
|
282
|
+
else:
|
283
|
+
logger.debug(f"🔄 {event.__class__.__name__}")
|