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.
Files changed (60) hide show
  1. gothos/__init__.py +35 -0
  2. gothos/__main__.py +5 -0
  3. gothos/benchmark/__init__.py +14 -0
  4. gothos/benchmark/harness.py +237 -0
  5. gothos/benchmark/suites.py +205 -0
  6. gothos/cli.py +431 -0
  7. gothos/console/__init__.py +7 -0
  8. gothos/console/server.py +180 -0
  9. gothos/console/session_manager.py +218 -0
  10. gothos/console/static/index.html +573 -0
  11. gothos/core/__init__.py +29 -0
  12. gothos/core/agent.py +622 -0
  13. gothos/core/cache.py +130 -0
  14. gothos/core/compactor.py +182 -0
  15. gothos/core/config.py +114 -0
  16. gothos/core/retry.py +103 -0
  17. gothos/core/state.py +43 -0
  18. gothos/core/strategy.py +280 -0
  19. gothos/core/telemetry.py +211 -0
  20. gothos/device/__init__.py +26 -0
  21. gothos/device/actions.py +246 -0
  22. gothos/device/adb.py +217 -0
  23. gothos/device/screenshot.py +83 -0
  24. gothos/extract/__init__.py +13 -0
  25. gothos/extract/a11y_parser.py +229 -0
  26. gothos/extract/extractor.py +329 -0
  27. gothos/extract/mcp_tool.py +183 -0
  28. gothos/grounding/__init__.py +25 -0
  29. gothos/grounding/a11y_navigator.py +266 -0
  30. gothos/grounding/ocr_fallback.py +111 -0
  31. gothos/grounding/provider_agnostic.py +347 -0
  32. gothos/llm/__init__.py +9 -0
  33. gothos/llm/factory.py +81 -0
  34. gothos/mcp/__init__.py +32 -0
  35. gothos/mcp/models.py +182 -0
  36. gothos/mcp/server.py +222 -0
  37. gothos/mcp/tools.py +347 -0
  38. gothos/memory/__init__.py +15 -0
  39. gothos/memory/action_recorder.py +181 -0
  40. gothos/memory/app_navigator.py +188 -0
  41. gothos/memory/models.py +90 -0
  42. gothos/memory/retriever.py +181 -0
  43. gothos/memory/store.py +489 -0
  44. gothos/planner/__init__.py +7 -0
  45. gothos/planner/planner.py +329 -0
  46. gothos/planner/prompts.py +103 -0
  47. gothos/py.typed +0 -0
  48. gothos/safety/__init__.py +42 -0
  49. gothos/safety/action_classifier.py +359 -0
  50. gothos/safety/confirmation.py +382 -0
  51. gothos/safety/dialog_handler.py +424 -0
  52. gothos/safety/policies.py +242 -0
  53. gothos/screen/__init__.py +15 -0
  54. gothos/screen/analyzer.py +219 -0
  55. gothos/screen/models.py +49 -0
  56. gothos-0.2.0.dist-info/METADATA +460 -0
  57. gothos-0.2.0.dist-info/RECORD +60 -0
  58. gothos-0.2.0.dist-info/WHEEL +4 -0
  59. gothos-0.2.0.dist-info/entry_points.txt +2 -0
  60. 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,5 @@
1
+ """Allow ``python -m gothos`` to invoke the CLI."""
2
+
3
+ from gothos.cli import main
4
+
5
+ main()
@@ -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]