Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion mobilerun/agent/executor/executor_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ async def prepare_context(
# Build prompt variables
variables = {
"instruction": self.shared_state.instruction,
"app_card": "",
"app_card": self.shared_state.app_card,
"device_state": self.shared_state.formatted_device_state,
"plan": self.shared_state.plan,
"subgoal": subgoal,
Expand Down
46 changes: 33 additions & 13 deletions mobilerun/config/app_cards/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,22 +118,42 @@ agent:
## Programmatic Usage

```python
from mobilerun.config_manager import AppCardLoader, config
import asyncio

# Load app card for a package
app_card = AppCardLoader.load_app_card(
package_name="com.google.android.gm",
app_cards_dir=config.agent.app_cards.app_cards_dir
from mobilerun.app_cards.providers import (
CompositeAppCardProvider,
LocalAppCardProvider,
ServerAppCardProvider,
)
from mobilerun.config_manager import ConfigLoader

# Check if enabled
if config.agent.app_cards.enabled:
print("App cards are enabled")

# Clear cache (useful for testing)
AppCardLoader.clear_cache()
async def main():
config = ConfigLoader.load()

# Get cache statistics
stats = AppCardLoader.get_cache_stats()
print(f"Cached entries: {stats['content_entries']}")
# Check if enabled and load a local app card.
if not config.agent.app_cards.enabled:
return

provider = LocalAppCardProvider(app_cards_dir=config.agent.app_cards.app_cards_dir)
app_card = await provider.load_app_card(
package_name="com.google.android.gm",
instruction="Summarize unread email",
)
print(app_card)

# Server-backed and server-with-local-fallback modes use:
server_provider = ServerAppCardProvider(server_url="https://example.com")
composite_provider = CompositeAppCardProvider(
server_url="https://example.com",
app_cards_dir=config.agent.app_cards.app_cards_dir,
)

# Clear cache and get cache statistics.
provider.clear_cache()
stats = provider.get_cache_stats()
print(f"Cached entries: {stats['content_entries']}")


asyncio.run(main())
```
93 changes: 93 additions & 0 deletions tests/test_app_cards.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import asyncio
import unittest
from pathlib import Path
from types import SimpleNamespace

from mobilerun.agent.droid.state import MobileAgentState
from mobilerun.agent.executor.executor_agent import ExecutorAgent
from mobilerun.agent.tool_registry import ToolRegistry
from mobilerun.agent.utils.prompt_resolver import PromptResolver
from mobilerun.app_cards.providers.local_provider import LocalAppCardProvider
from mobilerun.config_manager.config_manager import AgentConfig


class FakeStore:
def __init__(self):
self.values = {}

async def set(self, key, value):
self.values[key] = value

async def get(self, key):
return self.values[key]


class FakeWorkflowContext:
def __init__(self):
self.store = FakeStore()
self.events = []

def write_event_to_stream(self, event):
self.events.append(event)


class AppCardsTest(unittest.TestCase):
def test_executor_prompt_receives_shared_app_card(self):
async def run():
shared_state = MobileAgentState(
instruction="Inspect the app safely",
app_card="APP_CARD_SENTINEL",
formatted_device_state="Device state",
plan="Plan text",
progress_summary="Progress text",
platform="Android",
)
agent = ExecutorAgent(
llm=SimpleNamespace(),
registry=ToolRegistry(),
action_ctx=None,
shared_state=shared_state,
agent_config=AgentConfig(),
prompt_resolver=PromptResolver(
{
"executor_system": (
"Instruction={{ instruction }}\n"
"AppCard={{ app_card }}\n"
"State={{ device_state }}"
)
}
),
)
ctx = FakeWorkflowContext()

await agent.prepare_context(
ctx,
SimpleNamespace(get=lambda key, default="": "Read visible UI"),
)
return ctx.store.values["executor_messages"][0].blocks[0].text

prompt_text = asyncio.run(run())

self.assertIn("APP_CARD_SENTINEL", prompt_text)

def test_local_provider_loads_existing_app_card(self):
async def run():
provider = LocalAppCardProvider("config/app_cards")
return await provider.load_app_card("com.google.android.gm")

app_card = asyncio.run(run())

self.assertIn("Gmail App Guide", app_card)
self.assertIn("Search", app_card)
self.assertIn("compose", app_card)

def test_app_card_readme_uses_current_provider_api(self):
readme = Path("mobilerun/config/app_cards/README.md").read_text(encoding="utf-8")

self.assertNotIn("AppCardLoader", readme)
self.assertIn("LocalAppCardProvider", readme)
self.assertIn("await provider.load_app_card", readme)


if __name__ == "__main__":
unittest.main()
Loading