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.
@@ -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
+ }
@@ -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
+ 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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+