droidrun 0.3.10.dev5__py3-none-any.whl → 0.3.10.dev7__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 (31) hide show
  1. droidrun/agent/codeact/codeact_agent.py +21 -29
  2. droidrun/agent/context/task_manager.py +0 -1
  3. droidrun/agent/droid/droid_agent.py +1 -3
  4. droidrun/agent/droid/events.py +6 -3
  5. droidrun/agent/executor/executor_agent.py +24 -38
  6. droidrun/agent/executor/prompts.py +0 -108
  7. droidrun/agent/manager/__init__.py +1 -1
  8. droidrun/agent/manager/manager_agent.py +104 -87
  9. droidrun/agent/utils/executer.py +11 -10
  10. droidrun/agent/utils/llm_picker.py +63 -1
  11. droidrun/agent/utils/tools.py +30 -1
  12. droidrun/app_cards/app_card_provider.py +26 -0
  13. droidrun/app_cards/providers/__init__.py +7 -0
  14. droidrun/app_cards/providers/composite_provider.py +97 -0
  15. droidrun/app_cards/providers/local_provider.py +115 -0
  16. droidrun/app_cards/providers/server_provider.py +126 -0
  17. droidrun/cli/logs.py +4 -4
  18. droidrun/cli/main.py +244 -34
  19. droidrun/config_manager/__init__.py +0 -2
  20. droidrun/config_manager/config_manager.py +45 -102
  21. droidrun/config_manager/path_resolver.py +1 -1
  22. droidrun/config_manager/prompt_loader.py +48 -51
  23. droidrun/macro/cli.py +0 -1
  24. droidrun/portal.py +17 -0
  25. droidrun/tools/adb.py +13 -34
  26. {droidrun-0.3.10.dev5.dist-info → droidrun-0.3.10.dev7.dist-info}/METADATA +2 -9
  27. {droidrun-0.3.10.dev5.dist-info → droidrun-0.3.10.dev7.dist-info}/RECORD +30 -26
  28. droidrun/config_manager/app_card_loader.py +0 -148
  29. {droidrun-0.3.10.dev5.dist-info → droidrun-0.3.10.dev7.dist-info}/WHEEL +0 -0
  30. {droidrun-0.3.10.dev5.dist-info → droidrun-0.3.10.dev7.dist-info}/entry_points.txt +0 -0
  31. {droidrun-0.3.10.dev5.dist-info → droidrun-0.3.10.dev7.dist-info}/licenses/LICENSE +0 -0
@@ -1,9 +1,10 @@
1
- import io
2
1
  import contextlib
3
- import traceback
2
+ import io
4
3
  import logging
5
- from typing import Any, Dict, Optional
4
+ import traceback
6
5
  from asyncio import AbstractEventLoop
6
+ from typing import Any, Dict, Optional
7
+
7
8
  from pydantic import BaseModel
8
9
 
9
10
  logger = logging.getLogger("droidrun")
@@ -11,7 +12,7 @@ logger = logging.getLogger("droidrun")
11
12
  class ExecuterState(BaseModel):
12
13
  """State object for the code executor."""
13
14
  ui_state: Optional[Any] = None
14
-
15
+
15
16
  class Config:
16
17
  arbitrary_types_allowed = True
17
18
 
@@ -70,7 +71,7 @@ class SimpleCodeExecutor:
70
71
  self.locals = locals
71
72
  self.loop = loop
72
73
  self.use_same_scope = use_same_scope
73
-
74
+
74
75
  if self.use_same_scope:
75
76
  # If using the same scope, merge globals and locals
76
77
  self.globals = self.locals = {
@@ -85,7 +86,7 @@ class SimpleCodeExecutor:
85
86
  """
86
87
  # Update UI state
87
88
  self.globals['ui_state'] = ui_state
88
-
89
+
89
90
  # Capture stdout and stderr
90
91
  stdout = io.StringIO()
91
92
  stderr = io.StringIO()
@@ -111,7 +112,7 @@ class SimpleCodeExecutor:
111
112
  async def execute(self, state: ExecuterState, code: str) -> str:
112
113
  """
113
114
  Execute Python code and capture output and return values.
114
-
115
+
115
116
  Runs the code in a separate thread to prevent blocking.
116
117
 
117
118
  Args:
@@ -123,7 +124,7 @@ class SimpleCodeExecutor:
123
124
  """
124
125
  # Get UI state from the state object
125
126
  ui_state = state.ui_state
126
-
127
+
127
128
  # Run the execution in a thread pool executor
128
129
  output = await self.loop.run_in_executor(
129
130
  None,
@@ -131,5 +132,5 @@ class SimpleCodeExecutor:
131
132
  code,
132
133
  ui_state
133
134
  )
134
-
135
- return output
135
+
136
+ return output
@@ -1,11 +1,14 @@
1
1
  import importlib
2
2
  import logging
3
- from typing import Any
3
+ from typing import TYPE_CHECKING, Any
4
4
 
5
5
  from llama_index.core.llms.llm import LLM
6
6
 
7
7
  from droidrun.agent.usage import track_usage
8
8
 
9
+ if TYPE_CHECKING:
10
+ from droidrun.config_manager.config_manager import LLMProfile
11
+
9
12
  # Configure logging
10
13
  logger = logging.getLogger("droidrun")
11
14
 
@@ -104,6 +107,65 @@ def load_llm(provider_name: str, **kwargs: Any) -> LLM:
104
107
  raise e
105
108
 
106
109
 
110
+ def load_llms_from_profiles(
111
+ profiles: dict[str, "LLMProfile"],
112
+ profile_names: list[str] | None = None,
113
+ **override_kwargs_per_profile
114
+ ) -> dict[str, LLM]:
115
+ """
116
+ Load multiple LLMs from LLMProfile objects.
117
+
118
+ Args:
119
+ profiles: Dict of profile_name -> LLMProfile objects
120
+ profile_names: List of profile names to load. If None, loads all profiles
121
+ **override_kwargs_per_profile: Dict of profile-specific overrides
122
+ Example: manager={'temperature': 0.1}, executor={'max_tokens': 8000}
123
+
124
+ Returns:
125
+ Dict mapping profile names to initialized LLM instances
126
+
127
+ Example:
128
+ >>> config = ConfigManager()
129
+ >>> llms = load_llms_from_profiles(config.llm_profiles)
130
+ >>> manager_llm = llms['manager']
131
+
132
+ >>> # Load specific profiles with overrides
133
+ >>> llms = load_llms_from_profiles(
134
+ ... config.llm_profiles,
135
+ ... profile_names=['manager', 'executor'],
136
+ ... manager={'temperature': 0.1}
137
+ ... )
138
+ """
139
+ if profile_names is None:
140
+ profile_names = list(profiles.keys())
141
+
142
+ llms = {}
143
+ for profile_name in profile_names:
144
+ logger.debug(f"Loading LLM for profile: {profile_name}")
145
+
146
+ if profile_name not in profiles:
147
+ raise KeyError(
148
+ f"Profile '{profile_name}' not found. "
149
+ f"Available profiles: {list(profiles.keys())}"
150
+ )
151
+
152
+ profile = profiles[profile_name]
153
+
154
+ # Get base kwargs from profile
155
+ kwargs = profile.to_load_llm_kwargs()
156
+
157
+ # Apply profile-specific overrides if provided
158
+ if profile_name in override_kwargs_per_profile:
159
+ logger.debug(f"Applying overrides for {profile_name}: {override_kwargs_per_profile[profile_name]}")
160
+ kwargs.update(override_kwargs_per_profile[profile_name])
161
+
162
+ # Load the LLM
163
+ llms[profile_name] = load_llm(provider_name=profile.provider, **kwargs)
164
+ logger.debug(f"Successfully loaded {profile_name} LLM: {profile.provider}/{profile.model}")
165
+
166
+ return llms
167
+
168
+
107
169
  # --- Example Usage ---
108
170
  if __name__ == "__main__":
109
171
  # Install the specific LLM integrations you want to test:
@@ -1,5 +1,5 @@
1
- from typing import TYPE_CHECKING, List
2
1
  import time
2
+ from typing import TYPE_CHECKING, List
3
3
 
4
4
  if TYPE_CHECKING:
5
5
  from droidrun.tools import Tools
@@ -127,6 +127,35 @@ def open_app(tool_instance: "Tools", text: str) -> str:
127
127
  return result
128
128
 
129
129
 
130
+ def remember(tool_instance: "Tools", information: str) -> str:
131
+ """
132
+ Remember important information for later use.
133
+
134
+ Args:
135
+ tool_instance: The Tools instance
136
+ information: The information to remember
137
+
138
+ Returns:
139
+ Confirmation message
140
+ """
141
+ return tool_instance.remember(information)
142
+
143
+
144
+ def complete(tool_instance: "Tools", success: bool, reason: str = "") -> None:
145
+ """
146
+ Mark the task as complete.
147
+
148
+ Args:
149
+ tool_instance: The Tools instance
150
+ success: Whether the task was completed successfully
151
+ reason: Explanation for success or failure
152
+
153
+ Returns:
154
+ None
155
+ """
156
+ tool_instance.complete(success, reason)
157
+
158
+
130
159
  # =============================================================================
131
160
  # ATOMIC ACTION SIGNATURES - Single source of truth for both Executor and CodeAct
132
161
  # =============================================================================
@@ -0,0 +1,26 @@
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
+ class AppCardProvider(ABC):
12
+ """Abstract interface for loading app-specific instruction cards."""
13
+
14
+ @abstractmethod
15
+ async def load_app_card(self, package_name: str, instruction: str = "") -> str:
16
+ """
17
+ Load app card for a given package asynchronously.
18
+
19
+ Args:
20
+ package_name: Android package name (e.g., "com.google.android.gm")
21
+ instruction: User's instruction/goal (optional context for server providers)
22
+
23
+ Returns:
24
+ App card content as string, or empty string if not found or on error
25
+ """
26
+ pass
@@ -0,0 +1,7 @@
1
+ """App card provider implementations."""
2
+
3
+ from droidrun.app_cards.providers.composite_provider import CompositeAppCardProvider
4
+ from droidrun.app_cards.providers.local_provider import LocalAppCardProvider
5
+ from droidrun.app_cards.providers.server_provider import ServerAppCardProvider
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.app_cards.app_card_provider import AppCardProvider
11
+ from droidrun.app_cards.providers.local_provider import LocalAppCardProvider
12
+ from droidrun.app_cards.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,115 @@
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 typing import Dict
10
+
11
+ from droidrun.app_cards.app_card_provider import AppCardProvider
12
+ from droidrun.config_manager.path_resolver import PathResolver
13
+
14
+ logger = logging.getLogger("droidrun")
15
+
16
+
17
+ class LocalAppCardProvider(AppCardProvider):
18
+ """Load app cards from local filesystem with in-memory caching."""
19
+
20
+ def __init__(self, app_cards_dir: str = "config/app_cards"):
21
+ """
22
+ Initialize local provider.
23
+
24
+ Args:
25
+ app_cards_dir: Directory containing app_cards.json and markdown files
26
+ """
27
+ # Resolve app_cards.json path once
28
+ mapping_path = PathResolver.resolve(f"{app_cards_dir}/app_cards.json")
29
+ self.app_cards_dir = mapping_path.parent
30
+
31
+ # Load mapping immediately
32
+ try:
33
+ if mapping_path.exists():
34
+ with open(mapping_path, "r", encoding="utf-8") as f:
35
+ self.mapping = json.load(f)
36
+ logger.debug(f"Loaded app_cards.json with {len(self.mapping)} entries")
37
+ else:
38
+ logger.warning(f"app_cards.json not found at {mapping_path}")
39
+ self.mapping = {}
40
+ except Exception as e:
41
+ logger.warning(f"Failed to load app_cards.json: {e}")
42
+ self.mapping = {}
43
+
44
+ # Content cache: (package_name, instruction) -> content
45
+ self._content_cache: Dict[tuple[str, str], str] = {}
46
+
47
+ async def load_app_card(self, package_name: str, instruction: str = "") -> str:
48
+ """
49
+ Load app card for a package name from local files.
50
+
51
+ Args:
52
+ package_name: Android package name (e.g., "com.google.android.gm")
53
+ instruction: User instruction (for cache key consistency, not used in loading)
54
+
55
+ Returns:
56
+ App card content or empty string if not found
57
+ """
58
+ if not package_name:
59
+ return ""
60
+
61
+ # Check content cache first
62
+ cache_key = (package_name, instruction)
63
+ if cache_key in self._content_cache:
64
+ logger.debug(f"App card cache hit: {package_name}")
65
+ return self._content_cache[cache_key]
66
+
67
+ # Check if package exists in mapping
68
+ if package_name not in self.mapping:
69
+ self._content_cache[cache_key] = ""
70
+ return ""
71
+
72
+ # Get app card file path (relative to app_cards_dir)
73
+ filename = self.mapping[package_name]
74
+ app_card_path = self.app_cards_dir / filename
75
+
76
+ # Read file
77
+ try:
78
+ if not app_card_path.exists():
79
+ self._content_cache[cache_key] = ""
80
+ logger.debug(f"App card not found: {app_card_path}")
81
+ return ""
82
+
83
+ # Async file read
84
+ import asyncio
85
+ loop = asyncio.get_event_loop()
86
+ content = await loop.run_in_executor(
87
+ None, app_card_path.read_text, "utf-8"
88
+ )
89
+
90
+ # Cache and return
91
+ self._content_cache[cache_key] = content
92
+ logger.info(f"Loaded app card for {package_name} from {app_card_path}")
93
+ return content
94
+
95
+ except Exception as e:
96
+ logger.warning(f"Failed to load app card for {package_name}: {e}")
97
+ self._content_cache[cache_key] = ""
98
+ return ""
99
+
100
+ def clear_cache(self) -> None:
101
+ """Clear content cache (useful for testing or runtime reloading)."""
102
+ self._content_cache.clear()
103
+ logger.debug("Local app card cache cleared")
104
+
105
+ def get_cache_stats(self) -> Dict[str, int]:
106
+ """
107
+ Get cache statistics.
108
+
109
+ Returns:
110
+ Dict with cache stats (useful for debugging)
111
+ """
112
+ return {
113
+ "content_entries": len(self._content_cache),
114
+ "mapping_entries": len(self.mapping),
115
+ }
@@ -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.app_cards.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/logs.py CHANGED
@@ -21,14 +21,14 @@ from droidrun.agent.droid.events import (
21
21
  FinalizeEvent,
22
22
  TaskRunnerEvent,
23
23
  )
24
- from droidrun.agent.manager.events import (
25
- ManagerInternalPlanEvent,
26
- ManagerThinkingEvent,
27
- )
28
24
  from droidrun.agent.executor.events import (
29
25
  ExecutorInternalActionEvent,
30
26
  ExecutorInternalResultEvent,
31
27
  )
28
+ from droidrun.agent.manager.events import (
29
+ ManagerInternalPlanEvent,
30
+ ManagerThinkingEvent,
31
+ )
32
32
 
33
33
 
34
34
  class LogHandler(logging.Handler):