gothos 0.2.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.
- gothos/__init__.py +35 -0
- gothos/__main__.py +5 -0
- gothos/benchmark/__init__.py +14 -0
- gothos/benchmark/harness.py +237 -0
- gothos/benchmark/suites.py +205 -0
- gothos/cli.py +431 -0
- gothos/console/__init__.py +7 -0
- gothos/console/server.py +180 -0
- gothos/console/session_manager.py +218 -0
- gothos/console/static/index.html +573 -0
- gothos/core/__init__.py +29 -0
- gothos/core/agent.py +622 -0
- gothos/core/cache.py +130 -0
- gothos/core/compactor.py +182 -0
- gothos/core/config.py +114 -0
- gothos/core/retry.py +103 -0
- gothos/core/state.py +43 -0
- gothos/core/strategy.py +280 -0
- gothos/core/telemetry.py +211 -0
- gothos/device/__init__.py +26 -0
- gothos/device/actions.py +246 -0
- gothos/device/adb.py +217 -0
- gothos/device/screenshot.py +83 -0
- gothos/extract/__init__.py +13 -0
- gothos/extract/a11y_parser.py +229 -0
- gothos/extract/extractor.py +329 -0
- gothos/extract/mcp_tool.py +183 -0
- gothos/grounding/__init__.py +25 -0
- gothos/grounding/a11y_navigator.py +266 -0
- gothos/grounding/ocr_fallback.py +111 -0
- gothos/grounding/provider_agnostic.py +347 -0
- gothos/llm/__init__.py +9 -0
- gothos/llm/factory.py +81 -0
- gothos/mcp/__init__.py +32 -0
- gothos/mcp/models.py +182 -0
- gothos/mcp/server.py +222 -0
- gothos/mcp/tools.py +347 -0
- gothos/memory/__init__.py +15 -0
- gothos/memory/action_recorder.py +181 -0
- gothos/memory/app_navigator.py +188 -0
- gothos/memory/models.py +90 -0
- gothos/memory/retriever.py +181 -0
- gothos/memory/store.py +489 -0
- gothos/planner/__init__.py +7 -0
- gothos/planner/planner.py +329 -0
- gothos/planner/prompts.py +103 -0
- gothos/py.typed +0 -0
- gothos/safety/__init__.py +42 -0
- gothos/safety/action_classifier.py +359 -0
- gothos/safety/confirmation.py +382 -0
- gothos/safety/dialog_handler.py +424 -0
- gothos/safety/policies.py +242 -0
- gothos/screen/__init__.py +15 -0
- gothos/screen/analyzer.py +219 -0
- gothos/screen/models.py +49 -0
- gothos-0.2.0.dist-info/METADATA +460 -0
- gothos-0.2.0.dist-info/RECORD +60 -0
- gothos-0.2.0.dist-info/WHEEL +4 -0
- gothos-0.2.0.dist-info/entry_points.txt +2 -0
- gothos-0.2.0.dist-info/licenses/LICENSE +21 -0
gothos/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Copyright (c) 2026 GouthamRajam. MIT License.
|
|
2
|
+
|
|
3
|
+
"""GothOS — Android AI agent with safety, memory, planning and provider-agnostic grounding."""
|
|
4
|
+
|
|
5
|
+
__version__ = "0.2.0"
|
|
6
|
+
|
|
7
|
+
from gothos import (
|
|
8
|
+
benchmark,
|
|
9
|
+
console,
|
|
10
|
+
core,
|
|
11
|
+
device,
|
|
12
|
+
extract,
|
|
13
|
+
grounding,
|
|
14
|
+
llm,
|
|
15
|
+
mcp,
|
|
16
|
+
memory,
|
|
17
|
+
planner,
|
|
18
|
+
safety,
|
|
19
|
+
screen,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"benchmark",
|
|
24
|
+
"console",
|
|
25
|
+
"core",
|
|
26
|
+
"device",
|
|
27
|
+
"extract",
|
|
28
|
+
"grounding",
|
|
29
|
+
"llm",
|
|
30
|
+
"mcp",
|
|
31
|
+
"memory",
|
|
32
|
+
"planner",
|
|
33
|
+
"safety",
|
|
34
|
+
"screen",
|
|
35
|
+
]
|
gothos/__main__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Copyright (c) 2026 GouthamRajam. MIT License.
|
|
2
|
+
|
|
3
|
+
"""Benchmark harness — run GothOS against standardised task suites."""
|
|
4
|
+
|
|
5
|
+
from gothos.benchmark.harness import BenchmarkHarness, BenchmarkResult, TaskResult
|
|
6
|
+
from gothos.benchmark.suites import TaskSuite, load_suite
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"BenchmarkHarness",
|
|
10
|
+
"BenchmarkResult",
|
|
11
|
+
"TaskResult",
|
|
12
|
+
"TaskSuite",
|
|
13
|
+
"load_suite",
|
|
14
|
+
]
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# Copyright (c) 2026 GouthamRajam. MIT License.
|
|
2
|
+
|
|
3
|
+
"""Benchmark harness — executes task suites and collects results."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
|
|
14
|
+
from gothos.benchmark.suites import TaskCase, TaskSuite
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TaskResult(BaseModel):
|
|
20
|
+
"""Outcome of running one benchmark task."""
|
|
21
|
+
|
|
22
|
+
task_id: str
|
|
23
|
+
goal: str
|
|
24
|
+
category: str = ""
|
|
25
|
+
difficulty: str = ""
|
|
26
|
+
success: bool = False
|
|
27
|
+
steps_taken: int = 0
|
|
28
|
+
max_steps: int = 0
|
|
29
|
+
duration_seconds: float = 0.0
|
|
30
|
+
execution_mode: str = ""
|
|
31
|
+
error: str | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BenchmarkResult(BaseModel):
|
|
35
|
+
"""Aggregate results from a full benchmark run."""
|
|
36
|
+
|
|
37
|
+
suite_name: str
|
|
38
|
+
suite_version: str = ""
|
|
39
|
+
provider: str = ""
|
|
40
|
+
model: str = ""
|
|
41
|
+
execution_mode: str = ""
|
|
42
|
+
total_tasks: int = 0
|
|
43
|
+
passed: int = 0
|
|
44
|
+
failed: int = 0
|
|
45
|
+
skipped: int = 0
|
|
46
|
+
pass_rate: float = Field(default=0.0, ge=0.0, le=100.0)
|
|
47
|
+
total_duration_seconds: float = 0.0
|
|
48
|
+
avg_steps: float = 0.0
|
|
49
|
+
results: list[TaskResult] = Field(default_factory=list)
|
|
50
|
+
|
|
51
|
+
def by_category(self) -> dict[str, dict[str, Any]]:
|
|
52
|
+
"""Break down pass rates by category."""
|
|
53
|
+
cats: dict[str, list[TaskResult]] = {}
|
|
54
|
+
for r in self.results:
|
|
55
|
+
cats.setdefault(r.category, []).append(r)
|
|
56
|
+
return {
|
|
57
|
+
cat: {
|
|
58
|
+
"total": len(rs),
|
|
59
|
+
"passed": sum(1 for r in rs if r.success),
|
|
60
|
+
"pass_rate": round(
|
|
61
|
+
sum(1 for r in rs if r.success) / len(rs) * 100, 1
|
|
62
|
+
),
|
|
63
|
+
}
|
|
64
|
+
for cat, rs in cats.items()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
def by_difficulty(self) -> dict[str, dict[str, Any]]:
|
|
68
|
+
"""Break down pass rates by difficulty."""
|
|
69
|
+
diffs: dict[str, list[TaskResult]] = {}
|
|
70
|
+
for r in self.results:
|
|
71
|
+
diffs.setdefault(r.difficulty, []).append(r)
|
|
72
|
+
return {
|
|
73
|
+
diff: {
|
|
74
|
+
"total": len(rs),
|
|
75
|
+
"passed": sum(1 for r in rs if r.success),
|
|
76
|
+
"pass_rate": round(
|
|
77
|
+
sum(1 for r in rs if r.success) / len(rs) * 100, 1
|
|
78
|
+
),
|
|
79
|
+
}
|
|
80
|
+
for diff, rs in diffs.items()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
def summary(self) -> str:
|
|
84
|
+
"""Human-readable summary."""
|
|
85
|
+
lines = [
|
|
86
|
+
f"Benchmark: {self.suite_name} v{self.suite_version}",
|
|
87
|
+
f"Provider: {self.provider} / {self.model}",
|
|
88
|
+
f"Mode: {self.execution_mode}",
|
|
89
|
+
f"Results: {self.passed}/{self.total_tasks} passed "
|
|
90
|
+
f"({self.pass_rate:.1f}%)",
|
|
91
|
+
f"Duration: {self.total_duration_seconds:.1f}s total, "
|
|
92
|
+
f"{self.avg_steps:.1f} avg steps",
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
cats = self.by_category()
|
|
96
|
+
if cats:
|
|
97
|
+
lines.append("\nBy category:")
|
|
98
|
+
for cat, stats in sorted(cats.items()):
|
|
99
|
+
lines.append(
|
|
100
|
+
f" {cat}: {stats['passed']}/{stats['total']} "
|
|
101
|
+
f"({stats['pass_rate']}%)"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return "\n".join(lines)
|
|
105
|
+
|
|
106
|
+
def to_json(self, path: str | Path) -> None:
|
|
107
|
+
"""Write results to a JSON file."""
|
|
108
|
+
Path(path).write_text(self.model_dump_json(indent=2))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class BenchmarkHarness:
|
|
112
|
+
"""Runs a :class:`TaskSuite` against a GothOS agent and collects results.
|
|
113
|
+
|
|
114
|
+
Usage::
|
|
115
|
+
|
|
116
|
+
harness = BenchmarkHarness(
|
|
117
|
+
provider="anthropic",
|
|
118
|
+
model="claude-sonnet-4-20250514",
|
|
119
|
+
execution_mode="auto",
|
|
120
|
+
)
|
|
121
|
+
result = await harness.run(suite)
|
|
122
|
+
print(result.summary())
|
|
123
|
+
result.to_json("benchmark_results.json")
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
def __init__(
|
|
127
|
+
self,
|
|
128
|
+
provider: str = "anthropic",
|
|
129
|
+
model: str = "claude-sonnet-4-20250514",
|
|
130
|
+
execution_mode: str = "auto",
|
|
131
|
+
device_serial: str | None = None,
|
|
132
|
+
**agent_kwargs: Any,
|
|
133
|
+
) -> None:
|
|
134
|
+
self._provider = provider
|
|
135
|
+
self._model = model
|
|
136
|
+
self._execution_mode = execution_mode
|
|
137
|
+
self._device_serial = device_serial
|
|
138
|
+
self._agent_kwargs = agent_kwargs
|
|
139
|
+
|
|
140
|
+
async def run(
|
|
141
|
+
self,
|
|
142
|
+
suite: TaskSuite,
|
|
143
|
+
skip_categories: set[str] | None = None,
|
|
144
|
+
) -> BenchmarkResult:
|
|
145
|
+
"""Execute every task in *suite* and return aggregate results."""
|
|
146
|
+
|
|
147
|
+
results: list[TaskResult] = []
|
|
148
|
+
skipped = 0
|
|
149
|
+
start = time.monotonic()
|
|
150
|
+
|
|
151
|
+
for task in suite.tasks:
|
|
152
|
+
if skip_categories and task.category in skip_categories:
|
|
153
|
+
skipped += 1
|
|
154
|
+
logger.info("Skipping task %s (category: %s)", task.id, task.category)
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
logger.info("Running task %s: %s", task.id, task.goal)
|
|
158
|
+
task_result = await self._run_task(task)
|
|
159
|
+
results.append(task_result)
|
|
160
|
+
|
|
161
|
+
status = "PASS" if task_result.success else "FAIL"
|
|
162
|
+
logger.info(
|
|
163
|
+
" %s — %s (%.1fs, %d steps)",
|
|
164
|
+
task.id, status, task_result.duration_seconds, task_result.steps_taken,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
total_duration = time.monotonic() - start
|
|
168
|
+
passed = sum(1 for r in results if r.success)
|
|
169
|
+
total = len(results)
|
|
170
|
+
|
|
171
|
+
return BenchmarkResult(
|
|
172
|
+
suite_name=suite.name,
|
|
173
|
+
suite_version=suite.version,
|
|
174
|
+
provider=self._provider,
|
|
175
|
+
model=self._model,
|
|
176
|
+
execution_mode=self._execution_mode,
|
|
177
|
+
total_tasks=total + skipped,
|
|
178
|
+
passed=passed,
|
|
179
|
+
failed=total - passed,
|
|
180
|
+
skipped=skipped,
|
|
181
|
+
pass_rate=round(passed / total * 100, 1) if total else 0.0,
|
|
182
|
+
total_duration_seconds=round(total_duration, 2),
|
|
183
|
+
avg_steps=round(
|
|
184
|
+
sum(r.steps_taken for r in results) / total, 1
|
|
185
|
+
) if total else 0.0,
|
|
186
|
+
results=results,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
async def _run_task(self, task: TaskCase) -> TaskResult:
|
|
190
|
+
"""Run a single task and return its result."""
|
|
191
|
+
from gothos.core.agent import GothOSAgent
|
|
192
|
+
from gothos.core.config import AgentConfig
|
|
193
|
+
|
|
194
|
+
config = AgentConfig(
|
|
195
|
+
llm_provider=self._provider,
|
|
196
|
+
model=self._model,
|
|
197
|
+
execution_mode=self._execution_mode,
|
|
198
|
+
device_serial=self._device_serial,
|
|
199
|
+
max_steps=task.max_steps,
|
|
200
|
+
**self._agent_kwargs,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
agent = GothOSAgent(config)
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
await agent.initialize()
|
|
207
|
+
run_result = await agent.run(
|
|
208
|
+
goal=task.goal,
|
|
209
|
+
app_package=task.app_package,
|
|
210
|
+
)
|
|
211
|
+
return TaskResult(
|
|
212
|
+
task_id=task.id,
|
|
213
|
+
goal=task.goal,
|
|
214
|
+
category=task.category,
|
|
215
|
+
difficulty=task.difficulty,
|
|
216
|
+
success=run_result.success,
|
|
217
|
+
steps_taken=run_result.steps_taken,
|
|
218
|
+
max_steps=task.max_steps,
|
|
219
|
+
duration_seconds=run_result.duration_seconds,
|
|
220
|
+
execution_mode=run_result.execution_mode,
|
|
221
|
+
error=run_result.error_message,
|
|
222
|
+
)
|
|
223
|
+
except Exception as exc:
|
|
224
|
+
logger.exception("Task %s crashed: %s", task.id, exc)
|
|
225
|
+
return TaskResult(
|
|
226
|
+
task_id=task.id,
|
|
227
|
+
goal=task.goal,
|
|
228
|
+
category=task.category,
|
|
229
|
+
difficulty=task.difficulty,
|
|
230
|
+
success=False,
|
|
231
|
+
error=str(exc),
|
|
232
|
+
)
|
|
233
|
+
finally:
|
|
234
|
+
import contextlib
|
|
235
|
+
|
|
236
|
+
with contextlib.suppress(Exception):
|
|
237
|
+
await agent.shutdown()
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Copyright (c) 2026 GouthamRajam. MIT License.
|
|
2
|
+
|
|
3
|
+
"""Task suite definitions for benchmarking."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TaskCase(BaseModel):
|
|
14
|
+
"""A single benchmark task."""
|
|
15
|
+
|
|
16
|
+
id: str
|
|
17
|
+
goal: str
|
|
18
|
+
app_package: str
|
|
19
|
+
category: str = "general"
|
|
20
|
+
difficulty: str = "medium"
|
|
21
|
+
max_steps: int = 50
|
|
22
|
+
expected_outcome: str = ""
|
|
23
|
+
verification_steps: list[str] = Field(default_factory=list)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TaskSuite(BaseModel):
|
|
27
|
+
"""A collection of benchmark tasks."""
|
|
28
|
+
|
|
29
|
+
name: str
|
|
30
|
+
version: str = "1.0"
|
|
31
|
+
description: str = ""
|
|
32
|
+
tasks: list[TaskCase] = Field(default_factory=list)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def task_count(self) -> int:
|
|
36
|
+
return len(self.tasks)
|
|
37
|
+
|
|
38
|
+
def by_category(self) -> dict[str, list[TaskCase]]:
|
|
39
|
+
groups: dict[str, list[TaskCase]] = {}
|
|
40
|
+
for task in self.tasks:
|
|
41
|
+
groups.setdefault(task.category, []).append(task)
|
|
42
|
+
return groups
|
|
43
|
+
|
|
44
|
+
def by_difficulty(self) -> dict[str, list[TaskCase]]:
|
|
45
|
+
groups: dict[str, list[TaskCase]] = {}
|
|
46
|
+
for task in self.tasks:
|
|
47
|
+
groups.setdefault(task.difficulty, []).append(task)
|
|
48
|
+
return groups
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def load_suite(path: str | Path) -> TaskSuite:
|
|
52
|
+
"""Load a task suite from a JSON file."""
|
|
53
|
+
data = json.loads(Path(path).read_text())
|
|
54
|
+
return TaskSuite.model_validate(data)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
# Built-in suites
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
CORE_SUITE = TaskSuite(
|
|
62
|
+
name="gothos-core",
|
|
63
|
+
version="1.0",
|
|
64
|
+
description="Core functionality benchmark — basic device operations",
|
|
65
|
+
tasks=[
|
|
66
|
+
TaskCase(
|
|
67
|
+
id="core-001",
|
|
68
|
+
goal="Open the Settings app",
|
|
69
|
+
app_package="com.android.settings",
|
|
70
|
+
category="navigation",
|
|
71
|
+
difficulty="easy",
|
|
72
|
+
max_steps=5,
|
|
73
|
+
expected_outcome="Settings app is visible",
|
|
74
|
+
),
|
|
75
|
+
TaskCase(
|
|
76
|
+
id="core-002",
|
|
77
|
+
goal="Enable Wi-Fi in Settings",
|
|
78
|
+
app_package="com.android.settings",
|
|
79
|
+
category="settings",
|
|
80
|
+
difficulty="easy",
|
|
81
|
+
max_steps=10,
|
|
82
|
+
expected_outcome="Wi-Fi toggle is enabled",
|
|
83
|
+
),
|
|
84
|
+
TaskCase(
|
|
85
|
+
id="core-003",
|
|
86
|
+
goal="Open Chrome and navigate to google.com",
|
|
87
|
+
app_package="com.android.chrome",
|
|
88
|
+
category="navigation",
|
|
89
|
+
difficulty="medium",
|
|
90
|
+
max_steps=15,
|
|
91
|
+
expected_outcome="Google search page is displayed",
|
|
92
|
+
),
|
|
93
|
+
TaskCase(
|
|
94
|
+
id="core-004",
|
|
95
|
+
goal="Take a screenshot and save it",
|
|
96
|
+
app_package="",
|
|
97
|
+
category="device",
|
|
98
|
+
difficulty="easy",
|
|
99
|
+
max_steps=5,
|
|
100
|
+
expected_outcome="Screenshot captured",
|
|
101
|
+
),
|
|
102
|
+
TaskCase(
|
|
103
|
+
id="core-005",
|
|
104
|
+
goal="Open Contacts and search for a name",
|
|
105
|
+
app_package="com.android.contacts",
|
|
106
|
+
category="search",
|
|
107
|
+
difficulty="medium",
|
|
108
|
+
max_steps=15,
|
|
109
|
+
expected_outcome="Contact search results displayed",
|
|
110
|
+
),
|
|
111
|
+
TaskCase(
|
|
112
|
+
id="core-006",
|
|
113
|
+
goal="Change display brightness to maximum",
|
|
114
|
+
app_package="com.android.settings",
|
|
115
|
+
category="settings",
|
|
116
|
+
difficulty="medium",
|
|
117
|
+
max_steps=15,
|
|
118
|
+
expected_outcome="Brightness slider at maximum",
|
|
119
|
+
),
|
|
120
|
+
TaskCase(
|
|
121
|
+
id="core-007",
|
|
122
|
+
goal="Enable Dark Mode in display settings",
|
|
123
|
+
app_package="com.android.settings",
|
|
124
|
+
category="settings",
|
|
125
|
+
difficulty="medium",
|
|
126
|
+
max_steps=20,
|
|
127
|
+
expected_outcome="Dark mode is active",
|
|
128
|
+
),
|
|
129
|
+
TaskCase(
|
|
130
|
+
id="core-008",
|
|
131
|
+
goal="Open the calculator and compute 42 * 7",
|
|
132
|
+
app_package="com.google.android.calculator",
|
|
133
|
+
category="app_interaction",
|
|
134
|
+
difficulty="medium",
|
|
135
|
+
max_steps=15,
|
|
136
|
+
expected_outcome="Result 294 is displayed",
|
|
137
|
+
),
|
|
138
|
+
TaskCase(
|
|
139
|
+
id="core-009",
|
|
140
|
+
goal="Set an alarm for 7:30 AM",
|
|
141
|
+
app_package="com.google.android.deskclock",
|
|
142
|
+
category="app_interaction",
|
|
143
|
+
difficulty="hard",
|
|
144
|
+
max_steps=25,
|
|
145
|
+
expected_outcome="Alarm set for 7:30 AM",
|
|
146
|
+
),
|
|
147
|
+
TaskCase(
|
|
148
|
+
id="core-010",
|
|
149
|
+
goal="Open Google Maps and search for restaurants nearby",
|
|
150
|
+
app_package="com.google.android.apps.maps",
|
|
151
|
+
category="search",
|
|
152
|
+
difficulty="hard",
|
|
153
|
+
max_steps=20,
|
|
154
|
+
expected_outcome="Map shows restaurant results",
|
|
155
|
+
),
|
|
156
|
+
],
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
CROSS_APP_SUITE = TaskSuite(
|
|
160
|
+
name="gothos-cross-app",
|
|
161
|
+
version="1.0",
|
|
162
|
+
description="Cross-app workflows — tasks spanning multiple applications",
|
|
163
|
+
tasks=[
|
|
164
|
+
TaskCase(
|
|
165
|
+
id="xapp-001",
|
|
166
|
+
goal="Copy text from Chrome and paste it into Notes",
|
|
167
|
+
app_package="com.android.chrome",
|
|
168
|
+
category="cross_app",
|
|
169
|
+
difficulty="hard",
|
|
170
|
+
max_steps=30,
|
|
171
|
+
expected_outcome="Text appears in Notes app",
|
|
172
|
+
),
|
|
173
|
+
TaskCase(
|
|
174
|
+
id="xapp-002",
|
|
175
|
+
goal="Take a photo with Camera and share it via Messages",
|
|
176
|
+
app_package="com.android.camera",
|
|
177
|
+
category="cross_app",
|
|
178
|
+
difficulty="hard",
|
|
179
|
+
max_steps=35,
|
|
180
|
+
expected_outcome="Share dialog shows Messages option",
|
|
181
|
+
),
|
|
182
|
+
TaskCase(
|
|
183
|
+
id="xapp-003",
|
|
184
|
+
goal="Look up a contact in Contacts and call them via Phone",
|
|
185
|
+
app_package="com.android.contacts",
|
|
186
|
+
category="cross_app",
|
|
187
|
+
difficulty="hard",
|
|
188
|
+
max_steps=25,
|
|
189
|
+
expected_outcome="Phone dialer shows contact number",
|
|
190
|
+
),
|
|
191
|
+
],
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
BUILT_IN_SUITES: dict[str, TaskSuite] = {
|
|
195
|
+
"core": CORE_SUITE,
|
|
196
|
+
"cross-app": CROSS_APP_SUITE,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def get_suite(name: str) -> TaskSuite:
|
|
201
|
+
"""Get a built-in suite by name."""
|
|
202
|
+
if name not in BUILT_IN_SUITES:
|
|
203
|
+
available = ", ".join(BUILT_IN_SUITES.keys())
|
|
204
|
+
raise ValueError(f"Unknown suite '{name}'. Available: {available}")
|
|
205
|
+
return BUILT_IN_SUITES[name]
|