droidrun 0.3.10.dev5__py3-none-any.whl → 0.3.10.dev6__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/codeact_agent.py +18 -25
- droidrun/agent/droid/events.py +4 -1
- droidrun/agent/executor/executor_agent.py +24 -38
- droidrun/agent/executor/prompts.py +0 -108
- droidrun/agent/manager/manager_agent.py +104 -87
- droidrun/agent/utils/llm_picker.py +63 -1
- droidrun/agent/utils/tools.py +29 -0
- droidrun/app_cards/app_card_provider.py +27 -0
- droidrun/app_cards/providers/__init__.py +7 -0
- droidrun/app_cards/providers/composite_provider.py +97 -0
- droidrun/app_cards/providers/local_provider.py +116 -0
- droidrun/app_cards/providers/server_provider.py +126 -0
- droidrun/cli/main.py +241 -30
- droidrun/config_manager/__init__.py +0 -2
- droidrun/config_manager/config_manager.py +45 -101
- droidrun/config_manager/path_resolver.py +1 -1
- droidrun/config_manager/prompt_loader.py +48 -51
- droidrun/portal.py +17 -0
- droidrun/tools/adb.py +13 -34
- {droidrun-0.3.10.dev5.dist-info → droidrun-0.3.10.dev6.dist-info}/METADATA +2 -9
- {droidrun-0.3.10.dev5.dist-info → droidrun-0.3.10.dev6.dist-info}/RECORD +24 -20
- droidrun/config_manager/app_card_loader.py +0 -148
- {droidrun-0.3.10.dev5.dist-info → droidrun-0.3.10.dev6.dist-info}/WHEEL +0 -0
- {droidrun-0.3.10.dev5.dist-info → droidrun-0.3.10.dev6.dist-info}/entry_points.txt +0 -0
- {droidrun-0.3.10.dev5.dist-info → droidrun-0.3.10.dev6.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,27 @@
|
|
1
|
+
"""
|
2
|
+
Abstract base class for app card providers.
|
3
|
+
|
4
|
+
Providers load app-specific instruction cards based on package names.
|
5
|
+
Supports multiple backends: local files, remote servers, or composite strategies.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from abc import ABC, abstractmethod
|
9
|
+
|
10
|
+
|
11
|
+
|
12
|
+
class AppCardProvider(ABC):
|
13
|
+
"""Abstract interface for loading app-specific instruction cards."""
|
14
|
+
|
15
|
+
@abstractmethod
|
16
|
+
async def load_app_card(self, package_name: str, instruction: str = "") -> str:
|
17
|
+
"""
|
18
|
+
Load app card for a given package asynchronously.
|
19
|
+
|
20
|
+
Args:
|
21
|
+
package_name: Android package name (e.g., "com.google.android.gm")
|
22
|
+
instruction: User's instruction/goal (optional context for server providers)
|
23
|
+
|
24
|
+
Returns:
|
25
|
+
App card content as string, or empty string if not found or on error
|
26
|
+
"""
|
27
|
+
pass
|
@@ -0,0 +1,7 @@
|
|
1
|
+
"""App card provider implementations."""
|
2
|
+
|
3
|
+
from droidrun.app_cards.providers.local_provider import LocalAppCardProvider
|
4
|
+
from droidrun.app_cards.providers.server_provider import ServerAppCardProvider
|
5
|
+
from droidrun.app_cards.providers.composite_provider import CompositeAppCardProvider
|
6
|
+
|
7
|
+
__all__ = ["LocalAppCardProvider", "ServerAppCardProvider", "CompositeAppCardProvider"]
|
@@ -0,0 +1,97 @@
|
|
1
|
+
"""
|
2
|
+
Composite app card provider.
|
3
|
+
|
4
|
+
Tries server first, falls back to local if server fails or returns empty.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import logging
|
8
|
+
from typing import Dict
|
9
|
+
|
10
|
+
from droidrun.config_manager.app_card_provider import AppCardProvider
|
11
|
+
from droidrun.config_manager.providers.local_provider import LocalAppCardProvider
|
12
|
+
from droidrun.config_manager.providers.server_provider import ServerAppCardProvider
|
13
|
+
|
14
|
+
logger = logging.getLogger("droidrun")
|
15
|
+
|
16
|
+
|
17
|
+
class CompositeAppCardProvider(AppCardProvider):
|
18
|
+
"""
|
19
|
+
Load app cards from server with local fallback.
|
20
|
+
|
21
|
+
Strategy:
|
22
|
+
1. Try server first
|
23
|
+
2. If server fails or returns empty, try local
|
24
|
+
3. Return first non-empty result, or empty if both fail
|
25
|
+
"""
|
26
|
+
|
27
|
+
def __init__(
|
28
|
+
self,
|
29
|
+
server_url: str,
|
30
|
+
app_cards_dir: str = "config/app_cards",
|
31
|
+
server_timeout: float = 2.0,
|
32
|
+
server_max_retries: int = 2
|
33
|
+
):
|
34
|
+
"""
|
35
|
+
Initialize composite provider.
|
36
|
+
|
37
|
+
Args:
|
38
|
+
server_url: Base URL of the app card server
|
39
|
+
app_cards_dir: Directory containing local app_cards.json
|
40
|
+
server_timeout: Server request timeout in seconds
|
41
|
+
server_max_retries: Number of server retry attempts
|
42
|
+
"""
|
43
|
+
self.server_provider = ServerAppCardProvider(
|
44
|
+
server_url=server_url,
|
45
|
+
timeout=server_timeout,
|
46
|
+
max_retries=server_max_retries
|
47
|
+
)
|
48
|
+
self.local_provider = LocalAppCardProvider(app_cards_dir=app_cards_dir)
|
49
|
+
|
50
|
+
async def load_app_card(self, package_name: str, instruction: str = "") -> str:
|
51
|
+
"""
|
52
|
+
Load app card with server-first, local-fallback strategy.
|
53
|
+
|
54
|
+
Args:
|
55
|
+
package_name: Android package name (e.g., "com.google.android.gm")
|
56
|
+
instruction: User instruction/goal
|
57
|
+
|
58
|
+
Returns:
|
59
|
+
App card content from server or local, or empty string if both fail
|
60
|
+
"""
|
61
|
+
if not package_name:
|
62
|
+
return ""
|
63
|
+
|
64
|
+
# Try server first
|
65
|
+
server_result = await self.server_provider.load_app_card(package_name, instruction)
|
66
|
+
|
67
|
+
if server_result:
|
68
|
+
return server_result
|
69
|
+
|
70
|
+
# Server failed or returned empty, try local
|
71
|
+
logger.debug(f"Composite provider: falling back to local for {package_name}")
|
72
|
+
local_result = await self.local_provider.load_app_card(package_name, instruction)
|
73
|
+
|
74
|
+
if local_result:
|
75
|
+
logger.info(f"Composite provider: using local fallback for {package_name}")
|
76
|
+
else:
|
77
|
+
logger.debug(f"Composite provider: no app card found for {package_name}")
|
78
|
+
|
79
|
+
return local_result
|
80
|
+
|
81
|
+
def clear_cache(self) -> None:
|
82
|
+
"""Clear caches in both providers."""
|
83
|
+
self.server_provider.clear_cache()
|
84
|
+
self.local_provider.clear_cache()
|
85
|
+
logger.debug("Composite app card cache cleared")
|
86
|
+
|
87
|
+
def get_cache_stats(self) -> Dict[str, any]:
|
88
|
+
"""
|
89
|
+
Get cache statistics from both providers.
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
Dict with cache stats from server and local providers
|
93
|
+
"""
|
94
|
+
return {
|
95
|
+
"server": self.server_provider.get_cache_stats(),
|
96
|
+
"local": self.local_provider.get_cache_stats(),
|
97
|
+
}
|
@@ -0,0 +1,116 @@
|
|
1
|
+
"""
|
2
|
+
Local file-based app card provider.
|
3
|
+
|
4
|
+
Loads app cards from local filesystem using app_cards.json mapping.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import json
|
8
|
+
import logging
|
9
|
+
from pathlib import Path
|
10
|
+
from typing import Dict
|
11
|
+
|
12
|
+
from droidrun.config_manager.app_card_provider import AppCardProvider
|
13
|
+
from droidrun.config_manager.path_resolver import PathResolver
|
14
|
+
|
15
|
+
logger = logging.getLogger("droidrun")
|
16
|
+
|
17
|
+
|
18
|
+
class LocalAppCardProvider(AppCardProvider):
|
19
|
+
"""Load app cards from local filesystem with in-memory caching."""
|
20
|
+
|
21
|
+
def __init__(self, app_cards_dir: str = "config/app_cards"):
|
22
|
+
"""
|
23
|
+
Initialize local provider.
|
24
|
+
|
25
|
+
Args:
|
26
|
+
app_cards_dir: Directory containing app_cards.json and markdown files
|
27
|
+
"""
|
28
|
+
# Resolve app_cards.json path once
|
29
|
+
mapping_path = PathResolver.resolve(f"{app_cards_dir}/app_cards.json")
|
30
|
+
self.app_cards_dir = mapping_path.parent
|
31
|
+
|
32
|
+
# Load mapping immediately
|
33
|
+
try:
|
34
|
+
if mapping_path.exists():
|
35
|
+
with open(mapping_path, "r", encoding="utf-8") as f:
|
36
|
+
self.mapping = json.load(f)
|
37
|
+
logger.debug(f"Loaded app_cards.json with {len(self.mapping)} entries")
|
38
|
+
else:
|
39
|
+
logger.warning(f"app_cards.json not found at {mapping_path}")
|
40
|
+
self.mapping = {}
|
41
|
+
except Exception as e:
|
42
|
+
logger.warning(f"Failed to load app_cards.json: {e}")
|
43
|
+
self.mapping = {}
|
44
|
+
|
45
|
+
# Content cache: (package_name, instruction) -> content
|
46
|
+
self._content_cache: Dict[tuple[str, str], str] = {}
|
47
|
+
|
48
|
+
async def load_app_card(self, package_name: str, instruction: str = "") -> str:
|
49
|
+
"""
|
50
|
+
Load app card for a package name from local files.
|
51
|
+
|
52
|
+
Args:
|
53
|
+
package_name: Android package name (e.g., "com.google.android.gm")
|
54
|
+
instruction: User instruction (for cache key consistency, not used in loading)
|
55
|
+
|
56
|
+
Returns:
|
57
|
+
App card content or empty string if not found
|
58
|
+
"""
|
59
|
+
if not package_name:
|
60
|
+
return ""
|
61
|
+
|
62
|
+
# Check content cache first
|
63
|
+
cache_key = (package_name, instruction)
|
64
|
+
if cache_key in self._content_cache:
|
65
|
+
logger.debug(f"App card cache hit: {package_name}")
|
66
|
+
return self._content_cache[cache_key]
|
67
|
+
|
68
|
+
# Check if package exists in mapping
|
69
|
+
if package_name not in self.mapping:
|
70
|
+
self._content_cache[cache_key] = ""
|
71
|
+
return ""
|
72
|
+
|
73
|
+
# Get app card file path (relative to app_cards_dir)
|
74
|
+
filename = self.mapping[package_name]
|
75
|
+
app_card_path = self.app_cards_dir / filename
|
76
|
+
|
77
|
+
# Read file
|
78
|
+
try:
|
79
|
+
if not app_card_path.exists():
|
80
|
+
self._content_cache[cache_key] = ""
|
81
|
+
logger.debug(f"App card not found: {app_card_path}")
|
82
|
+
return ""
|
83
|
+
|
84
|
+
# Async file read
|
85
|
+
import asyncio
|
86
|
+
loop = asyncio.get_event_loop()
|
87
|
+
content = await loop.run_in_executor(
|
88
|
+
None, app_card_path.read_text, "utf-8"
|
89
|
+
)
|
90
|
+
|
91
|
+
# Cache and return
|
92
|
+
self._content_cache[cache_key] = content
|
93
|
+
logger.info(f"Loaded app card for {package_name} from {app_card_path}")
|
94
|
+
return content
|
95
|
+
|
96
|
+
except Exception as e:
|
97
|
+
logger.warning(f"Failed to load app card for {package_name}: {e}")
|
98
|
+
self._content_cache[cache_key] = ""
|
99
|
+
return ""
|
100
|
+
|
101
|
+
def clear_cache(self) -> None:
|
102
|
+
"""Clear content cache (useful for testing or runtime reloading)."""
|
103
|
+
self._content_cache.clear()
|
104
|
+
logger.debug("Local app card cache cleared")
|
105
|
+
|
106
|
+
def get_cache_stats(self) -> Dict[str, int]:
|
107
|
+
"""
|
108
|
+
Get cache statistics.
|
109
|
+
|
110
|
+
Returns:
|
111
|
+
Dict with cache stats (useful for debugging)
|
112
|
+
"""
|
113
|
+
return {
|
114
|
+
"content_entries": len(self._content_cache),
|
115
|
+
"mapping_entries": len(self.mapping),
|
116
|
+
}
|
@@ -0,0 +1,126 @@
|
|
1
|
+
"""
|
2
|
+
Server-based app card provider.
|
3
|
+
|
4
|
+
Fetches app cards from a remote HTTP server.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import logging
|
8
|
+
from typing import Dict
|
9
|
+
|
10
|
+
import httpx
|
11
|
+
|
12
|
+
from droidrun.config_manager.app_card_provider import AppCardProvider
|
13
|
+
|
14
|
+
logger = logging.getLogger("droidrun")
|
15
|
+
|
16
|
+
|
17
|
+
class ServerAppCardProvider(AppCardProvider):
|
18
|
+
"""Load app cards from remote server with in-memory caching."""
|
19
|
+
|
20
|
+
def __init__(
|
21
|
+
self,
|
22
|
+
server_url: str,
|
23
|
+
timeout: float = 2.0,
|
24
|
+
max_retries: int = 2
|
25
|
+
):
|
26
|
+
"""
|
27
|
+
Initialize server provider.
|
28
|
+
|
29
|
+
Args:
|
30
|
+
server_url: Base URL of the app card server (e.g., "https://api.example.com")
|
31
|
+
timeout: Request timeout in seconds
|
32
|
+
max_retries: Number of retry attempts on failure
|
33
|
+
"""
|
34
|
+
self.server_url = server_url.rstrip("/")
|
35
|
+
self.timeout = timeout
|
36
|
+
self.max_retries = max_retries
|
37
|
+
self._content_cache: Dict[tuple[str, str], str] = {}
|
38
|
+
|
39
|
+
async def load_app_card(self, package_name: str, instruction: str = "") -> str:
|
40
|
+
"""
|
41
|
+
Load app card from remote server.
|
42
|
+
|
43
|
+
Args:
|
44
|
+
package_name: Android package name (e.g., "com.google.android.gm")
|
45
|
+
instruction: User instruction/goal (sent to server for context)
|
46
|
+
|
47
|
+
Returns:
|
48
|
+
App card content or empty string if not found or on error
|
49
|
+
"""
|
50
|
+
if not package_name:
|
51
|
+
return ""
|
52
|
+
|
53
|
+
# Check content cache first (key: package_name, instruction)
|
54
|
+
cache_key = (package_name, instruction)
|
55
|
+
if cache_key in self._content_cache:
|
56
|
+
return self._content_cache[cache_key]
|
57
|
+
|
58
|
+
# Make HTTP request with retries
|
59
|
+
endpoint = f"{self.server_url}/app-cards"
|
60
|
+
payload = {
|
61
|
+
"package_name": package_name,
|
62
|
+
"instruction": instruction
|
63
|
+
}
|
64
|
+
|
65
|
+
for attempt in range(1, self.max_retries + 1):
|
66
|
+
try:
|
67
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
68
|
+
response = await client.post(endpoint, json=payload)
|
69
|
+
|
70
|
+
if response.status_code == 200:
|
71
|
+
data = response.json()
|
72
|
+
app_card = data.get("app_card", "")
|
73
|
+
|
74
|
+
# Cache the result (even if empty)
|
75
|
+
self._content_cache[cache_key] = app_card
|
76
|
+
return app_card
|
77
|
+
|
78
|
+
elif response.status_code == 404:
|
79
|
+
# Not found is expected, cache empty result
|
80
|
+
self._content_cache[cache_key] = ""
|
81
|
+
return ""
|
82
|
+
|
83
|
+
else:
|
84
|
+
logger.warning(
|
85
|
+
f"Server returned status {response.status_code} for {package_name} "
|
86
|
+
f"(attempt {attempt}/{self.max_retries})"
|
87
|
+
)
|
88
|
+
|
89
|
+
except httpx.TimeoutException:
|
90
|
+
logger.warning(
|
91
|
+
f"Server request timeout for {package_name} "
|
92
|
+
f"(attempt {attempt}/{self.max_retries})"
|
93
|
+
)
|
94
|
+
|
95
|
+
except httpx.RequestError as e:
|
96
|
+
logger.warning(
|
97
|
+
f"Server request failed for {package_name}: {e} "
|
98
|
+
f"(attempt {attempt}/{self.max_retries})"
|
99
|
+
)
|
100
|
+
|
101
|
+
except Exception as e:
|
102
|
+
logger.warning(
|
103
|
+
f"Unexpected error loading app card from server: {e} "
|
104
|
+
f"(attempt {attempt}/{self.max_retries})"
|
105
|
+
)
|
106
|
+
|
107
|
+
# All retries failed, cache empty result
|
108
|
+
logger.warning(f"Failed to load app card from server after {self.max_retries} attempts")
|
109
|
+
self._content_cache[cache_key] = ""
|
110
|
+
return ""
|
111
|
+
|
112
|
+
def clear_cache(self) -> None:
|
113
|
+
"""Clear content cache."""
|
114
|
+
self._content_cache.clear()
|
115
|
+
logger.debug("Server app card cache cleared")
|
116
|
+
|
117
|
+
def get_cache_stats(self) -> Dict[str, int]:
|
118
|
+
"""
|
119
|
+
Get cache statistics.
|
120
|
+
|
121
|
+
Returns:
|
122
|
+
Dict with cache stats (useful for debugging)
|
123
|
+
"""
|
124
|
+
return {
|
125
|
+
"content_entries": len(self._content_cache),
|
126
|
+
}
|
droidrun/cli/main.py
CHANGED
@@ -8,14 +8,13 @@ import os
|
|
8
8
|
import warnings
|
9
9
|
from contextlib import nullcontext
|
10
10
|
from functools import wraps
|
11
|
-
from pathlib import Path
|
12
11
|
|
13
12
|
import click
|
14
13
|
from adbutils import adb
|
15
14
|
from rich.console import Console
|
16
15
|
|
17
16
|
from droidrun.agent.droid import DroidAgent
|
18
|
-
from droidrun.agent.utils.llm_picker import load_llm
|
17
|
+
from droidrun.agent.utils.llm_picker import load_llm, load_llms_from_profiles
|
19
18
|
from droidrun.cli.logs import LogHandler
|
20
19
|
from droidrun.config_manager.config_manager import (
|
21
20
|
AgentConfig,
|
@@ -240,7 +239,7 @@ async def run_command(
|
|
240
239
|
if temperature is not None:
|
241
240
|
overrides = {name: {'temperature': temperature} for name in profile_names}
|
242
241
|
|
243
|
-
llms = config.
|
242
|
+
llms = load_llms_from_profiles(config.llm_profiles, profile_names=profile_names, **overrides)
|
244
243
|
logger.info(f"🧠 Loaded {len(llms)} agent-specific LLMs from profiles")
|
245
244
|
|
246
245
|
# ================================================================
|
@@ -532,29 +531,43 @@ def run(
|
|
532
531
|
):
|
533
532
|
"""Run a command on your Android device using natural language."""
|
534
533
|
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
534
|
+
try:
|
535
|
+
run_command(
|
536
|
+
command,
|
537
|
+
config,
|
538
|
+
device,
|
539
|
+
provider,
|
540
|
+
model,
|
541
|
+
steps,
|
542
|
+
base_url,
|
543
|
+
api_base,
|
544
|
+
vision,
|
545
|
+
manager_vision,
|
546
|
+
executor_vision,
|
547
|
+
codeact_vision,
|
548
|
+
reasoning,
|
549
|
+
tracing,
|
550
|
+
debug,
|
551
|
+
use_tcp,
|
552
|
+
temperature=temperature,
|
553
|
+
save_trajectory=save_trajectory,
|
554
|
+
allow_drag=allow_drag,
|
555
|
+
ios=ios if ios is not None else False,
|
556
|
+
)
|
557
|
+
finally:
|
558
|
+
# Disable DroidRun keyboard after execution
|
559
|
+
try:
|
560
|
+
if not (ios if ios is not None else False):
|
561
|
+
device_serial = adb.device().serial
|
562
|
+
if device_serial:
|
563
|
+
tools = AdbTools(serial=device, use_tcp=use_tcp if use_tcp is not None else False)
|
564
|
+
if hasattr(tools, 'device') and tools.device:
|
565
|
+
tools.device.shell("ime disable com.droidrun.portal/.DroidrunKeyboardIME")
|
566
|
+
click.echo("DroidRun keyboard disabled successfully")
|
567
|
+
# Cleanup tools
|
568
|
+
del tools
|
569
|
+
except Exception as disable_e:
|
570
|
+
click.echo(f"Warning: Failed to disable DroidRun keyboard: {disable_e}")
|
558
571
|
|
559
572
|
|
560
573
|
@cli.command()
|
@@ -756,8 +769,206 @@ def ping(device: str | None, use_tcp: bool, debug: bool):
|
|
756
769
|
cli.add_command(macro_cli, name="macro")
|
757
770
|
|
758
771
|
|
772
|
+
async def test(command: str):
|
773
|
+
config = ConfigManager(path="config.yaml")
|
774
|
+
# Initialize logging first (use config default if debug not specified)
|
775
|
+
debug_mode = debug if debug is not None else config.logging.debug
|
776
|
+
log_handler = configure_logging(command, debug_mode, config.logging.rich_text)
|
777
|
+
logger = logging.getLogger("droidrun")
|
778
|
+
|
779
|
+
log_handler.update_step("Initializing...")
|
780
|
+
|
781
|
+
with log_handler.render():
|
782
|
+
try:
|
783
|
+
logger.info(f"🚀 Starting: {command}")
|
784
|
+
print_telemetry_message()
|
785
|
+
|
786
|
+
# ================================================================
|
787
|
+
# STEP 1: Build config objects with CLI overrides
|
788
|
+
# ================================================================
|
789
|
+
|
790
|
+
# Build agent-specific configs with vision overrides
|
791
|
+
if vision is not None:
|
792
|
+
# --vision flag overrides all agents
|
793
|
+
manager_vision_val = vision
|
794
|
+
executor_vision_val = vision
|
795
|
+
codeact_vision_val = vision
|
796
|
+
logger.debug(f"CLI override: vision={vision} (all agents)")
|
797
|
+
else:
|
798
|
+
# Use individual overrides or config defaults
|
799
|
+
manager_vision_val = config.agent.manager.vision
|
800
|
+
executor_vision_val = config.agent.executor.vision
|
801
|
+
codeact_vision_val = config.agent.codeact.vision
|
802
|
+
|
803
|
+
manager_cfg = ManagerConfig(
|
804
|
+
vision=manager_vision_val,
|
805
|
+
system_prompt="rev1.jinja2"
|
806
|
+
)
|
807
|
+
|
808
|
+
executor_cfg = ExecutorConfig(
|
809
|
+
vision=executor_vision_val,
|
810
|
+
system_prompt="rev1.jinja2"
|
811
|
+
)
|
812
|
+
|
813
|
+
codeact_cfg = CodeActConfig(
|
814
|
+
vision=codeact_vision_val,
|
815
|
+
system_prompt=config.agent.codeact.system_prompt,
|
816
|
+
user_prompt=config.agent.codeact.user_prompt
|
817
|
+
)
|
818
|
+
|
819
|
+
agent_cfg = AgentConfig(
|
820
|
+
max_steps=steps if steps is not None else config.agent.max_steps,
|
821
|
+
reasoning=reasoning if reasoning is not None else config.agent.reasoning,
|
822
|
+
after_sleep_action=config.agent.after_sleep_action,
|
823
|
+
wait_for_stable_ui=config.agent.wait_for_stable_ui,
|
824
|
+
prompts_dir=config.agent.prompts_dir,
|
825
|
+
manager=manager_cfg,
|
826
|
+
executor=executor_cfg,
|
827
|
+
codeact=codeact_cfg,
|
828
|
+
app_cards=config.agent.app_cards,
|
829
|
+
)
|
830
|
+
|
831
|
+
device_cfg = DeviceConfig(
|
832
|
+
serial=device if device is not None else config.device.serial,
|
833
|
+
use_tcp=use_tcp if use_tcp is not None else config.device.use_tcp,
|
834
|
+
)
|
835
|
+
|
836
|
+
tools_cfg = ToolsConfig(
|
837
|
+
allow_drag=allow_drag if allow_drag is not None else config.tools.allow_drag,
|
838
|
+
)
|
839
|
+
|
840
|
+
logging_cfg = LoggingConfig(
|
841
|
+
debug=debug if debug is not None else config.logging.debug,
|
842
|
+
save_trajectory=save_trajectory if save_trajectory is not None else config.logging.save_trajectory,
|
843
|
+
rich_text=config.logging.rich_text,
|
844
|
+
)
|
845
|
+
|
846
|
+
tracing_cfg = TracingConfig(
|
847
|
+
enabled=tracing if tracing is not None else config.tracing.enabled,
|
848
|
+
)
|
849
|
+
|
850
|
+
# ================================================================
|
851
|
+
# STEP 3: Load LLMs
|
852
|
+
# ================================================================
|
853
|
+
|
854
|
+
log_handler.update_step("Loading LLMs...")
|
855
|
+
|
856
|
+
# No custom provider/model - use profiles from config
|
857
|
+
logger.info("📋 Loading LLMs from config profiles...")
|
858
|
+
|
859
|
+
profile_names = ['manager', 'executor', 'codeact', 'text_manipulator', 'app_opener']
|
860
|
+
|
861
|
+
# Apply temperature override to all profiles if specified
|
862
|
+
overrides = {}
|
863
|
+
if temperature is not None:
|
864
|
+
overrides = {name: {'temperature': temperature} for name in profile_names}
|
865
|
+
|
866
|
+
llms = load_llms_from_profiles(config.llm_profiles, profile_names=profile_names, **overrides)
|
867
|
+
logger.info(f"🧠 Loaded {len(llms)} agent-specific LLMs from profiles")
|
868
|
+
|
869
|
+
# ================================================================
|
870
|
+
# STEP 4: Setup device and tools
|
871
|
+
# ================================================================
|
872
|
+
|
873
|
+
log_handler.update_step("Setting up tools...")
|
874
|
+
|
875
|
+
device_serial = device_cfg.serial
|
876
|
+
if device_serial is None and not ios:
|
877
|
+
logger.info("🔍 Finding connected device...")
|
878
|
+
devices = adb.list()
|
879
|
+
if not devices:
|
880
|
+
raise ValueError("No connected devices found.")
|
881
|
+
device_serial = devices[0].serial
|
882
|
+
device_cfg = DeviceConfig(serial=device_serial, use_tcp=device_cfg.use_tcp)
|
883
|
+
logger.info(f"📱 Using device: {device_serial}")
|
884
|
+
elif device_serial is None and ios:
|
885
|
+
raise ValueError("iOS device not specified. Please specify device base url via --device")
|
886
|
+
else:
|
887
|
+
logger.info(f"📱 Using device: {device_serial}")
|
888
|
+
|
889
|
+
tools = (
|
890
|
+
AdbTools(
|
891
|
+
serial=device_serial,
|
892
|
+
use_tcp=device_cfg.use_tcp,
|
893
|
+
app_opener_llm=llms.get('app_opener'),
|
894
|
+
text_manipulator_llm=llms.get('text_manipulator')
|
895
|
+
)
|
896
|
+
if not ios
|
897
|
+
else IOSTools(url=device_serial)
|
898
|
+
)
|
899
|
+
|
900
|
+
excluded_tools = [] if tools_cfg.allow_drag else ["drag"]
|
901
|
+
|
902
|
+
# ================================================================
|
903
|
+
# STEP 5: Initialize DroidAgent with all settings
|
904
|
+
# ================================================================
|
905
|
+
|
906
|
+
log_handler.update_step("Initializing DroidAgent...")
|
907
|
+
|
908
|
+
mode = "planning with reasoning" if agent_cfg.reasoning else "direct execution"
|
909
|
+
logger.info(f"🤖 Agent mode: {mode}")
|
910
|
+
logger.info(f"👁️ Vision settings: Manager={agent_cfg.manager.vision}, "
|
911
|
+
f"Executor={agent_cfg.executor.vision}, CodeAct={agent_cfg.codeact.vision}")
|
912
|
+
|
913
|
+
if tracing_cfg.enabled:
|
914
|
+
logger.info("🔍 Tracing enabled")
|
915
|
+
|
916
|
+
droid_agent = DroidAgent(
|
917
|
+
goal=command,
|
918
|
+
llms=llms,
|
919
|
+
tools=tools,
|
920
|
+
config=config,
|
921
|
+
agent_config=agent_cfg,
|
922
|
+
device_config=device_cfg,
|
923
|
+
tools_config=tools_cfg,
|
924
|
+
logging_config=logging_cfg,
|
925
|
+
tracing_config=tracing_cfg,
|
926
|
+
excluded_tools=excluded_tools,
|
927
|
+
timeout=1000,
|
928
|
+
)
|
929
|
+
|
930
|
+
# ================================================================
|
931
|
+
# STEP 6: Run agent
|
932
|
+
# ================================================================
|
933
|
+
|
934
|
+
logger.info("▶️ Starting agent execution...")
|
935
|
+
logger.info("Press Ctrl+C to stop")
|
936
|
+
log_handler.update_step("Running agent...")
|
937
|
+
|
938
|
+
try:
|
939
|
+
handler = droid_agent.run()
|
940
|
+
|
941
|
+
async for event in handler.stream_events():
|
942
|
+
log_handler.handle_event(event)
|
943
|
+
result = await handler # noqa: F841
|
944
|
+
|
945
|
+
except KeyboardInterrupt:
|
946
|
+
log_handler.is_completed = True
|
947
|
+
log_handler.is_success = False
|
948
|
+
log_handler.current_step = "Stopped by user"
|
949
|
+
logger.info("⏹️ Stopped by user")
|
950
|
+
|
951
|
+
except Exception as e:
|
952
|
+
log_handler.is_completed = True
|
953
|
+
log_handler.is_success = False
|
954
|
+
log_handler.current_step = f"Error: {e}"
|
955
|
+
logger.error(f"💥 Error: {e}")
|
956
|
+
if logging_cfg.debug:
|
957
|
+
import traceback
|
958
|
+
logger.debug(traceback.format_exc())
|
959
|
+
|
960
|
+
except Exception as e:
|
961
|
+
log_handler.current_step = f"Error: {e}"
|
962
|
+
logger.error(f"💥 Setup error: {e}")
|
963
|
+
debug_mode = debug if debug is not None else config.logging.debug
|
964
|
+
if debug_mode:
|
965
|
+
import traceback
|
966
|
+
logger.debug(traceback.format_exc())
|
967
|
+
|
968
|
+
|
969
|
+
|
759
970
|
if __name__ == "__main__":
|
760
|
-
command = "
|
971
|
+
command = "set gboard to the default keyboard"
|
761
972
|
device = None
|
762
973
|
provider = "GoogleGenAI"
|
763
974
|
model = "models/gemini-2.5-flash"
|
@@ -765,7 +976,7 @@ if __name__ == "__main__":
|
|
765
976
|
api_key = os.getenv("GOOGLE_API_KEY")
|
766
977
|
steps = 15
|
767
978
|
vision = True
|
768
|
-
reasoning =
|
979
|
+
reasoning = False
|
769
980
|
tracing = True
|
770
981
|
debug = True
|
771
982
|
use_tcp = False
|
@@ -774,6 +985,6 @@ if __name__ == "__main__":
|
|
774
985
|
ios = False
|
775
986
|
save_trajectory = "none"
|
776
987
|
allow_drag = False
|
777
|
-
|
778
|
-
command
|
988
|
+
asyncio.run(
|
989
|
+
test(command)
|
779
990
|
)
|