allternit-computer-driver 0.1.0__tar.gz
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.
- allternit_computer_driver-0.1.0/PKG-INFO +47 -0
- allternit_computer_driver-0.1.0/README.md +36 -0
- allternit_computer_driver-0.1.0/allternit_computer_driver/__init__.py +15 -0
- allternit_computer_driver-0.1.0/allternit_computer_driver/anthropic.py +179 -0
- allternit_computer_driver-0.1.0/allternit_computer_driver/client.py +123 -0
- allternit_computer_driver-0.1.0/allternit_computer_driver/gemini.py +91 -0
- allternit_computer_driver-0.1.0/allternit_computer_driver/openai.py +102 -0
- allternit_computer_driver-0.1.0/allternit_computer_driver/py.typed +0 -0
- allternit_computer_driver-0.1.0/allternit_computer_driver.egg-info/PKG-INFO +47 -0
- allternit_computer_driver-0.1.0/allternit_computer_driver.egg-info/SOURCES.txt +13 -0
- allternit_computer_driver-0.1.0/allternit_computer_driver.egg-info/dependency_links.txt +1 -0
- allternit_computer_driver-0.1.0/allternit_computer_driver.egg-info/requires.txt +3 -0
- allternit_computer_driver-0.1.0/allternit_computer_driver.egg-info/top_level.txt +1 -0
- allternit_computer_driver-0.1.0/pyproject.toml +24 -0
- allternit_computer_driver-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: allternit-computer-driver
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Drop-in drivers for Allternit hosted computers: a plain client plus Anthropic (computer/browser toolset), OpenAI computer-use and Gemini computer_use adapters.
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Project-URL: Documentation, https://docs.allternit.com/api/platform/sdks/python
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Provides-Extra: anthropic
|
|
10
|
+
Requires-Dist: anthropic; extra == "anthropic"
|
|
11
|
+
|
|
12
|
+
# allternit-computer-driver
|
|
13
|
+
|
|
14
|
+
Drive an Allternit hosted computer (`/v1/computers`) from Claude, OpenAI computer use, or Gemini computer use. Standard library only; `pip install allternit-computer-driver[anthropic]` adds the Anthropic SDK. Needs a project key with the `computers` scope and the hosted driver turned on for the project.
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from allternit_computer_driver import AllternitComputers
|
|
18
|
+
client = AllternitComputers() # reads ALLTERNIT_API_KEY
|
|
19
|
+
computer = client.create(name="agent-1")
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Claude
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from allternit_computer_driver.anthropic import AllternitComputerToolset
|
|
26
|
+
toolset = AllternitComputerToolset(client, computer["id"], on_approval=lambda approval: ask_a_person(approval))
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Each member runs on the server through `POST /v1/computers/{id}/toolset`. A 409 `approval_required` calls `on_approval`: `True` approves it and resends with the grant, `False` returns the held result to the model as an error. When the installed `anthropic` package has `anthropic.tools.computer` / `anthropic.tools.browser`, these classes subclass its abstract toolsets. Older releases get a small stand-in with `to_dict()` and `tool_result(tool_use)` for a hand-written loop.
|
|
30
|
+
|
|
31
|
+
## OpenAI computer use
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from allternit_computer_driver import run_openai_action
|
|
35
|
+
step = run_openai_action(client, computer_id, call.call_id, call.action.model_dump(), display={"width": 1280, "height": 800})
|
|
36
|
+
next_input.append(step["output"])
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Gemini computer use
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from allternit_computer_driver import run_gemini_call
|
|
43
|
+
step = run_gemini_call(client, computer_id, fc.name, dict(fc.args))
|
|
44
|
+
# step["function_response"] and step["inline_data"] (0-999 coords sent as normalized_1000)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The OpenAI and Gemini adapters raise `ApprovalRequiredError`; call `client.approve()` and resend. All other failures raise `AllternitApiError` with `status`, `code` and `type`.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# allternit-computer-driver
|
|
2
|
+
|
|
3
|
+
Drive an Allternit hosted computer (`/v1/computers`) from Claude, OpenAI computer use, or Gemini computer use. Standard library only; `pip install allternit-computer-driver[anthropic]` adds the Anthropic SDK. Needs a project key with the `computers` scope and the hosted driver turned on for the project.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from allternit_computer_driver import AllternitComputers
|
|
7
|
+
client = AllternitComputers() # reads ALLTERNIT_API_KEY
|
|
8
|
+
computer = client.create(name="agent-1")
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Claude
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from allternit_computer_driver.anthropic import AllternitComputerToolset
|
|
15
|
+
toolset = AllternitComputerToolset(client, computer["id"], on_approval=lambda approval: ask_a_person(approval))
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Each member runs on the server through `POST /v1/computers/{id}/toolset`. A 409 `approval_required` calls `on_approval`: `True` approves it and resends with the grant, `False` returns the held result to the model as an error. When the installed `anthropic` package has `anthropic.tools.computer` / `anthropic.tools.browser`, these classes subclass its abstract toolsets. Older releases get a small stand-in with `to_dict()` and `tool_result(tool_use)` for a hand-written loop.
|
|
19
|
+
|
|
20
|
+
## OpenAI computer use
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from allternit_computer_driver import run_openai_action
|
|
24
|
+
step = run_openai_action(client, computer_id, call.call_id, call.action.model_dump(), display={"width": 1280, "height": 800})
|
|
25
|
+
next_input.append(step["output"])
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Gemini computer use
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from allternit_computer_driver import run_gemini_call
|
|
32
|
+
step = run_gemini_call(client, computer_id, fc.name, dict(fc.args))
|
|
33
|
+
# step["function_response"] and step["inline_data"] (0-999 coords sent as normalized_1000)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The OpenAI and Gemini adapters raise `ApprovalRequiredError`; call `client.approve()` and resend. All other failures raise `AllternitApiError` with `status`, `code` and `type`.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Drive Allternit hosted computers from Claude, OpenAI computer use or Gemini computer use.
|
|
2
|
+
|
|
3
|
+
The Anthropic toolsets are in ``allternit_computer_driver.anthropic`` so importing this package
|
|
4
|
+
never needs the optional ``anthropic`` dependency."""
|
|
5
|
+
|
|
6
|
+
from .client import DEFAULT_BASE_URL, AllternitApiError, AllternitComputers, ApprovalRequiredError, result_image, result_text
|
|
7
|
+
from .gemini import gemini_keys, gemini_to_calls, run_gemini_call
|
|
8
|
+
from .openai import openai_keys, openai_to_calls, run_openai_action
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"AllternitComputers", "AllternitApiError", "ApprovalRequiredError", "DEFAULT_BASE_URL", "result_image", "result_text",
|
|
14
|
+
"gemini_keys", "gemini_to_calls", "run_gemini_call", "openai_keys", "openai_to_calls", "run_openai_action",
|
|
15
|
+
]
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Anthropic toolset drivers for computer_toolset_20260801 / browser_toolset_20260801.
|
|
2
|
+
|
|
3
|
+
Each member call runs on an Allternit hosted computer via ``POST /v1/computers/{id}/toolset``.
|
|
4
|
+
When the installed ``anthropic`` package ships the abstract toolsets (``anthropic.tools.computer``
|
|
5
|
+
/ ``anthropic.tools.browser``), these classes subclass them and override ``execute``. Older
|
|
6
|
+
releases (anthropic <= 0.125.0 has none) get a small stand-in base with the same ``execute``
|
|
7
|
+
hook plus ``to_dict()`` and ``tool_result(tool_use)`` for a hand-written loop.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
from typing import Any, Callable, Dict, Optional
|
|
15
|
+
|
|
16
|
+
from .client import AllternitComputers, ApprovalRequiredError, ToolsetResult, result_image, result_text
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _sdk_base(module: str, names: tuple) -> Optional[type]:
|
|
20
|
+
try:
|
|
21
|
+
mod = __import__(module, fromlist=["_"])
|
|
22
|
+
except ImportError:
|
|
23
|
+
return None
|
|
24
|
+
for n in names:
|
|
25
|
+
if hasattr(mod, n):
|
|
26
|
+
return getattr(mod, n)
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _ToolError(Exception):
|
|
31
|
+
def __init__(self, content: Any) -> None:
|
|
32
|
+
super().__init__(content if isinstance(content, str) else "toolset error")
|
|
33
|
+
self.content = content
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
try: # The SDK's ToolError, so its runner reports is_error the same way.
|
|
37
|
+
from anthropic.lib.tools import ToolError as _SDKToolError # type: ignore
|
|
38
|
+
|
|
39
|
+
ToolError: Any = _SDKToolError
|
|
40
|
+
except ImportError:
|
|
41
|
+
ToolError = _ToolError
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class _StandIn:
|
|
45
|
+
type = ""
|
|
46
|
+
toolset_name = ""
|
|
47
|
+
|
|
48
|
+
def __init__(self, **_: Any) -> None:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
52
|
+
return {"type": self.type}
|
|
53
|
+
|
|
54
|
+
def tool_result(self, tool_use: Dict[str, Any]) -> Dict[str, Any]:
|
|
55
|
+
base = {"type": "tool_result", "tool_use_id": tool_use["id"], "toolset_name": self.toolset_name}
|
|
56
|
+
try:
|
|
57
|
+
out = self.execute(None, tool_use["name"], tool_use.get("input") or {}) # type: ignore[attr-defined]
|
|
58
|
+
except Exception as e: # noqa: BLE001
|
|
59
|
+
content = getattr(e, "content", str(e))
|
|
60
|
+
return {**base, "content": content, "is_error": True}
|
|
61
|
+
if isinstance(out, dict) and "data" in out:
|
|
62
|
+
out = [{"type": "image", "source": {"type": "base64", "media_type": out.get("media_type", "image/png"), "data": out["data"]}}]
|
|
63
|
+
elif isinstance(out, (dict, list)):
|
|
64
|
+
out = json.dumps(out)
|
|
65
|
+
return {**base, "content": out or "Done."}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
_ComputerBase: Any = _sdk_base("anthropic.tools.computer", ("BetaAbstractComputerToolset20260801", "AbstractComputerToolset20260801")) or type("_ComputerBase", (_StandIn,), {"type": "computer_toolset_20260801", "toolset_name": "computer"})
|
|
69
|
+
_BrowserBase: Any = _sdk_base("anthropic.tools.browser", ("BetaAbstractBrowserToolset20260801", "AbstractBrowserToolset20260801")) or type("_BrowserBase", (_StandIn,), {"type": "browser_toolset_20260801", "toolset_name": "browser"})
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _server_decides(*_: Any) -> bool:
|
|
73
|
+
"""Default ``confirm``: the Allternit server applies member policy and holds risky calls (409)."""
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class _Driver:
|
|
78
|
+
def _setup(self, client: AllternitComputers, computer_id: str, on_approval: Optional[Callable[[Dict[str, Any]], bool]], call_ids: Optional[Callable[[Any, str], Dict[str, Any]]], browser_session_id: Optional[str]) -> None:
|
|
79
|
+
self._client, self._computer_id = client, computer_id
|
|
80
|
+
self._on_approval, self._call_ids, self._session = on_approval, call_ids, browser_session_id
|
|
81
|
+
|
|
82
|
+
def _call(self, toolset: str, ctx: Any, member: str, input: Any) -> ToolsetResult:
|
|
83
|
+
# The SDK only calls members its toolset config turned on: that is the
|
|
84
|
+
# opt-in the executor asks for on off-by-default members.
|
|
85
|
+
call: Dict[str, Any] = {"toolset": toolset, "member": member, "input": dict(input or {}), "enable": [member]}
|
|
86
|
+
if self._call_ids:
|
|
87
|
+
call.update(self._call_ids(ctx, member) or {})
|
|
88
|
+
if self._session:
|
|
89
|
+
call["browser_session_id"] = self._session
|
|
90
|
+
try:
|
|
91
|
+
res = self._client.toolset(self._computer_id, call)
|
|
92
|
+
except ApprovalRequiredError as e:
|
|
93
|
+
if self._on_approval and self._on_approval(e.approval):
|
|
94
|
+
self._client.approve(self._computer_id, e.approval["id"])
|
|
95
|
+
res = self._client.toolset(self._computer_id, {**call, "approval_grant": e.approval["id"]})
|
|
96
|
+
else:
|
|
97
|
+
res = e.result
|
|
98
|
+
if res.get("is_error"):
|
|
99
|
+
raise ToolError(_blocks(res))
|
|
100
|
+
return res
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _blocks(r: ToolsetResult) -> Any:
|
|
104
|
+
out = [
|
|
105
|
+
{"type": "text", "text": b["text"]} if b.get("type") == "text" else {"type": "image", "source": {"type": "base64", "media_type": b["media_type"], "data": b["data"]}}
|
|
106
|
+
for b in r.get("content", [])
|
|
107
|
+
]
|
|
108
|
+
return out or "The action failed."
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _shot(r: ToolsetResult) -> Dict[str, str]:
|
|
112
|
+
img = result_image(r)
|
|
113
|
+
if not img:
|
|
114
|
+
raise ToolError("The computer returned no screenshot.")
|
|
115
|
+
return {"data": img["data"], "media_type": img["media_type"]}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _json(r: ToolsetResult) -> Any:
|
|
119
|
+
try:
|
|
120
|
+
return json.loads(result_text(r))
|
|
121
|
+
except ValueError:
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class AllternitComputerToolset(_Driver, _ComputerBase):
|
|
126
|
+
def __init__(self, client: AllternitComputers, computer_id: str, on_approval: Optional[Callable[[Dict[str, Any]], bool]] = None, call_ids: Optional[Callable[[Any, str], Dict[str, Any]]] = None, **sdk_options: Any) -> None:
|
|
127
|
+
sdk_options.setdefault("confirm", _server_decides)
|
|
128
|
+
_ComputerBase.__init__(self, **sdk_options)
|
|
129
|
+
self._setup(client, computer_id, on_approval, call_ids, None)
|
|
130
|
+
|
|
131
|
+
def execute(self, ctx: Any, name: str, input: Any) -> Any:
|
|
132
|
+
r = self._call("computer", ctx, name, input)
|
|
133
|
+
if name in ("screenshot", "zoom"):
|
|
134
|
+
return _shot(r)
|
|
135
|
+
if name == "cursor_position":
|
|
136
|
+
j = _json(r)
|
|
137
|
+
if isinstance(j, dict) and "x" in j:
|
|
138
|
+
return {"x": j["x"], "y": j["y"]}
|
|
139
|
+
m = re.search(r"(-?\d+)\D+(-?\d+)", result_text(r))
|
|
140
|
+
if not m:
|
|
141
|
+
raise ToolError("The computer returned no cursor position.")
|
|
142
|
+
return {"x": int(m.group(1)), "y": int(m.group(2))}
|
|
143
|
+
return result_text(r) or None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class AllternitBrowserToolset(_Driver, _BrowserBase):
|
|
147
|
+
def __init__(self, client: AllternitComputers, computer_id: str, on_approval: Optional[Callable[[Dict[str, Any]], bool]] = None, call_ids: Optional[Callable[[Any, str], Dict[str, Any]]] = None, browser_session_id: Optional[str] = None, **sdk_options: Any) -> None:
|
|
148
|
+
self._state: Optional[Dict[str, Any]] = None
|
|
149
|
+
sdk_options.setdefault("confirm", _server_decides)
|
|
150
|
+
sdk_options.setdefault("browser_state", self._current_state)
|
|
151
|
+
_BrowserBase.__init__(self, **sdk_options)
|
|
152
|
+
self._setup(client, computer_id, on_approval, call_ids, browser_session_id)
|
|
153
|
+
|
|
154
|
+
def _current_state(self, ctx: Any = None) -> Dict[str, Any]:
|
|
155
|
+
if self._state is None:
|
|
156
|
+
r = self._call("browser", ctx, "list_tabs", {})
|
|
157
|
+
self._state = r.get("browser_state") or {"tabs": _json(r) or []}
|
|
158
|
+
return self._state
|
|
159
|
+
|
|
160
|
+
def execute(self, ctx: Any, name: str, input: Any) -> Any:
|
|
161
|
+
r = self._call("browser", ctx, name, input)
|
|
162
|
+
if r.get("browser_state"):
|
|
163
|
+
self._state = r["browser_state"]
|
|
164
|
+
if name in ("screenshot", "zoom"):
|
|
165
|
+
return _shot(r)
|
|
166
|
+
j, tabs = _json(r), (self._state or {}).get("tabs", [])
|
|
167
|
+
if name == "navigate":
|
|
168
|
+
return j if isinstance(j, dict) and j.get("url") else {"url": tabs[0]["url"] if tabs else str((input or {}).get("url", ""))}
|
|
169
|
+
if name == "list_tabs":
|
|
170
|
+
return j if isinstance(j, list) else tabs
|
|
171
|
+
if name in ("new_tab", "switch_tab"):
|
|
172
|
+
if isinstance(j, dict) and j.get("tab_id"):
|
|
173
|
+
return j
|
|
174
|
+
if tabs:
|
|
175
|
+
return tabs[0]
|
|
176
|
+
raise ToolError("The browser returned no tab.")
|
|
177
|
+
if name == "close_tab":
|
|
178
|
+
return None
|
|
179
|
+
return result_text(r) or None
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Plain client for the Allternit hosted computer API (/v1/computers). Standard library only."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import uuid
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
from urllib.error import HTTPError, URLError
|
|
10
|
+
from urllib.parse import quote, urlencode
|
|
11
|
+
from urllib.request import Request, urlopen
|
|
12
|
+
|
|
13
|
+
DEFAULT_BASE_URL = "https://api.allternit.com"
|
|
14
|
+
DEFAULT_TIMEOUT = 60.0
|
|
15
|
+
|
|
16
|
+
ToolsetResult = Dict[str, Any]
|
|
17
|
+
"""``{is_error, content:[{type:"text",text}|{type:"image",media_type,data}], browser_state?, screen?, error?}``"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AllternitApiError(Exception):
|
|
21
|
+
def __init__(self, status: int, error: Dict[str, Any], body: Any) -> None:
|
|
22
|
+
super().__init__(error.get("message") or f"Allternit API error {status}")
|
|
23
|
+
self.status = status
|
|
24
|
+
self.type = error.get("type", "api_error")
|
|
25
|
+
self.code = error.get("code", "unknown")
|
|
26
|
+
self.param = error.get("param")
|
|
27
|
+
self.body = body
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ApprovalRequiredError(AllternitApiError):
|
|
31
|
+
"""409 ``approval_required``: the call is held until someone approves it."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, error: Dict[str, Any], approval: Dict[str, Any], result: ToolsetResult, body: Any) -> None:
|
|
34
|
+
super().__init__(409, error, body)
|
|
35
|
+
self.approval = approval
|
|
36
|
+
self.result = result
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AllternitComputers:
|
|
40
|
+
def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None, timeout: float = DEFAULT_TIMEOUT) -> None:
|
|
41
|
+
key = api_key or os.environ.get("ALLTERNIT_API_KEY")
|
|
42
|
+
if not key:
|
|
43
|
+
raise ValueError("AllternitComputers: api_key is required (or set ALLTERNIT_API_KEY).")
|
|
44
|
+
self._api_key = key
|
|
45
|
+
self.base_url = (base_url or os.environ.get("ALLTERNIT_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
|
|
46
|
+
self.timeout = timeout
|
|
47
|
+
|
|
48
|
+
def create(self, name: Optional[str] = None, account_id: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
49
|
+
body = {k: v for k, v in {"name": name, "account_id": account_id, "metadata": metadata}.items() if v is not None}
|
|
50
|
+
return self._req("POST", "/v1/computers", body)
|
|
51
|
+
|
|
52
|
+
def list(self, limit: Optional[int] = None, after: Optional[str] = None) -> Dict[str, Any]:
|
|
53
|
+
return self._req("GET", "/v1/computers" + _qs(limit=limit, after=after))
|
|
54
|
+
|
|
55
|
+
def get(self, computer_id: str) -> Dict[str, Any]:
|
|
56
|
+
return self._req("GET", f"/v1/computers/{quote(computer_id)}")
|
|
57
|
+
|
|
58
|
+
def start(self, computer_id: str) -> Dict[str, Any]:
|
|
59
|
+
return self._req("POST", f"/v1/computers/{quote(computer_id)}/start", {})
|
|
60
|
+
|
|
61
|
+
def stop(self, computer_id: str) -> Dict[str, Any]:
|
|
62
|
+
return self._req("POST", f"/v1/computers/{quote(computer_id)}/stop", {})
|
|
63
|
+
|
|
64
|
+
def delete(self, computer_id: str) -> Dict[str, Any]:
|
|
65
|
+
return self._req("DELETE", f"/v1/computers/{quote(computer_id)}")
|
|
66
|
+
|
|
67
|
+
def toolset(self, computer_id: str, call: Dict[str, Any]) -> ToolsetResult:
|
|
68
|
+
"""Run one member. Action failures return ``is_error: True``; a held call raises ApprovalRequiredError."""
|
|
69
|
+
return self._req("POST", f"/v1/computers/{quote(computer_id)}/toolset", call)
|
|
70
|
+
|
|
71
|
+
def schema(self, computer_id: str, toolset: str = "computer") -> Dict[str, Any]:
|
|
72
|
+
return self._req("GET", f"/v1/computers/{quote(computer_id)}/toolset/schema" + _qs(toolset=toolset))
|
|
73
|
+
|
|
74
|
+
def events(self, computer_id: str, after: Optional[str] = None, limit: Optional[int] = None) -> Dict[str, Any]:
|
|
75
|
+
return self._req("GET", f"/v1/computers/{quote(computer_id)}/events" + _qs(after=after, limit=limit))
|
|
76
|
+
|
|
77
|
+
def approve(self, computer_id: str, approval_id: str) -> Dict[str, Any]:
|
|
78
|
+
return self._req("POST", f"/v1/computers/{quote(computer_id)}/approvals/{quote(approval_id)}", {})
|
|
79
|
+
|
|
80
|
+
def _req(self, method: str, path: str, body: Any = None) -> Any:
|
|
81
|
+
headers = {"Authorization": f"Bearer {self._api_key}", "Accept": "application/json"}
|
|
82
|
+
data = None
|
|
83
|
+
if body is not None:
|
|
84
|
+
headers["Content-Type"] = "application/json"
|
|
85
|
+
data = json.dumps(body).encode()
|
|
86
|
+
if method == "POST":
|
|
87
|
+
headers["Idempotency-Key"] = str(uuid.uuid4())
|
|
88
|
+
req = Request(self.base_url + path, data=data, method=method, headers=headers)
|
|
89
|
+
try:
|
|
90
|
+
with urlopen(req, timeout=self.timeout) as res:
|
|
91
|
+
text = res.read().decode()
|
|
92
|
+
return json.loads(text) if text else None
|
|
93
|
+
except HTTPError as e:
|
|
94
|
+
text = e.read().decode(errors="replace")
|
|
95
|
+
try:
|
|
96
|
+
payload = json.loads(text) if text else {}
|
|
97
|
+
except ValueError:
|
|
98
|
+
payload = {"error": {"message": text}}
|
|
99
|
+
err = payload.get("error") or {}
|
|
100
|
+
if e.code == 409 and err.get("code") == "approval_required" and payload.get("approval"):
|
|
101
|
+
raise ApprovalRequiredError(err, payload["approval"], payload.get("result") or {"is_error": True, "content": []}, payload) from None
|
|
102
|
+
raise AllternitApiError(e.code, err, payload) from None
|
|
103
|
+
except URLError as e:
|
|
104
|
+
raise AllternitApiError(0, {"type": "connection_error", "code": "connection_error", "message": str(e.reason)}, None) from None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def result_text(r: ToolsetResult) -> str:
|
|
108
|
+
return "\n".join(b["text"] for b in r.get("content", []) if b.get("type") == "text")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def result_image(r: ToolsetResult) -> Optional[Dict[str, str]]:
|
|
112
|
+
for b in r.get("content", []):
|
|
113
|
+
if b.get("type") == "image":
|
|
114
|
+
return {"media_type": b["media_type"], "data": b["data"]}
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _qs(**q: Any) -> str:
|
|
119
|
+
p = [(k, str(v)) for k, v in q.items() if v not in (None, "")]
|
|
120
|
+
return "?" + urlencode(p) if p else ""
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
__all__: List[str] = ["AllternitComputers", "AllternitApiError", "ApprovalRequiredError", "DEFAULT_BASE_URL", "result_text", "result_image"]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Gemini computer_use adapter. Gemini coordinates are 0-999 normalized, so every call
|
|
2
|
+
is sent with ``coordinate_space: "normalized_1000"`` and the server scales to the screen."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any, Dict, List
|
|
7
|
+
|
|
8
|
+
from .client import AllternitComputers, result_image, result_text
|
|
9
|
+
|
|
10
|
+
Step = Dict[str, Any]
|
|
11
|
+
_MAP = {"control": "ctrl", "meta": "super", "command": "super", "enter": "Return", "escape": "Escape", "backspace": "BackSpace", "tab": "Tab", "delete": "Delete"}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def gemini_keys(keys: str) -> str:
|
|
15
|
+
"""``"Control+Shift+T"`` -> ``"ctrl+shift+t"``."""
|
|
16
|
+
parts = [k.strip() for k in keys.split("+")]
|
|
17
|
+
return "+".join(_MAP.get(k.lower(), k.lower() if len(k) == 1 else k) for k in parts)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _pt(x: Any, y: Any) -> List[int]:
|
|
21
|
+
return [round(float(x)), round(float(y))]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _key(text: str) -> Step:
|
|
25
|
+
return {"member": "key", "input": {"text": text}}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _type(text: str) -> Step:
|
|
29
|
+
return {"member": "type", "input": {"text": text}}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _go(url: str) -> List[Step]:
|
|
33
|
+
return [_key("ctrl+l"), _type(url), _key("Return")]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def gemini_to_calls(name: str, args: Dict[str, Any], search_url: str = "https://www.google.com") -> List[Step]:
|
|
37
|
+
a = args or {}
|
|
38
|
+
d = str(a.get("direction", "down"))
|
|
39
|
+
if name == "open_web_browser":
|
|
40
|
+
return []
|
|
41
|
+
if name == "wait_5_seconds":
|
|
42
|
+
return [{"member": "wait", "input": {"duration": 5}}]
|
|
43
|
+
if name == "go_back":
|
|
44
|
+
return [_key("alt+Left")]
|
|
45
|
+
if name == "go_forward":
|
|
46
|
+
return [_key("alt+Right")]
|
|
47
|
+
if name == "search":
|
|
48
|
+
return _go(search_url)
|
|
49
|
+
if name == "navigate":
|
|
50
|
+
return _go(str(a.get("url", "")))
|
|
51
|
+
if name == "click_at":
|
|
52
|
+
return [{"member": "left_click", "input": {"coordinate": _pt(a["x"], a["y"])}}]
|
|
53
|
+
if name == "hover_at":
|
|
54
|
+
return [{"member": "mouse_move", "input": {"coordinate": _pt(a["x"], a["y"])}}]
|
|
55
|
+
if name == "type_text_at":
|
|
56
|
+
steps: List[Step] = [{"member": "left_click", "input": {"coordinate": _pt(a["x"], a["y"])}}]
|
|
57
|
+
if a.get("clear_before_typing", True):
|
|
58
|
+
steps += [_key("ctrl+a"), _key("BackSpace")]
|
|
59
|
+
steps.append(_type(str(a.get("text", ""))))
|
|
60
|
+
if a.get("press_enter", True):
|
|
61
|
+
steps.append(_key("Return"))
|
|
62
|
+
return steps
|
|
63
|
+
if name == "key_combination":
|
|
64
|
+
return [_key(gemini_keys(str(a.get("keys", ""))))]
|
|
65
|
+
if name == "scroll_document":
|
|
66
|
+
return [{"member": "scroll", "input": {"coordinate": [500, 500], "scroll_direction": d, "scroll_amount": 5}}]
|
|
67
|
+
if name == "scroll_at":
|
|
68
|
+
amount = max(1, round(float(a.get("magnitude", 800)) / 160))
|
|
69
|
+
return [{"member": "scroll", "input": {"coordinate": _pt(a["x"], a["y"]), "scroll_direction": d, "scroll_amount": amount}}]
|
|
70
|
+
if name == "drag_and_drop":
|
|
71
|
+
return [{"member": "left_click_drag", "input": {"start_coordinate": _pt(a["x"], a["y"]), "coordinate": _pt(a["destination_x"], a["destination_y"])}}]
|
|
72
|
+
raise ValueError(f"Unsupported Gemini computer_use function: {name}")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def run_gemini_call(client: AllternitComputers, computer_id: str, name: str, args: Dict[str, Any], search_url: str = "https://www.google.com") -> Dict[str, Any]:
|
|
76
|
+
"""Returns ``{"function_response": {name, response}, "inline_data": {mime_type, data}, "error": ...}``."""
|
|
77
|
+
base = {"toolset": "computer", "coordinate_space": "normalized_1000"}
|
|
78
|
+
error = None
|
|
79
|
+
for step in gemini_to_calls(name, args, search_url):
|
|
80
|
+
r = client.toolset(computer_id, {**base, **step})
|
|
81
|
+
if r.get("is_error"):
|
|
82
|
+
error = r
|
|
83
|
+
break
|
|
84
|
+
s = client.toolset(computer_id, {**base, "member": "screenshot", "input": {}})
|
|
85
|
+
img = result_image(s)
|
|
86
|
+
if not img:
|
|
87
|
+
raise RuntimeError("Allternit computer returned no screenshot.")
|
|
88
|
+
response: Dict[str, Any] = {"url": ""}
|
|
89
|
+
if error:
|
|
90
|
+
response["error"] = result_text(error)
|
|
91
|
+
return {"function_response": {"name": name, "response": response}, "inline_data": {"mime_type": img["media_type"], "data": img["data"]}, "error": error}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""OpenAI computer-use adapter: run one ``computer_call`` action on an Allternit hosted
|
|
2
|
+
computer and return a ``computer_call_output`` item carrying the next screenshot."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from .client import AllternitComputers, result_image
|
|
9
|
+
|
|
10
|
+
_KEYS = {
|
|
11
|
+
"CTRL": "ctrl", "CONTROL": "ctrl", "ALT": "alt", "OPTION": "alt", "SHIFT": "shift", "CMD": "super", "META": "super",
|
|
12
|
+
"SUPER": "super", "WIN": "super", "ENTER": "Return", "RETURN": "Return", "ESC": "Escape", "ESCAPE": "Escape",
|
|
13
|
+
"TAB": "Tab", "SPACE": "space", "BACKSPACE": "BackSpace", "DELETE": "Delete", "UP": "Up", "DOWN": "Down",
|
|
14
|
+
"LEFT": "Left", "RIGHT": "Right", "ARROWUP": "Up", "ARROWDOWN": "Down", "ARROWLEFT": "Left", "ARROWRIGHT": "Right",
|
|
15
|
+
"HOME": "Home", "END": "End", "PAGEUP": "Page_Up", "PAGEDOWN": "Page_Down",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
Step = Dict[str, Any]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def openai_keys(keys: List[str]) -> str:
|
|
22
|
+
"""``["CTRL","C"]`` -> ``"ctrl+c"``."""
|
|
23
|
+
return "+".join(_KEYS.get(k.upper(), k.lower() if len(k) == 1 else k) for k in keys)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _c(x: Any, y: Any) -> List[int]:
|
|
27
|
+
return [round(float(x)), round(float(y))]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def openai_to_calls(a: Dict[str, Any], per_click: int = 100) -> List[Step]:
|
|
31
|
+
t = a.get("type")
|
|
32
|
+
if t == "click":
|
|
33
|
+
b = a.get("button", "left")
|
|
34
|
+
if b == "back":
|
|
35
|
+
return [{"member": "key", "input": {"text": "alt+Left"}}]
|
|
36
|
+
if b == "forward":
|
|
37
|
+
return [{"member": "key", "input": {"text": "alt+Right"}}]
|
|
38
|
+
member = {"right": "right_click", "wheel": "middle_click"}.get(b, "left_click")
|
|
39
|
+
return [{"member": member, "input": {"coordinate": _c(a["x"], a["y"])}}]
|
|
40
|
+
if t == "double_click":
|
|
41
|
+
return [{"member": "double_click", "input": {"coordinate": _c(a["x"], a["y"])}}]
|
|
42
|
+
if t == "drag":
|
|
43
|
+
p = a.get("path") or []
|
|
44
|
+
if len(p) < 2:
|
|
45
|
+
return []
|
|
46
|
+
if len(p) == 2:
|
|
47
|
+
return [{"member": "left_click_drag", "input": {"start_coordinate": _c(p[0]["x"], p[0]["y"]), "coordinate": _c(p[1]["x"], p[1]["y"])}}]
|
|
48
|
+
return (
|
|
49
|
+
[{"member": "mouse_move", "input": {"coordinate": _c(p[0]["x"], p[0]["y"])}}, {"member": "left_mouse_down", "input": {}}]
|
|
50
|
+
+ [{"member": "mouse_move", "input": {"coordinate": _c(q["x"], q["y"])}} for q in p[1:]]
|
|
51
|
+
+ [{"member": "left_mouse_up", "input": {}}]
|
|
52
|
+
)
|
|
53
|
+
if t == "keypress":
|
|
54
|
+
return [{"member": "key", "input": {"text": openai_keys(a.get("keys") or [])}}]
|
|
55
|
+
if t == "move":
|
|
56
|
+
return [{"member": "mouse_move", "input": {"coordinate": _c(a["x"], a["y"])}}]
|
|
57
|
+
if t == "scroll":
|
|
58
|
+
out: List[Step] = []
|
|
59
|
+
amt = lambda v: max(1, round(abs(v) / per_click)) # noqa: E731
|
|
60
|
+
sx, sy = a.get("scroll_x", 0) or 0, a.get("scroll_y", 0) or 0
|
|
61
|
+
if sy:
|
|
62
|
+
out.append({"member": "scroll", "input": {"coordinate": _c(a["x"], a["y"]), "scroll_direction": "down" if sy > 0 else "up", "scroll_amount": amt(sy)}})
|
|
63
|
+
if sx:
|
|
64
|
+
out.append({"member": "scroll", "input": {"coordinate": _c(a["x"], a["y"]), "scroll_direction": "right" if sx > 0 else "left", "scroll_amount": amt(sx)}})
|
|
65
|
+
return out
|
|
66
|
+
if t == "type":
|
|
67
|
+
return [{"member": "type", "input": {"text": a.get("text", "")}}]
|
|
68
|
+
if t == "wait":
|
|
69
|
+
return [{"member": "wait", "input": {"duration": max(1, round((a.get("ms") or 2000) / 1000))}}]
|
|
70
|
+
if t == "screenshot":
|
|
71
|
+
return []
|
|
72
|
+
raise ValueError(f"Unsupported OpenAI computer action: {t}")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def run_openai_action(
|
|
76
|
+
client: AllternitComputers,
|
|
77
|
+
computer_id: str,
|
|
78
|
+
call_id: str,
|
|
79
|
+
action: Dict[str, Any],
|
|
80
|
+
display: Optional[Dict[str, int]] = None,
|
|
81
|
+
pixels_per_scroll_click: int = 100,
|
|
82
|
+
) -> Dict[str, Any]:
|
|
83
|
+
"""Returns ``{"output": <computer_call_output item>, "error": <failed result or None>}``."""
|
|
84
|
+
frame = {"model_frame": display, "coordinate_space": "pixels"} if display else {}
|
|
85
|
+
error = None
|
|
86
|
+
for step in openai_to_calls(action, pixels_per_scroll_click):
|
|
87
|
+
r = client.toolset(computer_id, {"toolset": "computer", **step, **frame})
|
|
88
|
+
if r.get("is_error"):
|
|
89
|
+
error = r
|
|
90
|
+
break
|
|
91
|
+
s = client.toolset(computer_id, {"toolset": "computer", "member": "screenshot", "input": {}, **frame})
|
|
92
|
+
img = result_image(s)
|
|
93
|
+
if not img:
|
|
94
|
+
raise RuntimeError("Allternit computer returned no screenshot.")
|
|
95
|
+
return {
|
|
96
|
+
"output": {
|
|
97
|
+
"type": "computer_call_output",
|
|
98
|
+
"call_id": call_id,
|
|
99
|
+
"output": {"type": "computer_screenshot", "image_url": f"data:{img['media_type']};base64,{img['data']}"},
|
|
100
|
+
},
|
|
101
|
+
"error": error,
|
|
102
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: allternit-computer-driver
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Drop-in drivers for Allternit hosted computers: a plain client plus Anthropic (computer/browser toolset), OpenAI computer-use and Gemini computer_use adapters.
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Project-URL: Documentation, https://docs.allternit.com/api/platform/sdks/python
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Provides-Extra: anthropic
|
|
10
|
+
Requires-Dist: anthropic; extra == "anthropic"
|
|
11
|
+
|
|
12
|
+
# allternit-computer-driver
|
|
13
|
+
|
|
14
|
+
Drive an Allternit hosted computer (`/v1/computers`) from Claude, OpenAI computer use, or Gemini computer use. Standard library only; `pip install allternit-computer-driver[anthropic]` adds the Anthropic SDK. Needs a project key with the `computers` scope and the hosted driver turned on for the project.
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from allternit_computer_driver import AllternitComputers
|
|
18
|
+
client = AllternitComputers() # reads ALLTERNIT_API_KEY
|
|
19
|
+
computer = client.create(name="agent-1")
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Claude
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from allternit_computer_driver.anthropic import AllternitComputerToolset
|
|
26
|
+
toolset = AllternitComputerToolset(client, computer["id"], on_approval=lambda approval: ask_a_person(approval))
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Each member runs on the server through `POST /v1/computers/{id}/toolset`. A 409 `approval_required` calls `on_approval`: `True` approves it and resends with the grant, `False` returns the held result to the model as an error. When the installed `anthropic` package has `anthropic.tools.computer` / `anthropic.tools.browser`, these classes subclass its abstract toolsets. Older releases get a small stand-in with `to_dict()` and `tool_result(tool_use)` for a hand-written loop.
|
|
30
|
+
|
|
31
|
+
## OpenAI computer use
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from allternit_computer_driver import run_openai_action
|
|
35
|
+
step = run_openai_action(client, computer_id, call.call_id, call.action.model_dump(), display={"width": 1280, "height": 800})
|
|
36
|
+
next_input.append(step["output"])
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Gemini computer use
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from allternit_computer_driver import run_gemini_call
|
|
43
|
+
step = run_gemini_call(client, computer_id, fc.name, dict(fc.args))
|
|
44
|
+
# step["function_response"] and step["inline_data"] (0-999 coords sent as normalized_1000)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The OpenAI and Gemini adapters raise `ApprovalRequiredError`; call `client.approve()` and resend. All other failures raise `AllternitApiError` with `status`, `code` and `type`.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
allternit_computer_driver/__init__.py
|
|
4
|
+
allternit_computer_driver/anthropic.py
|
|
5
|
+
allternit_computer_driver/client.py
|
|
6
|
+
allternit_computer_driver/gemini.py
|
|
7
|
+
allternit_computer_driver/openai.py
|
|
8
|
+
allternit_computer_driver/py.typed
|
|
9
|
+
allternit_computer_driver.egg-info/PKG-INFO
|
|
10
|
+
allternit_computer_driver.egg-info/SOURCES.txt
|
|
11
|
+
allternit_computer_driver.egg-info/dependency_links.txt
|
|
12
|
+
allternit_computer_driver.egg-info/requires.txt
|
|
13
|
+
allternit_computer_driver.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
allternit_computer_driver
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "allternit-computer-driver"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Drop-in drivers for Allternit hosted computers: a plain client plus Anthropic (computer/browser toolset), OpenAI computer-use and Gemini computer_use adapters."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "Apache-2.0" }
|
|
12
|
+
dependencies = []
|
|
13
|
+
|
|
14
|
+
[project.optional-dependencies]
|
|
15
|
+
anthropic = ["anthropic"]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Documentation = "https://docs.allternit.com/api/platform/sdks/python"
|
|
19
|
+
|
|
20
|
+
[tool.setuptools]
|
|
21
|
+
packages = ["allternit_computer_driver"]
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.package-data]
|
|
24
|
+
allternit_computer_driver = ["py.typed"]
|